Serenity Operating System
at portability 560 lines 20 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include "LineEditor.h" 28#include "GlobalState.h" 29#include <ctype.h> 30#include <stdio.h> 31#include <sys/ioctl.h> 32#include <unistd.h> 33 34LineEditor::LineEditor() 35{ 36 struct winsize ws; 37 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) 38 m_num_columns = 80; 39 else 40 m_num_columns = ws.ws_col; 41} 42 43LineEditor::~LineEditor() 44{ 45} 46 47void LineEditor::add_to_history(const String& line) 48{ 49 if ((m_history.size() + 1) > m_history_capacity) 50 m_history.take_first(); 51 m_history.append(line); 52} 53 54void LineEditor::clear_line() 55{ 56 for (size_t i = 0; i < m_cursor; ++i) 57 fputc(0x8, stdout); 58 fputs("\033[K", stdout); 59 fflush(stdout); 60 m_buffer.clear(); 61 m_cursor = 0; 62} 63 64void LineEditor::insert(const String& string) 65{ 66 fputs(string.characters(), stdout); 67 fflush(stdout); 68 69 if (m_cursor == (size_t)m_buffer.size()) { 70 m_buffer.append(string.characters(), (int)string.length()); 71 m_cursor = (size_t)m_buffer.size(); 72 return; 73 } 74 75 vt_save_cursor(); 76 vt_clear_to_end_of_line(); 77 for (size_t i = m_cursor; i < (size_t)m_buffer.size(); ++i) 78 fputc(m_buffer[(int)i], stdout); 79 vt_restore_cursor(); 80 81 m_buffer.ensure_capacity(m_buffer.size() + (int)string.length()); 82 for (size_t i = 0; i < string.length(); ++i) 83 m_buffer.insert((int)m_cursor + (int)i, string[i]); 84 m_cursor += string.length(); 85} 86 87void LineEditor::insert(const char ch) 88{ 89 putchar(ch); 90 fflush(stdout); 91 92 if (m_cursor == (size_t)m_buffer.size()) { 93 m_buffer.append(ch); 94 m_cursor = (size_t)m_buffer.size(); 95 return; 96 } 97 98 vt_save_cursor(); 99 vt_clear_to_end_of_line(); 100 for (size_t i = m_cursor; i < (size_t)m_buffer.size(); ++i) 101 fputc(m_buffer[(int)i], stdout); 102 vt_restore_cursor(); 103 104 m_buffer.insert((int)m_cursor, ch); 105 ++m_cursor; 106} 107 108void LineEditor::cache_path() 109{ 110 if (!m_path.is_empty()) 111 m_path.clear_with_capacity(); 112 113 String path = getenv("PATH"); 114 if (path.is_empty()) 115 return; 116 117 auto directories = path.split(':'); 118 for (const auto& directory : directories) { 119 Core::DirIterator programs(directory.characters(), Core::DirIterator::SkipDots); 120 while (programs.has_next()) { 121 auto program = programs.next_path(); 122 String program_path = String::format("%s/%s", directory.characters(), program.characters()); 123 struct stat program_status; 124 int stat_error = stat(program_path.characters(), &program_status); 125 if (!stat_error && (program_status.st_mode & S_IXUSR)) 126 m_path.append(program.characters()); 127 } 128 } 129 130 quick_sort(m_path.begin(), m_path.end(), AK::is_less_than<String>); 131} 132 133void LineEditor::cut_mismatching_chars(String& completion, const String& other, size_t start_compare) 134{ 135 size_t i = start_compare; 136 while (i < completion.length() && i < other.length() && completion[i] == other[i]) 137 ++i; 138 completion = completion.substring(0, i); 139} 140 141// Function returns Vector<String> as assignment is made from return value at callsite 142// (instead of StringView) 143Vector<String> LineEditor::tab_complete_first_token(const String& token) 144{ 145 auto match = binary_search(m_path.data(), m_path.size(), token, [](const String& token, const String& program) -> int { 146 return strncmp(token.characters(), program.characters(), token.length()); 147 }); 148 if (!match) 149 return Vector<String>(); 150 151 String completion = *match; 152 Vector<String> suggestions; 153 154 // Now that we have a program name starting with our token, we look at 155 // other program names starting with our token and cut off any mismatching 156 // characters. 157 158 bool seen_others = false; 159 int index = match - m_path.data(); 160 for (int i = index - 1; i >= 0 && m_path[i].starts_with(token); --i) { 161 suggestions.append(m_path[i]); 162 cut_mismatching_chars(completion, m_path[i], token.length()); 163 seen_others = true; 164 } 165 for (size_t i = index + 1; i < m_path.size() && m_path[i].starts_with(token); ++i) { 166 cut_mismatching_chars(completion, m_path[i], token.length()); 167 suggestions.append(m_path[i]); 168 seen_others = true; 169 } 170 suggestions.append(m_path[index]); 171 172 // If we have characters to add, add them. 173 if (completion.length() > token.length()) 174 insert(completion.substring(token.length(), completion.length() - token.length())); 175 // If we have a single match, we add a space, unless we already have one. 176 if (!seen_others && (m_cursor == (size_t)m_buffer.size() || m_buffer[(int)m_cursor] != ' ')) 177 insert(' '); 178 179 return suggestions; 180} 181 182Vector<String> LineEditor::tab_complete_other_token(String& token) 183{ 184 String path; 185 Vector<String> suggestions; 186 187 int last_slash = (int)token.length() - 1; 188 while (last_slash >= 0 && token[last_slash] != '/') 189 --last_slash; 190 191 if (last_slash >= 0) { 192 // Split on the last slash. We'll use the first part as the directory 193 // to search and the second part as the token to complete. 194 path = token.substring(0, (size_t)last_slash + 1); 195 if (path[0] != '/') 196 path = String::format("%s/%s", g.cwd.characters(), path.characters()); 197 path = canonicalized_path(path); 198 token = token.substring((size_t)last_slash + 1, token.length() - (size_t)last_slash - 1); 199 } else { 200 // We have no slashes, so the directory to search is the current 201 // directory and the token to complete is just the original token. 202 path = g.cwd; 203 } 204 205 // This is a bit naughty, but necessary without reordering the loop 206 // below. The loop terminates early, meaning that 207 // the suggestions list is incomplete. 208 // We only do this if the token is empty though. 209 if (token.is_empty()) { 210 Core::DirIterator suggested_files(path, Core::DirIterator::SkipDots); 211 while (suggested_files.has_next()) { 212 suggestions.append(suggested_files.next_path()); 213 } 214 } 215 216 String completion; 217 218 bool seen_others = false; 219 Core::DirIterator files(path, Core::DirIterator::SkipDots); 220 while (files.has_next()) { 221 auto file = files.next_path(); 222 if (file.starts_with(token)) { 223 if (!token.is_empty()) 224 suggestions.append(file); 225 if (completion.is_empty()) { 226 completion = file; // Will only be set once. 227 } else { 228 cut_mismatching_chars(completion, file, token.length()); 229 if (completion.is_empty()) // We cut everything off! 230 return suggestions; 231 seen_others = true; 232 } 233 } 234 } 235 if (completion.is_empty()) 236 return suggestions; 237 238 // If we have characters to add, add them. 239 if (completion.length() > token.length()) 240 insert(completion.substring(token.length(), completion.length() - token.length())); 241 // If we have a single match and it's a directory, we add a slash. If it's 242 // a regular file, we add a space, unless we already have one. 243 if (!seen_others) { 244 String file_path = String::format("%s/%s", path.characters(), completion.characters()); 245 struct stat program_status; 246 int stat_error = stat(file_path.characters(), &program_status); 247 if (!stat_error) { 248 if (S_ISDIR(program_status.st_mode)) 249 insert('/'); 250 else if (m_cursor == (size_t)m_buffer.size() || m_buffer[(int)m_cursor] != ' ') 251 insert(' '); 252 } 253 } 254 255 return {}; // Return an empty vector 256} 257 258String LineEditor::get_line(const String& prompt) 259{ 260 fputs(prompt.characters(), stdout); 261 fflush(stdout); 262 263 m_history_cursor = m_history.size(); 264 m_cursor = 0; 265 for (;;) { 266 char keybuf[16]; 267 ssize_t nread = read(0, keybuf, sizeof(keybuf)); 268 // FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead. 269 if (nread == 0) 270 exit(0); 271 if (nread < 0) { 272 if (errno == EINTR) { 273 if (g.was_interrupted) { 274 g.was_interrupted = false; 275 if (!m_buffer.is_empty()) 276 printf("^C"); 277 } 278 if (g.was_resized) { 279 g.was_resized = false; 280 printf("\033[2K\r"); 281 m_buffer.clear(); 282 283 struct winsize ws; 284 int rc = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); 285 ASSERT(rc == 0); 286 m_num_columns = ws.ws_col; 287 288 return String::empty(); 289 } 290 m_buffer.clear(); 291 putchar('\n'); 292 return String::empty(); 293 } 294 perror("read failed"); 295 // FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead. 296 exit(2); 297 } 298 299 auto do_delete = [&] { 300 if (m_cursor == (size_t)m_buffer.size()) { 301 fputc('\a', stdout); 302 fflush(stdout); 303 return; 304 } 305 m_buffer.remove((int)m_cursor - 1); 306 fputs("\033[3~", stdout); 307 fflush(stdout); 308 vt_save_cursor(); 309 vt_clear_to_end_of_line(); 310 for (size_t i = m_cursor; i < (size_t)m_buffer.size(); ++i) 311 fputc(m_buffer[i], stdout); 312 vt_restore_cursor(); 313 }; 314 315 for (ssize_t i = 0; i < nread; ++i) { 316 char ch = keybuf[i]; 317 if (ch == 0) 318 continue; 319 320 switch (m_state) { 321 case InputState::ExpectBracket: 322 if (ch == '[') { 323 m_state = InputState::ExpectFinal; 324 continue; 325 } else { 326 m_state = InputState::Free; 327 break; 328 } 329 case InputState::ExpectFinal: 330 switch (ch) { 331 case 'A': // up 332 if (m_history_cursor > 0) 333 --m_history_cursor; 334 clear_line(); 335 if (m_history_cursor < m_history.size()) 336 insert(m_history[m_history_cursor]); 337 m_state = InputState::Free; 338 continue; 339 case 'B': // down 340 if (m_history_cursor < m_history.size()) 341 ++m_history_cursor; 342 clear_line(); 343 if (m_history_cursor < m_history.size()) 344 insert(m_history[m_history_cursor]); 345 m_state = InputState::Free; 346 continue; 347 case 'D': // left 348 if (m_cursor > 0) { 349 --m_cursor; 350 fputs("\033[D", stdout); 351 fflush(stdout); 352 } 353 m_state = InputState::Free; 354 continue; 355 case 'C': // right 356 if (m_cursor < (size_t)m_buffer.size()) { 357 ++m_cursor; 358 fputs("\033[C", stdout); 359 fflush(stdout); 360 } 361 m_state = InputState::Free; 362 continue; 363 case 'H': 364 if (m_cursor > 0) { 365 fprintf(stdout, "\033[%zuD", m_cursor); 366 fflush(stdout); 367 m_cursor = 0; 368 } 369 m_state = InputState::Free; 370 continue; 371 case 'F': 372 if (m_cursor < (size_t)m_buffer.size()) { 373 fprintf(stdout, "\033[%zuC", (size_t)m_buffer.size() - m_cursor); 374 fflush(stdout); 375 m_cursor = (size_t)m_buffer.size(); 376 } 377 m_state = InputState::Free; 378 continue; 379 case '3': 380 do_delete(); 381 m_state = InputState::ExpectTerminator; 382 continue; 383 default: 384 dbgprintf("Shell: Unhandled final: %b (%c)\n", ch, ch); 385 m_state = InputState::Free; 386 continue; 387 } 388 break; 389 case InputState::ExpectTerminator: 390 m_state = InputState::Free; 391 continue; 392 case InputState::Free: 393 if (ch == 27) { 394 m_state = InputState::ExpectBracket; 395 continue; 396 } 397 break; 398 } 399 400 if (ch == '\t') { 401 bool is_empty_token = m_cursor == 0 || m_buffer[(int)m_cursor - 1] == ' '; 402 m_times_tab_pressed++; 403 404 int token_start = (int)m_cursor - 1; 405 if (!is_empty_token) { 406 while (token_start >= 0 && m_buffer[token_start] != ' ') 407 --token_start; 408 ++token_start; 409 } 410 411 bool is_first_token = true; 412 for (int i = token_start - 1; i >= 0; --i) { 413 if (m_buffer[i] != ' ') { 414 is_first_token = false; 415 break; 416 } 417 } 418 419 String token = is_empty_token ? String() : String(&m_buffer[token_start], m_cursor - (size_t)token_start); 420 Vector<String> suggestions; 421 422 if (is_first_token) 423 suggestions = tab_complete_first_token(token); 424 else 425 suggestions = tab_complete_other_token(token); 426 427 if (m_times_tab_pressed > 1 && !suggestions.is_empty()) { 428 size_t longest_suggestion_length = 0; 429 430 for (auto& suggestion : suggestions) 431 longest_suggestion_length = max(longest_suggestion_length, suggestion.length()); 432 433 size_t num_printed = 0; 434 putchar('\n'); 435 for (auto& suggestion : suggestions) { 436 size_t next_column = num_printed + suggestion.length() + longest_suggestion_length + 2; 437 438 if (next_column > m_num_columns) { 439 putchar('\n'); 440 num_printed = 0; 441 } 442 443 num_printed += fprintf(stderr, "%-*s", static_cast<int>(longest_suggestion_length) + 2, suggestion.characters()); 444 } 445 446 putchar('\n'); 447 write(STDOUT_FILENO, prompt.characters(), prompt.length()); 448 write(STDOUT_FILENO, m_buffer.data(), m_cursor); 449 // Prevent not printing characters in case the user has moved the cursor and then pressed tab 450 write(STDOUT_FILENO, m_buffer.data() + m_cursor, m_buffer.size() - m_cursor); 451 m_cursor = m_buffer.size(); // bash doesn't do this, but it makes a little bit more sense 452 } 453 454 suggestions.clear_with_capacity(); 455 continue; 456 } 457 458 m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB 459 460 auto do_backspace = [&] { 461 if (m_cursor == 0) { 462 fputc('\a', stdout); 463 fflush(stdout); 464 return; 465 } 466 m_buffer.remove((int)m_cursor - 1); 467 --m_cursor; 468 putchar(8); 469 vt_save_cursor(); 470 vt_clear_to_end_of_line(); 471 for (size_t i = m_cursor; i < (size_t)m_buffer.size(); ++i) 472 fputc(m_buffer[(int)i], stdout); 473 vt_restore_cursor(); 474 }; 475 476 if (ch == 8 || ch == g.termios.c_cc[VERASE]) { 477 do_backspace(); 478 continue; 479 } 480 if (ch == g.termios.c_cc[VWERASE]) { 481 bool has_seen_nonspace = false; 482 while (m_cursor > 0) { 483 if (isspace(m_buffer[(int)m_cursor - 1])) { 484 if (has_seen_nonspace) 485 break; 486 } else { 487 has_seen_nonspace = true; 488 } 489 do_backspace(); 490 } 491 continue; 492 } 493 if (ch == g.termios.c_cc[VKILL]) { 494 while (m_cursor > 0) 495 do_backspace(); 496 continue; 497 } 498 if (ch == 0xc) { // ^L 499 printf("\033[3J\033[H\033[2J"); // Clear screen. 500 fputs(prompt.characters(), stdout); 501 for (size_t i = 0; i < m_buffer.size(); ++i) 502 fputc(m_buffer[i], stdout); 503 if (m_cursor < (size_t)m_buffer.size()) 504 printf("\033[%zuD", (size_t)m_buffer.size() - m_cursor); // Move cursor N steps left. 505 fflush(stdout); 506 continue; 507 } 508 if (ch == 0x01) { // ^A 509 if (m_cursor > 0) { 510 printf("\033[%zuD", m_cursor); 511 fflush(stdout); 512 m_cursor = 0; 513 } 514 continue; 515 } 516 if (ch == g.termios.c_cc[VEOF]) { // Normally ^D 517 if (m_buffer.is_empty()) { 518 printf("<EOF>\n"); 519 exit(0); 520 } 521 continue; 522 } 523 if (ch == 0x05) { // ^E 524 if (m_cursor < (size_t)m_buffer.size()) { 525 printf("\033[%zuC", (size_t)m_buffer.size() - m_cursor); 526 fflush(stdout); 527 m_cursor = (size_t)m_buffer.size(); 528 } 529 continue; 530 } 531 if (ch == '\n') { 532 putchar('\n'); 533 fflush(stdout); 534 auto string = String::copy(m_buffer); 535 m_buffer.clear(); 536 return string; 537 } 538 539 insert(ch); 540 } 541 } 542} 543 544void LineEditor::vt_save_cursor() 545{ 546 fputs("\033[s", stdout); 547 fflush(stdout); 548} 549 550void LineEditor::vt_restore_cursor() 551{ 552 fputs("\033[u", stdout); 553 fflush(stdout); 554} 555 556void LineEditor::vt_clear_to_end_of_line() 557{ 558 fputs("\033[K", stdout); 559 fflush(stdout); 560}