less.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  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 efficency 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. if (size == -1)
  237. return false;
  238. // Strip trailing newline.
  239. if (line[size - 1] == '\n')
  240. --size;
  241. m_lines.append(String(line, size));
  242. free(line);
  243. return true;
  244. }
  245. bool at_end()
  246. {
  247. return feof(m_file) && m_line == end_line() && m_subline == end_subline();
  248. }
  249. private:
  250. void redraw()
  251. {
  252. write_range(m_line, m_subline, m_height - 1);
  253. status_line();
  254. fflush(m_tty);
  255. }
  256. void full_redraw()
  257. {
  258. out("\e[2J\e[0G\e[0d");
  259. redraw();
  260. }
  261. void read_enough_for_line(size_t line)
  262. {
  263. // This might read a bounded number of extra lines.
  264. while (m_lines.size() < line + m_height - 1) {
  265. if (!read_line())
  266. break;
  267. }
  268. }
  269. size_t render_status_line(StringView prompt, size_t off = 0, char end = '\0', bool ignored = false)
  270. {
  271. for (; prompt[off] != end && off < prompt.length(); ++off) {
  272. if (ignored)
  273. continue;
  274. if (off + 1 >= prompt.length()) {
  275. // Don't parse any multi-character sequences if we are at the end of input.
  276. out(m_tty, "{}", prompt[off]);
  277. continue;
  278. }
  279. switch (prompt[off]) {
  280. case '?':
  281. switch (prompt[++off]) {
  282. case 'f':
  283. off = render_status_line(prompt, off + 1, ':', m_file == stdin);
  284. off = render_status_line(prompt, off + 1, '.', m_file != stdin);
  285. break;
  286. case 'e':
  287. off = render_status_line(prompt, off + 1, ':', !at_end());
  288. off = render_status_line(prompt, off + 1, '.', at_end());
  289. break;
  290. default:
  291. // Unknown flags are never true.
  292. off = render_status_line(prompt, off + 1, ':', true);
  293. off = render_status_line(prompt, off + 1, '.', false);
  294. }
  295. break;
  296. case '%':
  297. switch (prompt[++off]) {
  298. case 'f':
  299. out(m_tty, "{}", m_filename);
  300. break;
  301. case 'l':
  302. out(m_tty, "{}", m_line);
  303. break;
  304. default:
  305. out(m_tty, "?");
  306. }
  307. break;
  308. case '\\':
  309. ++off;
  310. [[fallthrough]];
  311. default:
  312. out(m_tty, "{}", prompt[off]);
  313. }
  314. }
  315. return off;
  316. }
  317. Vector<StringView> const& sublines(size_t line)
  318. {
  319. return m_subline_cache.ensure(line, [&]() {
  320. return wrap_line(m_lines[line], m_width);
  321. });
  322. }
  323. size_t line_subline_add(size_t& line, size_t& subline, int delta, bool bounded = true)
  324. {
  325. int unit = delta / AK::abs(delta);
  326. size_t i;
  327. for (i = 0; i < (size_t)AK::abs(delta); ++i) {
  328. if (subline == 0 && unit == -1) {
  329. if (line == 0)
  330. return i;
  331. line--;
  332. subline = sublines(line).size() - 1;
  333. } else if (subline == sublines(line).size() - 1 && unit == 1) {
  334. if (bounded && feof(m_file) && line == end_line() && subline == end_subline())
  335. return i;
  336. if (line >= m_lines.size() - 1)
  337. return i;
  338. line++;
  339. subline = 0;
  340. } else {
  341. subline += unit;
  342. }
  343. }
  344. return i;
  345. }
  346. void bound_cursor()
  347. {
  348. if (!feof(m_file))
  349. return;
  350. if (m_line == end_line() && m_subline >= end_subline()) {
  351. m_subline = end_subline();
  352. } else if (m_line > end_line()) {
  353. m_line = end_line();
  354. m_subline = end_subline();
  355. }
  356. }
  357. void calculate_end()
  358. {
  359. size_t end_line = m_lines.size() - 1;
  360. size_t end_subline = sublines(end_line).size() - 1;
  361. line_subline_add(end_line, end_subline, -(m_height - 1), false);
  362. m_end_line = end_line;
  363. m_end_subline = end_subline;
  364. }
  365. // Only valid after all lines are read.
  366. size_t end_line()
  367. {
  368. if (!m_end_line.has_value())
  369. calculate_end();
  370. return m_end_line.value();
  371. }
  372. // Only valid after all lines are read.
  373. size_t end_subline()
  374. {
  375. if (!m_end_subline.has_value())
  376. calculate_end();
  377. return m_end_subline.value();
  378. }
  379. void reflow()
  380. {
  381. m_subline_cache.clear();
  382. m_end_line = {};
  383. m_end_subline = {};
  384. m_subline = 0;
  385. }
  386. // FIXME: Don't save scrollback when emulating more.
  387. Vector<String> m_lines;
  388. size_t m_line { 0 };
  389. size_t m_subline { 0 };
  390. HashMap<size_t, Vector<StringView>> m_subline_cache;
  391. Optional<size_t> m_end_line;
  392. Optional<size_t> m_end_subline;
  393. FILE* m_file;
  394. FILE* m_tty;
  395. size_t m_width { 0 };
  396. size_t m_height { 0 };
  397. String m_filename;
  398. String m_prompt;
  399. };
  400. /// Return the next key sequence, or nothing if a signal is received while waiting
  401. /// to read the next sequence.
  402. static Optional<String> get_key_sequence()
  403. {
  404. // We need a buffer to handle ansi sequences.
  405. char buff[8];
  406. ssize_t n = read(STDOUT_FILENO, buff, sizeof(buff));
  407. if (n > 0) {
  408. return String(buff, n);
  409. } else {
  410. return {};
  411. }
  412. }
  413. static void cat_file(FILE* file)
  414. {
  415. Array<u8, 4096> buffer;
  416. while (!feof(file)) {
  417. size_t n = fread(buffer.data(), 1, buffer.size(), file);
  418. if (n == 0 && ferror(file)) {
  419. perror("fread");
  420. exit(1);
  421. }
  422. n = fwrite(buffer.data(), 1, n, stdout);
  423. if (n == 0 && ferror(stdout)) {
  424. perror("fwrite");
  425. exit(1);
  426. }
  427. }
  428. }
  429. ErrorOr<int> serenity_main(Main::Arguments arguments)
  430. {
  431. TRY(Core::System::pledge("stdio rpath tty sigaction", nullptr));
  432. char const* filename = "-";
  433. char const* prompt = "?f%f :.(line %l)?e (END):.";
  434. bool dont_switch_buffer = false;
  435. bool quit_at_eof = false;
  436. bool emulate_more = false;
  437. if (LexicalPath::basename(arguments.strings[0]) == "more"sv)
  438. emulate_more = true;
  439. Core::ArgsParser args_parser;
  440. args_parser.add_positional_argument(filename, "The paged file", "file", Core::ArgsParser::Required::No);
  441. args_parser.add_option(prompt, "Prompt line", "prompt", 'P', "Prompt");
  442. args_parser.add_option(dont_switch_buffer, "Don't use xterm alternate buffer", "no-init", 'X');
  443. args_parser.add_option(quit_at_eof, "Exit when the end of the file is reached", "quit-at-eof", 'e');
  444. args_parser.add_option(emulate_more, "Pretend that we are more(1)", "emulate-more", 'm');
  445. args_parser.parse(arguments);
  446. FILE* file;
  447. if (String("-") == filename) {
  448. file = stdin;
  449. } else if ((file = fopen(filename, "r")) == nullptr) {
  450. perror("fopen");
  451. exit(1);
  452. }
  453. // On SIGWINCH set this flag so that the main-loop knows when the terminal
  454. // has been resized.
  455. signal(SIGWINCH, [](auto) {
  456. g_resized = true;
  457. });
  458. TRY(Core::System::pledge("stdio tty", nullptr));
  459. if (emulate_more) {
  460. // Configure options that match more's behavior
  461. dont_switch_buffer = true;
  462. quit_at_eof = true;
  463. prompt = "--More--";
  464. }
  465. if (!isatty(STDOUT_FILENO)) {
  466. cat_file(file);
  467. return 0;
  468. }
  469. TRY(setup_tty(!dont_switch_buffer));
  470. Pager pager(filename, file, stdout, prompt);
  471. pager.init();
  472. StringBuilder modifier_buffer = StringBuilder(10);
  473. for (Optional<String> sequence_value;; sequence_value = get_key_sequence()) {
  474. if (g_resized) {
  475. g_resized = false;
  476. pager.resize();
  477. }
  478. if (!sequence_value.has_value()) {
  479. continue;
  480. }
  481. const auto& sequence = sequence_value.value();
  482. if (sequence.to_uint().has_value()) {
  483. modifier_buffer.append(sequence);
  484. } else {
  485. if (sequence == "" || sequence == "q") {
  486. break;
  487. } else if (sequence == "j" || sequence == "\e[B" || sequence == "\n") {
  488. if (!emulate_more) {
  489. if (!modifier_buffer.is_empty())
  490. pager.down_n(modifier_buffer.build().to_uint().value_or(1));
  491. else
  492. pager.down();
  493. }
  494. } else if (sequence == "k" || sequence == "\e[A") {
  495. if (!emulate_more) {
  496. if (!modifier_buffer.is_empty())
  497. pager.up_n(modifier_buffer.build().to_uint().value_or(1));
  498. else
  499. pager.up();
  500. }
  501. } else if (sequence == "g") {
  502. if (!emulate_more) {
  503. if (!modifier_buffer.is_empty())
  504. pager.go_to_line(modifier_buffer.build().to_uint().value());
  505. else
  506. pager.top();
  507. }
  508. } else if (sequence == "G") {
  509. if (!emulate_more) {
  510. if (!modifier_buffer.is_empty())
  511. pager.go_to_line(modifier_buffer.build().to_uint().value());
  512. else
  513. pager.bottom();
  514. }
  515. } else if (sequence == " " || sequence == "\e[6~") {
  516. pager.down_page();
  517. } else if (sequence == "\e[5~" && !emulate_more) {
  518. pager.up_page();
  519. } else if (sequence == "d") {
  520. pager.down_half_page();
  521. } else if (sequence == "u" && !emulate_more) {
  522. pager.up_half_page();
  523. }
  524. modifier_buffer.clear();
  525. }
  526. if (quit_at_eof && pager.at_end())
  527. break;
  528. }
  529. pager.clear_status();
  530. TRY(teardown_tty(!dont_switch_buffer));
  531. return 0;
  532. }