Serenity Operating System
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}