/* * Copyright (c) 2014, 2015, 2016 Jonas 'Sortie' Termansen. * Copyright (c) 2021 Juhani 'nortti' Krekelä. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * pager.c * Displays files one page at a time. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define CONTROL_SEQUENCE_MAX 128 enum control_state { CONTROL_STATE_NONE = 0, CONTROL_STATE_CSI, CONTROL_STATE_COMMAND, }; struct line { char* content; size_t content_used; size_t content_length; }; static struct termios restore_tio; static bool restore_scrollback; static int tty_fd; static bool stdout_is_tty; static struct winsize winsize; static mbstate_t in_ps; static mbstate_t out_ps; static const char* input_prompt_name; static size_t possible_lines; static size_t allowed_lines; static bool quiting; static bool flag_raw_control_chars; static bool flag_color_sequences; static enum control_state control_state; static wchar_t control_sequence[CONTROL_SEQUENCE_MAX]; static size_t control_sequence_length; static bool input_set_color; static struct line* lines; static size_t lines_used; static size_t lines_length; static enum control_state incoming_control_state; static struct line* incoming_line; static size_t incoming_line_width; static size_t current_line; static size_t current_line_offset; static bool allowance_ever_exhausted; static bool skipping_to_end; static bool next_bold; static bool next_underline; static void exit_restore_tio(void) { if ( restore_scrollback ) dprintf(1, "\e[?1049l"); if ( tcsetattr(tty_fd, TCSADRAIN, &restore_tio) < 0 ) warn("tcsetattr"); } static void init(void) { tty_fd = 0; if ( !isatty(tty_fd) ) { if ( (tty_fd = open("/dev/tty", O_RDONLY)) < 0 ) err(1, "/dev/tty"); if ( !isatty(tty_fd) ) err(1, "/dev/tty"); } if ( tcgetattr(tty_fd, &restore_tio) < 0 ) err(1, "tcgetattr"); if ( atexit(exit_restore_tio) < 0 ) err(1, "atexit"); struct termios tio = restore_tio; tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); if ( tcsetattr(tty_fd, TCSADRAIN, &tio) < 0 ) err(1, "tcsetattr"); if ( (stdout_is_tty = isatty(1)) ) { if ( ioctl(1, TIOCGWINSZ, &winsize) < 0 ) err(1, "ioctl: TIOCGWINSZ"); possible_lines = winsize.ws_row - 1; allowed_lines = possible_lines; const char* term = getenv("TERM"); if ( term && strcmp(term, "sortix") != 0 && strncmp(term, "sortix-", strlen("sortix-")) != 0 ) { dprintf(1, "\e[?1049h\e[H"); restore_scrollback = true; } } memset(&in_ps, 0, sizeof(in_ps)); memset(&out_ps, 0, sizeof(out_ps)); } static char next_char(void) { char c; if ( read(tty_fd, &c, 1) < 1 ) err(1, "/dev/tty"); return c; } static void prompt(bool at_end) { const char* pre = input_set_color ? "" : "\e[47;30m"; const char* post = input_set_color ? "" : "\e[m"; if ( at_end ) dprintf(1, "%s(END)%s\e[J", pre, post); else if ( input_prompt_name[0] ) dprintf(1, "%s%s%s\e[J", pre, input_prompt_name, post); else dprintf(1, ":"); while ( true ) { char c; char buffer[CONTROL_SEQUENCE_MAX + 1]; size_t buffer_used = 0; buffer[buffer_used++] = c = next_char(); if ( c == '\e' && (buffer[buffer_used++] = c = next_char()) == '[' ) { while ( buffer_used < CONTROL_SEQUENCE_MAX ) { char c = next_char(); buffer[buffer_used++] = c; if ( '@' <= c && c <= '~' ) break; } } buffer[buffer_used] = '\0'; if ( !strcmp(buffer, "\n") || !strcmp(buffer, "j") || !strcmp(buffer, "\x0e") /* ^N */ || !strcmp(buffer, "\e[B") /* Down Arrow */ ) { dprintf(1, "\r\e[J"); allowed_lines++; return; } if ( !strcmp(buffer, "k") || !strcmp(buffer, "\x10") /* ^P */ || !strcmp(buffer, "\e[A") /* Up Arrow */ ) { if ( current_line <= possible_lines ) continue; dprintf(1, "\e[2J\e[H"); current_line -= possible_lines + 1; current_line_offset = 0; allowed_lines = possible_lines; return; } if ( !strcmp(buffer, " ") || !strcmp(buffer, "f") || !strcmp(buffer, "\x06") /* ^F */ || !strcmp(buffer, "\x16") /* ^V */ || !strcmp(buffer, "\e[6~") /* Page Down */ ) { dprintf(1, "\r\e[J"); allowed_lines = possible_lines; return; } if ( !strcmp(buffer, "b") || !strcmp(buffer, "\x02") /* ^B */ || !strcmp(buffer, "\ev") /* ESC-v */ || !strcmp(buffer, "\e[5~") /* Page Up */) { if ( current_line <= possible_lines ) continue; size_t distance = possible_lines; if ( current_line - possible_lines < distance ) distance = current_line - possible_lines; dprintf(1, "\e[2J\e[H"); current_line -= possible_lines + distance; current_line_offset = 0; allowed_lines = possible_lines; return; } if ( !strcmp(buffer, ">") || !strcmp(buffer, "G") || !strcmp(buffer, "\e[F") /* End */ || !strcmp(buffer, "\e[4~") /* End (Linux console) */ ) { dprintf(1, "\r\e[J"); skipping_to_end = true; allowed_lines = SIZE_MAX; return; } if ( !strcmp(buffer, "<") || !strcmp(buffer, "g") || !strcmp(buffer, "\e[H") /* Home */ || !strcmp(buffer, "\e[1~") /* Home (Linux console) */ ) { if ( current_line <= possible_lines ) continue; dprintf(1, "\e[2J\e[H"); current_line = 0; current_line_offset = 0; allowed_lines = possible_lines; return; } if ( !strcmp(buffer, "q") || !strcmp(buffer, "Q") ) { dprintf(1, "\r\e[J"); quiting = true; return; } } err(1, "/dev/tty"); } static void line_push_char(struct line* line, char c) { if ( line->content_used == line->content_length ) { size_t length = line->content_length; if ( !length ) length = 64; char* new = reallocarray(line->content, length, 2); if ( !new ) err(1, "malloc"); line->content = new; line->content_length = length * 2; } line->content[line->content_used++] = c; } static void line_push_string(struct line* line, const char* str) { for ( size_t i = 0; str[i]; i++ ) line_push_char(line, str[i]); } static struct line* continue_line(void) { if ( incoming_line ) return incoming_line; if ( lines_used == lines_length ) { size_t length = lines_length; if ( !length ) length = 64; struct line* new = reallocarray(lines, length, sizeof(struct line) * 2); if ( !new ) err(1, "malloc"); lines = new; lines_length = length * 2; } incoming_line = &lines[lines_used++]; memset(incoming_line, 0, sizeof(*incoming_line)); incoming_line_width = 0; return incoming_line; } static void finish_line(void) { struct line* line = incoming_line; assert(line); size_t length = line->content_used ? line->content_used : 1; char* final_content = realloc(line->content, length); if ( final_content ) line->content = final_content; incoming_line = NULL; incoming_line_width = 0; } static struct line* next_line(void) { finish_line(); return continue_line(); } static void push_wchar(wchar_t wc) { bool newline = false; int width; struct line* line = continue_line(); if ( incoming_control_state == CONTROL_STATE_CSI ) { incoming_control_state = CONTROL_STATE_NONE; if ( wc == '[' ) incoming_control_state = CONTROL_STATE_COMMAND; } else if ( incoming_control_state == CONTROL_STATE_COMMAND ) { incoming_control_state = CONTROL_STATE_NONE; if ( ('0' <= wc && wc <= '9') || wc == L';' || wc == L':' || wc == L'?' ) incoming_control_state = CONTROL_STATE_COMMAND; } else if ( wc == L'\b' ) { if ( incoming_line_width ) incoming_line_width--; while ( line->content_used && (line->content[line->content_used-1] & 0xC0) == 0x80 ) line->content_used--; if ( line->content_used ) { char c = line->content[--line->content_used]; if ( c == '_' ) next_underline = true; else if ( c == ' ' ) next_bold = false; else next_bold = true; } return; } else if ( wc == L'\e' ) { incoming_control_state = CONTROL_STATE_CSI; } else if ( wc == L'\n' ) { newline = true; } else if ( wc == L'\t' ) { if ( winsize.ws_col == incoming_line_width ) line = next_line(); do { if ( winsize.ws_col == incoming_line_width ) break; incoming_line_width++; } while ( incoming_line_width % 8 != 0 ); } else if ( wc == L'\r' ) { incoming_line_width = 0; } else if ( wc == 127 ) { } else if ( 0 <= (width = wcwidth(wc)) ) { size_t left = winsize.ws_col - incoming_line_width; if ( left < (size_t) width ) line = next_line(); incoming_line_width += width; } else { // TODO: What can cause this and how to handle it? } char mb[MB_CUR_MAX]; size_t amount = wcrtomb(mb, wc, &out_ps); if ( amount != (size_t) -1 ) { if ( next_bold && next_underline ) line_push_string(line, "\e[1;4m"); else if ( next_bold ) line_push_string(line, "\e[1m"); else if ( next_underline ) line_push_string(line, "\e[4m"); for ( size_t i = 0; i < amount; i++ ) line_push_char(line, mb[i]); if ( next_bold && next_underline ) line_push_string(line, "\e[22;24m"); else if ( next_bold ) line_push_string(line, "\e[22m"); else if ( next_underline ) line_push_string(line, "\e[24m"); next_bold = false; next_underline = false; } if ( newline ) finish_line(); } static bool push_wchar_is_escaped(wchar_t wc) { if ( wc == '\b' && (flag_raw_control_chars || flag_color_sequences) ) return false; return wc < 32 && wc != L'\t' && wc != L'\n'; } static void push_wchar_escape(wchar_t wc) { if ( push_wchar_is_escaped(wc) ) { push_wchar(L'^'); //push_wchar(L'\b'); //push_wchar(L'^'); push_wchar(L'@' + wc); //push_wchar(L'\b'); //push_wchar(L'@' + wc); } else { push_wchar(wc); } } static void control_sequence_begin(void) { control_sequence_length = 0; } static void control_sequence_accept(void) { for ( size_t i = 0; i < control_sequence_length; i++ ) push_wchar(control_sequence[i]); control_sequence_length = 0; control_state = CONTROL_STATE_NONE; } static void control_sequence_reject(void) { for ( size_t i = 0; i < control_sequence_length; i++ ) push_wchar_escape(control_sequence[i]); control_sequence_length = 0; control_state = CONTROL_STATE_NONE; } static void control_sequence_push(wchar_t wc) { if ( flag_raw_control_chars ) return push_wchar(wc); if ( CONTROL_SEQUENCE_MAX <= control_sequence_length ) { control_sequence_reject(); push_wchar_escape(wc); return; } control_sequence[control_sequence_length++] = wc; } static void control_sequence_finish(wchar_t wc) { control_sequence_push(wc); if ( control_state == CONTROL_STATE_NONE ) return; if ( wc == L'm' ) { input_set_color = true; return control_sequence_accept(); } if ( !flag_raw_control_chars ) return control_sequence_reject(); control_sequence_accept(); } static void push_wchar_filter(wchar_t wc) { if ( wc == L'\e' && (flag_raw_control_chars || flag_color_sequences) && control_state == CONTROL_STATE_NONE ) { control_sequence_begin(); control_sequence_push(wc); control_state = CONTROL_STATE_CSI; return; } else if ( control_state == CONTROL_STATE_CSI ) { if ( wc == L'[' ) { control_sequence_push(wc); control_state = CONTROL_STATE_COMMAND; return; } control_sequence_reject(); } else if ( control_state == CONTROL_STATE_COMMAND ) { if ( ('0' <= wc && wc <= '9') || wc == L';' || wc == L':' || wc == L'?' ) { control_sequence_push(wc); return; } control_sequence_finish(wc); return; } push_wchar_escape(wc); } static void push_byte(unsigned char byte) { if ( quiting ) return; wchar_t wc; size_t amount = mbrtowc(&wc, (const char*) &byte, 1, &in_ps); if ( amount == (size_t) -2 ) return; if ( amount == (size_t) -1 ) { wc = 0xFFFD /* REPLACEMENT CHARACTER */; memset(&in_ps, 0, sizeof(in_ps)); } push_wchar_filter(wc); } static bool read_fd(int fd, const char* fdpath) { unsigned char buffer[4096]; ssize_t amount = read(fd, buffer, sizeof(buffer)); if ( amount < 0 ) err(1, "%s", fdpath); for ( ssize_t i = 0; i < amount; i++ ) push_byte(buffer[i]); return amount != 0; } static void simple_fd(int fd, const char* fdpath) { unsigned char buffer[4096]; ssize_t amount = 0; while ( 0 < (amount = read(fd, buffer, sizeof(buffer))) ) { ssize_t sofar = 0; while ( sofar < amount ) { ssize_t done = write(1, buffer + sofar, amount - sofar); if ( done < 0 ) err(1, ""); sofar += done; } } if ( amount < 0 ) err(1, "%s", fdpath); } static bool can_page(void) { if ( current_line + 1 == lines_used ) { struct line* line = &lines[current_line]; return current_line_offset < line->content_used; } return current_line + 1 < lines_used; } static void page(void) { struct line* line = &lines[current_line]; if ( current_line_offset < line->content_used ) { const char* buffer = line->content + current_line_offset; size_t amount = line->content_used - current_line_offset; size_t sofar = 0; while ( sofar < amount ) { ssize_t done = write(1, buffer + sofar, amount - sofar); if ( done < 0 ) err(1, ""); sofar += done; } current_line_offset = line->content_used; } if ( current_line + 1 < lines_used ) { if ( allowed_lines != SIZE_MAX ) allowed_lines--; current_line++; current_line_offset = 0; } } static void push_fd(int fd, const char* fdpath) { if ( quiting ) return; if ( !strcmp(fdpath, "") ) input_prompt_name = ""; else input_prompt_name = fdpath; // TODO: In this case, we should disable echoing and read from the terminal // anyways. Remember to enable it again. if ( isatty(fd) ) errx(1, "/dev/tty: Is a terminal"); if ( !stdout_is_tty ) return simple_fd(fd, fdpath); bool eof = false; while ( !quiting ) { if ( !skipping_to_end ) { if ( allowed_lines == 0 ) { allowance_ever_exhausted = true; prompt(false); continue; } if ( can_page() ) { page(); continue; } } if ( eof ) break; if ( !read_fd(fd, fdpath) ) eof = true; } } static void push_path(const char* path) { if ( quiting ) return; if ( !strcmp(path, "-") ) return push_fd(0, ""); int fd = open(path, O_RDONLY); if ( fd < 0 ) err(1, "%s", path); push_fd(fd, path); close(fd); } static void compact_arguments(int* argc, char*** argv) { for ( int i = 0; i < *argc; i++ ) { while ( i < *argc && !(*argv)[i] ) { for ( int n = i; n < *argc; n++ ) (*argv)[n] = (*argv)[n+1]; (*argc)--; } } } int main(int argc, char* argv[]) { setlocale(LC_ALL, ""); if ( getenv("LESS") ) { const char* options = getenv("LESS"); char c; while ( (c = *options++) ) { switch ( c ) { case '-': break; case 'r': flag_raw_control_chars = true; break; case 'R': flag_color_sequences = true; break; default: break; } } } for ( int i = 1; i < argc; i++ ) { const char* arg = argv[i]; if ( arg[0] != '-' || !arg[1] ) continue; argv[i] = NULL; if ( !strcmp(arg, "--") ) break; if ( arg[1] != '-' ) { char c; while ( (c = *++arg) ) switch ( c ) { case 'r': flag_raw_control_chars = true; break; case 'R': flag_color_sequences = true; break; default: errx(1, "unknown option -- '%c'", c); } } else errx(1, "unknown option: %s", arg); } compact_arguments(&argc, &argv); init(); if ( argc == 1 ) { if ( tty_fd == 0 ) errx(1, "missing file operand"); push_fd(0, ""); } else for ( int i = 1; i < argc; i++ ) { push_path(argv[i]); } while ( stdout_is_tty && (allowance_ever_exhausted || restore_scrollback) && !quiting ) { if ( skipping_to_end ) { dprintf(1, "\e[2J\e[H"); size_t line = 0; if ( possible_lines <= lines_used ) line = lines_used - possible_lines; current_line = line; current_line_offset = 0; allowed_lines = possible_lines; skipping_to_end = false; } bool cant_page = !can_page(); if ( cant_page || allowed_lines == 0 ) { prompt(cant_page); continue; } page(); } return 0; }