Serenity Operating System
at master 613 lines 16 kB view raw
1/* 2 * Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/LexicalPath.h> 8#include <LibCore/ArgsParser.h> 9#include <LibCore/System.h> 10#include <LibLine/Editor.h> 11#include <LibMain/Main.h> 12#include <csignal> 13#include <stdio.h> 14#include <sys/ioctl.h> 15#include <termios.h> 16#include <unistd.h> 17 18static struct termios g_save; 19 20// Flag set by a SIGWINCH signal handler to notify the main loop that the window has been resized. 21static Atomic<bool> g_resized { false }; 22 23static ErrorOr<void> setup_tty(bool switch_buffer) 24{ 25 // Save previous tty settings. 26 g_save = TRY(Core::System::tcgetattr(STDOUT_FILENO)); 27 28 struct termios raw = g_save; 29 raw.c_lflag &= ~(ECHO | ICANON); 30 31 // Disable echo and line buffering 32 TRY(Core::System::tcsetattr(STDOUT_FILENO, TCSAFLUSH, raw)); 33 34 if (switch_buffer) { 35 // Save cursor and switch to alternate buffer. 36 out("\e[s\e[?1047h"); 37 } 38 39 return {}; 40} 41 42static ErrorOr<void> teardown_tty(bool switch_buffer) 43{ 44 TRY(Core::System::tcsetattr(STDOUT_FILENO, TCSAFLUSH, g_save)); 45 46 if (switch_buffer) { 47 out("\e[?1047l\e[u"); 48 } 49 50 return {}; 51} 52 53static Vector<StringView> wrap_line(DeprecatedString const& string, size_t width) 54{ 55 auto const result = Line::Editor::actual_rendered_string_metrics(string, {}, width); 56 57 Vector<StringView> spans; 58 size_t span_start = 0; 59 for (auto const& line_metric : result.line_metrics) { 60 VERIFY(line_metric.bit_length.has_value()); 61 auto const bit_length = line_metric.bit_length.value(); 62 spans.append(string.substring_view(span_start, bit_length)); 63 span_start += bit_length; 64 } 65 66 return spans; 67} 68 69class Pager { 70public: 71 Pager(StringView filename, FILE* file, FILE* tty, StringView prompt) 72 : m_file(file) 73 , m_tty(tty) 74 , m_filename(filename) 75 , m_prompt(prompt) 76 { 77 } 78 79 void up() 80 { 81 up_n(1); 82 } 83 84 void down() 85 { 86 down_n(1); 87 } 88 89 void up_page() 90 { 91 up_n(m_height - 1); 92 } 93 94 void down_page() 95 { 96 down_n(m_height - 1); 97 } 98 99 void up_n(size_t n) 100 { 101 if (m_line == 0 && m_subline == 0) 102 return; 103 104 line_subline_add(m_line, m_subline, -n); 105 106 full_redraw(); 107 } 108 109 void down_n(size_t n) 110 { 111 if (at_end()) 112 return; 113 114 clear_status(); 115 116 read_enough_for_line(m_line + n); 117 118 size_t real_n = line_subline_add(m_line, m_subline, n); 119 120 // If we are moving less than a screen down, just draw the extra lines 121 // for efficiency and more(1) compatibility. 122 if (n < m_height - 1) { 123 size_t line = m_line; 124 size_t subline = m_subline; 125 line_subline_add(line, subline, (m_height - 1) - real_n, false); 126 write_range(line, subline, real_n); 127 } else { 128 write_range(m_line, m_subline, m_height - 1); 129 } 130 131 status_line(); 132 133 fflush(m_tty); 134 } 135 136 void top() 137 { 138 m_line = 0; 139 m_subline = 0; 140 full_redraw(); 141 } 142 143 void bottom() 144 { 145 while (read_line()) 146 ; 147 148 m_line = end_line(); 149 m_subline = end_subline(); 150 full_redraw(); 151 } 152 153 void up_half_page() 154 { 155 up_n(m_height / 2); 156 } 157 158 void down_half_page() 159 { 160 down_n(m_height / 2); 161 } 162 163 void go_to_line(size_t line_num) 164 { 165 read_enough_for_line(line_num); 166 167 m_line = line_num; 168 m_subline = 0; 169 bound_cursor(); 170 full_redraw(); 171 } 172 173 void init() 174 { 175 resize(false); 176 } 177 178 void resize(bool clear = true) 179 { 180 // First, we get the current size of the window. 181 struct winsize window; 182 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) == -1) { 183 perror("ioctl(2)"); 184 return; 185 } 186 187 auto original_height = m_height; 188 189 m_width = window.ws_col; 190 m_height = window.ws_row; 191 192 // If the window is now larger than it was before, read more lines of 193 // the file so that there is enough data to fill the whole screen. 194 // 195 // m_height is initialized to 0, so if the terminal was 80x25 when 196 // this is called for the first time, then additional_lines will be 80 197 // and 80 lines of text will be buffered. 198 auto additional_lines = m_height - original_height; 199 while (additional_lines > 0) { 200 if (!read_line()) { 201 // End of file has been reached. 202 break; 203 } 204 --additional_lines; 205 } 206 207 reflow(); 208 bound_cursor(); 209 210 // Next, we repaint the whole screen. We need to figure out what line was at the top 211 // of the screen, and seek there and re-display everything again. 212 if (clear) { 213 full_redraw(); 214 } else { 215 redraw(); 216 } 217 } 218 219 size_t write_range(size_t line, size_t subline, size_t length) 220 { 221 size_t lines = 0; 222 for (size_t i = line; i < m_lines.size(); ++i) { 223 for (auto string : sublines(i)) { 224 if (subline > 0) { 225 --subline; 226 continue; 227 } 228 if (lines >= length) 229 return lines; 230 231 outln(m_tty, "{}", string); 232 ++lines; 233 } 234 } 235 236 return lines; 237 } 238 239 void clear_status() 240 { 241 out(m_tty, "\e[2K\r"); 242 } 243 244 void status_line() 245 { 246 out(m_tty, "\e[0;7m "); 247 render_status_line(m_prompt); 248 out(m_tty, " \e[0m"); 249 } 250 251 bool read_line() 252 { 253 char* line = nullptr; 254 size_t n = 0; 255 ssize_t size = getline(&line, &n, m_file); 256 ScopeGuard guard([line] { 257 free(line); 258 }); 259 260 if (size == -1) 261 return false; 262 263 // Strip trailing newline. 264 if (line[size - 1] == '\n') 265 --size; 266 267 m_lines.append(DeprecatedString(line, size)); 268 return true; 269 } 270 271 bool at_end() 272 { 273 return feof(m_file) && m_line == end_line() && m_subline == end_subline(); 274 } 275 276private: 277 void redraw() 278 { 279 write_range(m_line, m_subline, m_height - 1); 280 status_line(); 281 fflush(m_tty); 282 } 283 284 void full_redraw() 285 { 286 out("\e[2J\e[0G\e[0d"); 287 redraw(); 288 } 289 290 void read_enough_for_line(size_t line) 291 { 292 // This might read a bounded number of extra lines. 293 while (m_lines.size() < line + m_height) { 294 if (!read_line()) 295 break; 296 } 297 } 298 299 size_t render_status_line(StringView prompt, size_t off = 0, char end = '\0', bool ignored = false) 300 { 301 for (; off < prompt.length() && prompt[off] != end; ++off) { 302 if (ignored) 303 continue; 304 305 if (off + 1 >= prompt.length()) { 306 // Don't parse any multi-character sequences if we are at the end of input. 307 out(m_tty, "{}", prompt[off]); 308 continue; 309 } 310 311 switch (prompt[off]) { 312 case '?': 313 switch (prompt[++off]) { 314 case 'f': 315 off = render_status_line(prompt, off + 1, ':', m_file == stdin); 316 off = render_status_line(prompt, off + 1, '.', m_file != stdin); 317 break; 318 case 'e': 319 off = render_status_line(prompt, off + 1, ':', !at_end()); 320 off = render_status_line(prompt, off + 1, '.', at_end()); 321 break; 322 default: 323 // Unknown flags are never true. 324 off = render_status_line(prompt, off + 1, ':', true); 325 off = render_status_line(prompt, off + 1, '.', false); 326 } 327 break; 328 case '%': 329 switch (prompt[++off]) { 330 case 'f': 331 out(m_tty, "{}", m_filename); 332 break; 333 case 'l': 334 out(m_tty, "{}", m_line + 1); 335 break; 336 default: 337 out(m_tty, "?"); 338 } 339 break; 340 case '\\': 341 ++off; 342 [[fallthrough]]; 343 default: 344 out(m_tty, "{}", prompt[off]); 345 } 346 } 347 return off; 348 } 349 350 Vector<StringView> const& sublines(size_t line) 351 { 352 return m_subline_cache.ensure(line, [&]() { 353 return wrap_line(m_lines[line], m_width); 354 }); 355 } 356 357 size_t line_subline_add(size_t& line, size_t& subline, int delta, bool bounded = true) 358 { 359 int unit = delta / AK::abs(delta); 360 size_t i; 361 for (i = 0; i < (size_t)AK::abs(delta); ++i) { 362 if (subline == 0 && unit == -1) { 363 if (line == 0) 364 return i; 365 366 line--; 367 subline = sublines(line).size() - 1; 368 } else if (subline == sublines(line).size() - 1 && unit == 1) { 369 if (bounded && feof(m_file) && line == end_line() && subline == end_subline()) 370 return i; 371 372 if (line >= m_lines.size() - 1) 373 return i; 374 375 line++; 376 subline = 0; 377 } else { 378 subline += unit; 379 } 380 } 381 return i; 382 } 383 384 void bound_cursor() 385 { 386 if (!feof(m_file)) 387 return; 388 389 if (m_line == end_line() && m_subline >= end_subline()) { 390 m_subline = end_subline(); 391 } else if (m_line > end_line()) { 392 m_line = end_line(); 393 m_subline = end_subline(); 394 } 395 } 396 397 void calculate_end() 398 { 399 if (m_lines.is_empty()) { 400 m_end_line = 0; 401 m_end_subline = 0; 402 return; 403 } 404 size_t end_line = m_lines.size() - 1; 405 size_t end_subline = sublines(end_line).size() - 1; 406 line_subline_add(end_line, end_subline, -(m_height - 1), false); 407 m_end_line = end_line; 408 m_end_subline = end_subline; 409 } 410 411 // Only valid after all lines are read. 412 size_t end_line() 413 { 414 if (!m_end_line.has_value()) 415 calculate_end(); 416 417 return m_end_line.value(); 418 } 419 420 // Only valid after all lines are read. 421 size_t end_subline() 422 { 423 if (!m_end_subline.has_value()) 424 calculate_end(); 425 426 return m_end_subline.value(); 427 } 428 429 void reflow() 430 { 431 m_subline_cache.clear(); 432 m_end_line = {}; 433 m_end_subline = {}; 434 435 m_subline = 0; 436 } 437 438 // FIXME: Don't save scrollback when emulating more. 439 Vector<DeprecatedString> m_lines; 440 441 size_t m_line { 0 }; 442 size_t m_subline { 0 }; 443 444 HashMap<size_t, Vector<StringView>> m_subline_cache; 445 Optional<size_t> m_end_line; 446 Optional<size_t> m_end_subline; 447 448 FILE* m_file; 449 FILE* m_tty; 450 451 size_t m_width { 0 }; 452 size_t m_height { 0 }; 453 454 DeprecatedString m_filename; 455 DeprecatedString m_prompt; 456}; 457 458/// Return the next key sequence, or nothing if a signal is received while waiting 459/// to read the next sequence. 460static Optional<DeprecatedString> get_key_sequence() 461{ 462 // We need a buffer to handle ansi sequences. 463 char buff[8]; 464 465 ssize_t n = read(STDOUT_FILENO, buff, sizeof(buff)); 466 if (n > 0) { 467 return DeprecatedString(buff, n); 468 } else { 469 return {}; 470 } 471} 472 473static void cat_file(FILE* file) 474{ 475 Array<u8, 4096> buffer; 476 while (!feof(file)) { 477 size_t n = fread(buffer.data(), 1, buffer.size(), file); 478 if (n == 0 && ferror(file)) { 479 perror("fread"); 480 exit(1); 481 } 482 483 n = fwrite(buffer.data(), 1, n, stdout); 484 if (n == 0 && ferror(stdout)) { 485 perror("fwrite"); 486 exit(1); 487 } 488 } 489} 490 491ErrorOr<int> serenity_main(Main::Arguments arguments) 492{ 493 TRY(Core::System::pledge("stdio rpath tty sigaction")); 494 495 // FIXME: Make these into StringViews once we stop using fopen below. 496 DeprecatedString filename = "-"; 497 DeprecatedString prompt = "?f%f :.(line %l)?e (END):."; 498 bool dont_switch_buffer = false; 499 bool quit_at_eof = false; 500 bool emulate_more = false; 501 502 if (LexicalPath::basename(arguments.strings[0]) == "more"sv) 503 emulate_more = true; 504 505 Core::ArgsParser args_parser; 506 args_parser.add_positional_argument(filename, "The paged file", "file", Core::ArgsParser::Required::No); 507 args_parser.add_option(prompt, "Prompt line", "prompt", 'P', "Prompt"); 508 args_parser.add_option(dont_switch_buffer, "Don't use xterm alternate buffer", "no-init", 'X'); 509 args_parser.add_option(quit_at_eof, "Exit when the end of the file is reached", "quit-at-eof", 'e'); 510 args_parser.add_option(emulate_more, "Pretend that we are more(1)", "emulate-more", 'm'); 511 args_parser.parse(arguments); 512 513 FILE* file; 514 if (DeprecatedString("-") == filename) { 515 file = stdin; 516 } else if ((file = fopen(filename.characters(), "r")) == nullptr) { 517 perror("fopen"); 518 exit(1); 519 } 520 521 // On SIGWINCH set this flag so that the main-loop knows when the terminal 522 // has been resized. 523 signal(SIGWINCH, [](auto) { 524 g_resized = true; 525 }); 526 527 TRY(Core::System::pledge("stdio tty")); 528 529 if (emulate_more) { 530 // Configure options that match more's behavior 531 dont_switch_buffer = true; 532 quit_at_eof = true; 533 prompt = "--More--"; 534 } 535 536 if (!isatty(STDOUT_FILENO)) { 537 cat_file(file); 538 return 0; 539 } 540 541 TRY(setup_tty(!dont_switch_buffer)); 542 543 Pager pager(filename, file, stdout, prompt); 544 pager.init(); 545 546 StringBuilder modifier_buffer = StringBuilder(10); 547 for (Optional<DeprecatedString> sequence_value;; sequence_value = get_key_sequence()) { 548 if (g_resized) { 549 g_resized = false; 550 pager.resize(); 551 } 552 553 if (!sequence_value.has_value()) { 554 continue; 555 } 556 557 auto const& sequence = sequence_value.value(); 558 559 if (sequence.to_uint().has_value()) { 560 modifier_buffer.append(sequence); 561 } else { 562 if (sequence == "" || sequence == "q") { 563 break; 564 } else if (sequence == "j" || sequence == "\e[B" || sequence == "\n") { 565 if (!emulate_more) { 566 if (!modifier_buffer.is_empty()) 567 pager.down_n(modifier_buffer.to_deprecated_string().to_uint().value_or(1)); 568 else 569 pager.down(); 570 } 571 } else if (sequence == "k" || sequence == "\e[A") { 572 if (!emulate_more) { 573 if (!modifier_buffer.is_empty()) 574 pager.up_n(modifier_buffer.to_deprecated_string().to_uint().value_or(1)); 575 else 576 pager.up(); 577 } 578 } else if (sequence == "g") { 579 if (!emulate_more) { 580 if (!modifier_buffer.is_empty()) 581 pager.go_to_line(modifier_buffer.to_deprecated_string().to_uint().value()); 582 else 583 pager.top(); 584 } 585 } else if (sequence == "G") { 586 if (!emulate_more) { 587 if (!modifier_buffer.is_empty()) 588 pager.go_to_line(modifier_buffer.to_deprecated_string().to_uint().value()); 589 else 590 pager.bottom(); 591 } 592 } else if (sequence == " " || sequence == "f" || sequence == "\e[6~") { 593 pager.down_page(); 594 } else if ((sequence == "\e[5~" || sequence == "b") && !emulate_more) { 595 pager.up_page(); 596 } else if (sequence == "d") { 597 pager.down_half_page(); 598 } else if (sequence == "u" && !emulate_more) { 599 pager.up_half_page(); 600 } 601 602 modifier_buffer.clear(); 603 } 604 605 if (quit_at_eof && pager.at_end()) 606 break; 607 } 608 609 pager.clear_status(); 610 611 TRY(teardown_tty(!dont_switch_buffer)); 612 return 0; 613}