less.cpp 16 KB

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