#include #include #include #include #include #include #include #include struct config_t { bool raw { false }; bool quit_if_one_screen { false }; bool no_init { false }; }; static config_t config; static void usage(const char* argv0, int ret) { FILE* fout = ret ? stderr : stdout; fprintf(fout, "usage: %s [OPTIONS]... [--] [FILE]...\n", argv0); fprintf(fout, " -r, -R, --raw print ANSI control sequences\n"); fprintf(fout, " -F, -quit-if-one-screen exit immediately if output fits in one screen\n"); fprintf(fout, " -X, -no-init don't clear screen at the startup\n"); fprintf(fout, " -h, --help show this message and exit\n"); exit(ret); } static void handle_option_short(const char* argv0, char opt, bool exit_on_error) { switch (opt) { case 'r': case 'R': config.raw = true; break; case 'F': config.quit_if_one_screen = true; break; case 'X': config.no_init = true; break; case 'h': usage(argv0, 0); break; default: fprintf(stderr, "unknown option: %c\n", opt); fprintf(stderr, "see --help for usage\n"); if (exit_on_error) exit(1); } } static void handle_option_long(const char* argv0, const char* opt, bool exit_on_error) { if (!strcmp(opt, "--raw")) config.raw = true; else if (!strcmp(opt, "--quit-if-one-screen")) config.quit_if_one_screen = true; else if (!strcmp(opt, "--no-init")) config.no_init = true; else if (!strcmp(opt, "--help")) usage(argv0, 0); else { fprintf(stderr, "unknown option: %s\n", opt); fprintf(stderr, "see --help for usage\n"); if (exit_on_error) exit(1); } } static int parse_config_or_exit(int argc, char** argv) { // FIXME: long environment options if (const char* env = getenv("LESS")) for (size_t i = 0; env[i]; i++) (void)handle_option_short(argv[0], env[i], false); int i = 1; for (; i < argc; i++) { if (argv[i][0] != '-' || !strcmp(argv[i], "--")) break; if (argv[i][1] == '-') handle_option_long(argv[0], argv[i], true); else for (size_t j = 1; argv[i][j]; j++) handle_option_short(argv[0], argv[i][j], true); } return i; } static int get_keyboard_fd() { // if stdin is a terminal, use it if (isatty(STDIN_FILENO)) return STDIN_FILENO; // otherwise try to open our controlling terminal int fd = open("/dev/tty", O_RDONLY); if (fd != -1) return fd; // if that fails, try stderr as the last fallback if (isatty(STDERR_FILENO)) return STDERR_FILENO; return -1; } static int output_to_non_terminal(int fd) { for (;;) { char buffer[128]; const ssize_t nread = read(fd, buffer, sizeof(buffer)); if (nread == -1) perror("read"); if (nread <= 0) break; ssize_t total = 0; while (total < nread) { const ssize_t nwrite = write(STDOUT_FILENO, buffer + total, nread - total); if (nwrite < 0) perror("write"); if (nwrite <= 0) break; total += nwrite; } } return 0; } static bool read_lines(int fd, winsize ws, BAN::Vector& out) { char buffer[128]; const ssize_t nread = read(fd, buffer, sizeof(buffer)); if (nread < 0) perror("read"); if (nread <= 0) return false; if (out.empty()) MUST(out.emplace_back()); bool in_ansi = false; size_t col = 0; for (size_t i = 0; i < out.back().size(); i++) { if (in_ansi) { if (isalpha(out.back()[i])) in_ansi = false; } else if (out.back()[i] == '\e') in_ansi = true; else col++; } for (ssize_t i = 0; i < nread; i++) { if (in_ansi) { if (config.raw) MUST(out.back().push_back(buffer[i])); if (isalpha(buffer[i])) in_ansi = false; continue; } const auto append_char = [&col, &out, ws](char ch) { if (col >= ws.ws_col) { MUST(out.emplace_back()); col = 0; } MUST(out.back().push_back(ch)); col++; }; switch (buffer[i]) { case '\e': if (config.raw) MUST(out.back().push_back(buffer[i])); in_ansi = true; break; case '\n': MUST(out.emplace_back()); col = 0; break; case '\t': append_char(' '); while (col % 8) append_char(' '); break; default: append_char(buffer[i]); break; } } return true; } static bool less_file(int fd, int kb_fd, winsize ws) { if (!isatty(STDOUT_FILENO) || kb_fd == -1 || ws.ws_col == 0 || ws.ws_row == 0) return output_to_non_terminal(fd); BAN::Vector lines; size_t lines_size = 0; const auto update_lines_size = [&lines, &lines_size] { lines_size = lines.size(); if (!lines.empty() && lines.back().empty()) lines_size--; }; while (lines_size < ws.ws_row) { if (!read_lines(fd, ws, lines)) break; update_lines_size(); } if (!config.no_init) { int y = 1; if (lines_size < ws.ws_row) y = ws.ws_row - lines_size; printf("\e[2J\e[%dH", y); } for (size_t i = 0; i < lines_size && i < static_cast(ws.ws_row - 1); i++) printf("%s\n", lines[i].data()); fflush(stdout); if (lines_size < ws.ws_row && config.quit_if_one_screen) return true; printf(":"); fflush(stdout); size_t line = 0; for (;;) { char ch; if (read(kb_fd, &ch, 1) != 1) break; switch (ch) { case 'q': printf("\e[G\e[K"); fflush(stdout); return true; case '\e': { if (read(kb_fd, &ch, 1) != 1 || ch != '[') break; if (read(kb_fd, &ch, 1) != 1) break; switch (ch) { case 'A': if (line == 0) break; line--; printf("\e[H"); for (int i = 0; i < ws.ws_row - 1; i++) printf("%s\e[K\n", lines[line + i].data()); printf(":\e[K"); fflush(stdout); break; case 'B': while (lines_size - (line + 1) < ws.ws_row) { if (!read_lines(fd, ws, lines)) break; update_lines_size(); } if (lines_size - (line + 1) < static_cast(ws.ws_row - 1)) break; line++; printf("\e[H"); for (int i = 0; i < ws.ws_row - 1; i++) printf("%s\e[K\n", lines[line + i].data()); printf(":\e[K"); fflush(stdout); break; } break; } } } return true; } int main(int argc, char** argv) { int i = parse_config_or_exit(argc, argv); int kb_fd = get_keyboard_fd(); if (kb_fd != -1) { int flags = 0; fcntl(kb_fd, F_GETFL, &flags); if (flags & O_NONBLOCK) fcntl(kb_fd, F_SETFL, flags & ~O_NONBLOCK); termios termios; tcgetattr(kb_fd, &termios); termios.c_lflag &= ~(ECHO | ICANON); tcsetattr(kb_fd, TCSANOW, &termios); } winsize ws { .ws_row = 0, .ws_col = 0 }; if (isatty(STDOUT_FILENO)) { if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { // try to make stdout fully buffered to reduce flicker const size_t bufsize = 2 * ws.ws_row * ws.ws_col; if (char* buffer = static_cast(malloc(bufsize))) setvbuf(stdout, buffer, _IOFBF, bufsize); } } if (i == argc) { if (!less_file(STDIN_FILENO, kb_fd, ws)) return EXIT_FAILURE; return EXIT_SUCCESS; } int ret = EXIT_SUCCESS; for (; i < argc; i++) { int fd = open(argv[i], O_RDONLY); if (fd == -1) { perror(argv[i]); continue; } if (!less_file(fd, kb_fd, ws)) ret = EXIT_FAILURE; close(fd); } return ret; }