less.cpp 17 KB

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