diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index 58ebf8795c..8849c33a81 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -4,7 +4,7 @@ list(APPEND REQUIRED_TARGETS arp base64 basename cat chmod chown chroot clear cp cut date dd df dirname dmesg du echo env expr false fgrep file find grep groups head host hostname id ifconfig kill killall ln ls mkdir mount mv nproc pgrep pidof ping pmap ps readlink realpath reboot rm rmdir seq shutdown sleep sort stat stty su tail test - touch tr true umount uname uniq uptime w wc which whoami xargs yes + touch tr true umount uname uniq uptime w wc which whoami xargs yes less ) list(APPEND RECOMMENDED_TARGETS adjtime aplay avol bt checksum chres cksum copy fortune gunzip gzip init keymap lsirq lsof lspci man mknod mktemp diff --git a/Userland/Utilities/less.cpp b/Userland/Utilities/less.cpp new file mode 100644 index 0000000000..7d3524176e --- /dev/null +++ b/Userland/Utilities/less.cpp @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2021, Peter Elliott + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct termios g_save; +static struct winsize g_wsize; + +static void setup_tty() +{ + // Save previous tty settings. + if (tcgetattr(STDOUT_FILENO, &g_save) == -1) { + perror("tcgetattr(3)"); + } + + // Get the window size. + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &g_wsize) == -1) { + perror("ioctl(2)"); + } + + struct termios raw = g_save; + raw.c_lflag &= ~(ECHO | ICANON); + + // Disable echo and line buffering + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &raw) == -1) { + perror("tcsetattr(3)"); + } + + // Save cursor and switch to alternate buffer. + out("\e[s\e[?1047h"); +} + +static void teardown_tty() +{ + if (tcsetattr(STDOUT_FILENO, TCSAFLUSH, &g_save) == -1) { + perror("tcsetattr(3)"); + } + + out("\e[?1047l\e[u"); +} + +static Vector wrap_line(Utf8View const& string, size_t width) +{ + Vector lines; + + StringBuilder builder; + size_t offset = 0; + + bool in_ansi = false; + for (auto codepoint : string) { + if (offset >= width) { + builder.append('\n'); + lines.append(builder.build()); + builder.clear(); + offset = 0; + } + + builder.append(codepoint); + + if (codepoint == '\e') + in_ansi = true; + + if (!in_ansi) + // FIXME: calcuate the printed width of the character. + offset++; + + if (isalpha(codepoint)) + in_ansi = false; + } + + if (builder.length() > 0) + lines.append(builder.build()); + + return lines; +} + +class Pager { +public: + Pager(FILE* file, FILE* tty, size_t width, size_t height) + : m_file(file) + , m_tty(tty) + , m_width(width) + , m_height(height) + { + } + + void up() + { + up_n(1); + } + + void down() + { + down_n(1); + } + + void up_page() + { + up_n(m_height - 1); + } + + void down_page() + { + down_n(m_height - 1); + } + + void up_n(size_t n) + { + if (m_line == 0) + return; + + m_line = (m_line > n) ? m_line - n : 0; + + // Clear screen and reset cursor position. + out("\e[2J\e[0G\e[0d"); + write_range(m_line, m_height - 1); + status_line(); + fflush(m_tty); + } + + void down_n(size_t n) + { + clear_status(); + + while (n - (m_lines.size() - m_line) > 0) { + if (!read_line()) + break; + } + m_line += write_range(min(m_line + m_height - 1, m_line + (m_lines.size() - m_line)), n); + status_line(); + + fflush(m_tty); + } + + void init() + { + while (m_lines.size() < m_height) { + if (!read_line()) + break; + } + write_range(0, m_height - 1); + status_line(); + m_line = 0; + fflush(m_tty); + } + + size_t write_range(size_t start, size_t length) + { + size_t lines = min(length, m_lines.size() - start); + for (size_t i = 0; i < lines; ++i) { + out(m_tty, "{}", m_lines[start + i]); + } + return lines; + } + + void clear_status() + { + out(m_tty, "\e[2K\r"); + } + + void status_line() + { + out(m_tty, "\e[7m -- less -- (line {})\e[27m", m_line + 1); + } + + bool read_line() + { + char* line = nullptr; + size_t n = 0; + ssize_t size = getline(&line, &n, m_file); + + if (size == -1) + return false; + + m_lines.extend(wrap_line(Utf8View(line), m_width)); + free(line); + return true; + } + +private: + Vector m_lines; + size_t m_line { 0 }; + FILE* m_file; + FILE* m_tty; + + size_t m_width; + size_t m_height; + + String m_filename; + String m_prompt; +}; + +static String get_key_sequence() +{ + // We need a buffer to handle ansi sequences. + char buff[8]; + ssize_t n = read(STDOUT_FILENO, buff, sizeof(buff)); + return String(buff, n); +} + +int main(int argc, char** argv) +{ + const char* filename = "-"; + Core::ArgsParser args_parser; + args_parser.add_positional_argument(filename, "The paged file", "file", Core::ArgsParser::Required::No); + args_parser.parse(argc, argv); + + FILE* file; + if (String("-") == filename) { + file = stdin; + } else { + file = fopen(filename, "r"); + } + + setup_tty(); + + Pager pager(file, stdout, g_wsize.ws_col, g_wsize.ws_row); + + pager.init(); + + for (String sequence;; sequence = get_key_sequence()) { + if (sequence == "" || sequence == "q") { + break; + } else if (sequence == "j" || sequence == "\e[B" || sequence == "\n") { + pager.down(); + } else if (sequence == "k" || sequence == "\e[A") { + pager.up(); + } else if (sequence == " ") { + pager.down_page(); + } + } + + teardown_tty(); + return 0; +}