/******************************************************************************* * * most.c * * AUTHOR: * Dan Harkless * * COPYRIGHT: * This file is Copyright (C) 2016 by Dan Harkless, and is released under the * GNU General Public License . * * USAGE: * % most [-r] [-s] [-X] [...] * * DESCRIPTION: * Which would you rather use, 'less', 'more', or 'most'? 'Nuff said. ;^> * * Of course I am only kidding. 'less' is a great replacement for crappy * implementations of 'more', and has a large and useful featureset. However, * a 'more' problem it doesn't fix is that if you copy a long, wrapping line * from its output, the single line gets split on paste into multiple * newline-terminated screen-width lines. This is especially a problem when * copying a long commandline into a shell prompt, or copying a long URL into a * browser, since in these cases you don't get an opportunity to edit out the * spurious newlines after the paste (as you can when pasting into an editor). * Thus you have to copy and paste these wrapping lines one screen line at a * time, which is tedious and error-prone. * * With 'most', wrapping lines are allowed to wrap naturally -- unnecessary * hard newlines are not inserted. Therefore, as long as your terminal is one * that's smart enough to remember which lines were terminated by newlines and * which ones wrapped (e.g. rxvt on UNIX or PuTTY on Windows), you can copy and * paste these wrapping lines intact. * * Another improvement 'most' has over 'more' is that it acts like "trn -m"'s * builtin pager. (This was also an advantage over 'less', at the time I wrote * 'most', though as of less 335, I see Mark Nudelman has implemented my (and * others') suggestion, and has added an equivalent capability: the -w / -W * options.) * * That is, when you hit the spacebar to view the next page of text, it goes * back and redraws the bottom line of text on the screen in inverse mode, and * then outputs the next page of text below that. When the next page is a full * page, the top line on the screen (the bottom line from the previous page) * will be inverse. When the next page is a partial page, that inverse line * will be somewhere in the middle of the page, and your eye can quickly find * it to determine where to continue reading, saving a lot of lost * productivity. * * This is a superior solution to more's (and less's) -c option, which clears * the screen after each page of text, since that causes your terminal's * scrollback buffer to contain only the last partial page of text (along with * a bunch of '~'s, in less's case). * * 'most' also has some improvements over "trn -m"'s basic feature. As with * 'less -W', any forward movement larger than 1 line will cause the * highlighting (this doesn't actually come up in trn's pager because it can * only move forward by a full screen or a half screen, not by arbitrary * amounts). Also, unlike trn's pager and 'less', 'most' also does * highlighting when doing backwards movement. In this case, of course, it's * the top line of text on the screen that gets highlighted, rather than the * bottom line. * * Another special feature of 'most' (one I originally thought was another * feature lacking from 'less', though I've since found out a basically * undocumented way to do it is to put "+Gg" on its commandline, or type "Gg" * at the first prompt) is that when you're piping input into it, it consumes * the entire stdin so that it can display the percentage done in the prompt * just as when you're running it on a file. Ideally this behavior ought to be * under control of an option (you wouldn't want it to do this if stdin is * *huge*), but currently it always behaves this way. * * COMMANDLINE OPTIONS: * Options may be freely interspersed with filenames, but currently you may not * group them (as in "-rs"). If you have a filename in the current directory * starting with a '-', you must prepend a "./" path to it. * * -r By default, 'most' escapes "unprintable" characters (those that * isgraph() and isspace() return false for, except for backspace) on * output. It outputs ASCII 0 through 31 as an underlined version of the * corresponding control letter (an underlined '@' for ASCII 0, an * underlined 'A' for ASCII 1, etc.) and any other unprintables (in the * 127-255 range) as an underlined '?'. * * To output these characters in raw rather than escaped form, use -r. All * characters will be passed through as-is, except for NUL (ASCII 0), which * will be stripped. * * Note that in this mode, we assume that unprintable characters take no * space onscreen. This assumption will be incorrect for characters that * your locale considers unprintable but that your terminal prints glyphs * for. So if you're using the "C" locale, running 'most -r' on a file * containing ISO-8859-1 characters will cause page breaks to come in the * wrong place if there are lines with enough supposedly unprintable * characters to make them longer than the width of your terminal. Rather * than using 'most -r' to try to print such a file, instead ensure that * your locale setting matches the capabilities of your terminal and its * font (e.g. use LANG=en_US rather than LANG=C). * * Also, in this mode, 'most' considers entire VT/ANSI-style escape * sequences (ESC [ m) to take no space * onscreen. * * -s Squeeze multiple consecutive blank lines on input into 1 on output. * * -X Suppress the terminal initialization and deinitialization sequences * ("ti" and "te"). You may want to do this if they have undesirable * effects, like switching your terminal into and out of an alternate text * buffer whose contents won't show up in the scrollback buffer after * 'most' exits. * * KEYS: * Once you're in paging mode, all you can do is move around. There are no * search features or anything else. ;^( Here are the keymappings -- below, * '|' separates multiple keys that have the same functionality. * * KEY FUNCTION * =================== ==================================================== * A number typed before a movement command will repeat * the movement that many times (e.g. "102d"). If no * number is typed, the default is always 1 line/page. * b Back pages. * d | Down lines. * f | Forward pages. * q Quit. * | Redraw. * u Up lines. * * COMPILATION: * AIX/Solaris/...: % cc most.c -lcurses -o most * HP-UX 9.x: % cc -Ae most.c -lcurses -o most * HP-UX 10.x: % cc -Ae -I/usr/include/curses_colr most.c -lcur_colr -o most * * DATE MODIFICATION * ========== ================================================================== * 2022-12-31 Renamed exit_terminfo() to my_exit_terminfo() to fix "too few * arguments to function" error on recent versions of gcc. * 2016-03-15 Cast ssize_t arguments to fprintf() to ints to stop complaints * from gcc -Wall / -Wformat. * 2003-06-25 Ignore setocale() failure if ENOENT for use on minimal installs. * 2003-05-01 Don't decrement line_len if it's already 0 (excessive '\b's). * 2003-04-28 When a line to be inverted is nothing but "\n", we output an * inverted " \n" instead. Now we do the same for "\r\n" lines. * 2003-04-23 Need setlocale(LC_ALL, "") for isgraph() to work for non-"C" loc. * 2003-04-21 Added -X option (named the same as less's option for this). * 2003-04-21 Renamed -u to -r to match 'less' and some versions of 'more', and * added intelligence to recognize escape sequences like 'less -R'. * 2003-04-21 Escaping unprintable characters as "\XX" (or anything greater than * a single character long) greatly complicates the index_down() and * index_up() routines, since if an escaped unprintable character was * at the end of a line, those routines would have to be able to * operate with sub-(buffer-)character accuracy. Instead, print * characters 0-31 as an underlined version of their control letter * (underlined '@' for ASCII 0, underlined 'A' for ASCII 1, etc.) and * any unprintable characters between 127-255 as an underlined '?'. * 2003-04-21 At least on some terminals, if the top screen line wraps (doesn't * have a newline at the end), we apparently need to add our own * newline after inverting the line when moving backwards. * 2003-04-20 Added -u option to prevent unprintable character escaping. * 2003-04-20 When running on a file rather than stdin, use mmap() rather than * allocating a buffer the size of the file and reading it all in. * 2003-04-18 If running on multiple files, re-use buffer_global from the * previous file if it's big enough; free() and re-malloc() if not. * When running on stdin, read() the file in up to 512-byte chunks * and double the buffer size with realloc() if it fills up. * 2003-04-17 Moved -s handling out of the file-reading code and into the * displaying code so we don't have to read file in byte-by-byte. * 2003-04-17 Moved the unprintable character handling out of put_line() and * into get_line(), where it needs to be to prevent lines that wrap * due to our inserted characters from messing up the page break * location. Updated index_down() and index_up() to realize that * unprintable characters take 3 screen characters to display. * 2003-04-17 Upgraded C89 signal handling code to POSIX. Restore cbreak mode * in SIGCONT handler. Resize and redraw in SIGWINCH handler. * 2003-04-15 On the OSes mentioned in the previous entry, it doesn't hurt to * call reset_shell_mode() at the end of the program even if you're * not doing "true" curses output, but rather just using curses * routines to access the terminfo database stuff like we are, and in * fact the documentation I was using implied that you were supposed * to call it. On ncurses / Linux, however, if you do that it puts * the terminal into a screwy state that causes redraw problems in * Emacs and hang problems in OpenSSH. Removed the call, as I now * think we're not supposed to use it on any OS (Solaris is the only * other OS I have access to at the moment, but this works there). * 2003-03-31 On AIX, HP-UX, and Solaris, you can call cbreak() after having * called just setupterm() and not initscr() (actually on HP-UX 9.x * you had to call initscr() then call endwin()). With ncurses on * Linux, if you try to do that you get a seg fault. Instead, turn * off the ICANON bit and set VMIN and VTIME appropriately with the * tcsetattr() call we're already using in place of curses' noecho(). * 1999-04-09 Updated file to my new convention: all global vars end in _global. * 1998-12-23 Need to redraw_screen() if we get stopped by any signal, not just * SIGTSTP. SIGSTOP isn't even catchable, so redraw in CONT handler. * 1998-02-13 Fixed reverse-scroll inversing bug on rxvt terminals. * 1998-02-12 Improved multiple file output readability. * 1996-09-26 Original. * *******************************************************************************/ #include /* for isgraph() and isspace() */ #include /* for setupterm(), etc. */ #include /* for errno */ #include /* for setlocale() */ #include /* for sigaction(), etc. */ #include /* for putchar(), etc. */ #include /* for exit(), etc. */ #include /* for strerror() */ #include /* for ioctl(), etc. */ #include /* for mmap(), etc. */ #include /* for fstat(), etc. */ #include /* must follow */ #include /* for tcsetattr(), etc. */ #include /* for read(), etc. */ #define CTRL_L '\014' #define CTRL_R '\022' #define ESC '\033' #define MIN(a,b) ((a) < (b) ? (a) : (b)) #define ROUND_UP(a,b) ((((a) + (b) - 1) / (b)) * (b)) #define SCREENFULL (lines - 2) #define MAX_CONTIGUOUS_SIGNAL_INSTALLATION_FAILURES 32 #define MAX_LINE 512 #define STDIN_BUFFER_CHUNK_SIZE 512 #define STDIN_BUFFER_INITIAL_SIZE 65536 bool exiting_most_global = FALSE; bool initialized_terminfo_global = FALSE; bool raw_unprintables_global = FALSE; bool squeeze_global = FALSE; bool suppress_term_init_global = FALSE; char line_global[MAX_LINE + 1]; char* buffer_global; char* filename_global; char* our_program_name_global; FILE* terminal_global; int display_mode_global = A_NORMAL; int file_bytes_global, file_index_global = 1, index_global; int number_of_files_global = 0, top_of_cur_page_global; struct termios original_termios_global; void clear_bottom_line(); void die_max_line(); void die_syscall(const char* syscall_and_or_args); void display_lines(int number); void display_prompt(); void exit_most(int exit_status); void get_line(); int index_down(int* index_ptr, int number); int index_up(int* index_ptr, int number); void init_terminfo(); void install_signal_handlers(); void mode(int change_to_mode); void my_cbreak(); void my_exit_terminfo(); void process_file(FILE* file); char put_line(); void redraw_screen(); void scroll_down(int number); void scroll_up(int number); void signal_handler_CONT(int SIGCONT_signal_num); void signal_handler_stop(int stop_signal); void signal_handler_WINCH(int SIGWINCH_signal_num); void clear_bottom_line() { putp(tparm(cursor_address, lines - 1, 0)); putp(clr_eol); } void die_max_line() { /* TBD: Make line_global be dynamically allocated, and make MAX_LINE adjustable via the commandline? Or automatically realloc() line_global if it fills up, up to some limit (adjustable via the commandline)? */ if (initialized_terminfo_global) { mode(A_NORMAL); clear_bottom_line(); } fprintf(stderr, "%s: FATAL: MAX_LINE of %d exceeded!\n", our_program_name_global, MAX_LINE); exit_most(EXIT_FAILURE); } void die_syscall(const char* syscall_and_or_args) { if (initialized_terminfo_global) { mode(A_NORMAL); clear_bottom_line(); } fprintf(stderr, "%s: FATAL: %s: %s.\n", our_program_name_global, syscall_and_or_args, strerror(errno)); exit_most(EXIT_FAILURE); } void display_lines(int number) { int lines_written = 0; mode(A_NORMAL); while (lines_written < number) { get_line(); put_line(); if (index_global == file_bytes_global) return; lines_written++; } } void display_prompt() { mode(A_REVERSE); printf("%s (%d%%)", filename_global, (int)(((float)index_global / file_bytes_global) * 100)); mode(A_NORMAL); fflush(stdout); } void exit_most(int exit_status) { /* Make sure we don't infinite-loop if we hit an error while exiting. */ if (!exiting_most_global) { exiting_most_global = TRUE; if (initialized_terminfo_global) my_exit_terminfo(); exit(exit_status); } } void get_line() { int j = 0, line_len = 0; do { if (buffer_global[index_global] == '\b' || isgraph(buffer_global[index_global]) || isspace(buffer_global[index_global])) { /* This is a printable character. Note that isprint(c) is *not* the same thing as isgraph(c) || isspace(c). */ line_global[j] = buffer_global[index_global]; if (line_global[j] == '\b') { if (line_len > 0) line_len--; } else if (line_global[j] == '\t') { if (j == 0) { /* Tabs don't wrap -- need an initial space to wrap the line in case this is a continuation of a multi-screen-line logical line where the last char of the previous screen line was a tab. */ line_global[0] = ' '; line_global[1] = '\t'; j++; } line_len++; /* it'll at least move over one character */ line_len = ROUND_UP(line_len, 8); } else if (squeeze_global && j == 0 && line_global[j] == '\n') while (buffer_global[index_global + 1] == '\n') index_global++; else line_len++; j++; } else { /* Unprintable character. */ if (raw_unprintables_global) { /* We won't do any escaping. We don't increment line_len in these cases -- we assume unprintable characters and escape sequences don't move the cursor. */ if (buffer_global[index_global] == ESC && index_global + 1 < file_bytes_global && buffer_global[index_global + 1] == '[') { /* Assume this entire escape sequence doesn't take up screen space. */ while (index_global < file_bytes_global && buffer_global[index_global] != 'm' && (index_global + 1 == file_bytes_global || buffer_global[index_global + 1] != '\n')) { if (j > MAX_LINE - 1) die_max_line(); if (buffer_global[index_global] != '\0') /* Don't copy NULs -- line_global NUL-terminated. */ line_global[j++] = buffer_global[index_global]; index_global++; } /* Add the final character in the escape sequence ('m', if it's formatted as we expect). If there was a '\n' before we saw an 'm', that won't get added here -- it'll get handled as a printable character in the next iteration of the loop. */ if (buffer_global[index_global] != '\0') /* Don't copy NULs -- line_global NUL-terminated. */ line_global[j++] = buffer_global[index_global]; } else if (buffer_global[index_global] != '\0') { /* Don't copy NULs -- line_global NUL-terminated. */ line_global[j++] = buffer_global[index_global]; } } else { /* Escape the unprintable character. */ char newly_printable; char unprintable = buffer_global[index_global]; if (unprintable >= '\0' && unprintable < ' ') newly_printable = unprintable + '@'; else newly_printable = '?'; if (j > MAX_LINE - 3) die_max_line(); line_global[j++] = newly_printable; line_global[j++] = '\b'; line_global[j++] = '_'; line_len++; } } index_global++; if (j > MAX_LINE) die_max_line(); } while (buffer_global[index_global - 1] != '\n' && index_global < file_bytes_global && (line_len < columns || (line_len == columns && buffer_global[index_global] == '\n'))); if (line_global[j - 1] == '\t') { /* Tabs don't wrap -- need a final space to wrap the line so that the 'most' prompt won't start printing in the final column. */ line_global[j] = ' '; j++; if (j > MAX_LINE) die_max_line(); } line_global[j] = 0; } int index_down(int* i_ptr, int number) { int chars_moved_down = 0, lines_moved_down = 0; while (lines_moved_down < number && *i_ptr < file_bytes_global) { if (buffer_global[*i_ptr] == '\n') { if (squeeze_global && chars_moved_down == 0) /* If we're in -s mode, and chars_moved_down == 0 (which means either the previous character was also a newline or this newline is the first byte in the file), then skip to the last newline in this series. */ while (*i_ptr + 1 < file_bytes_global && buffer_global[*i_ptr + 1] == '\n') (*i_ptr)++; lines_moved_down++; chars_moved_down = 0; } else if (buffer_global[*i_ptr] == '\b') { if (chars_moved_down >= 1) chars_moved_down--; } else if (!isgraph(buffer_global[*i_ptr]) && !isspace(buffer_global[*i_ptr])) { if (raw_unprintables_global) { /* We assume this character takes no screen space. */ if (buffer_global[*i_ptr] == ESC && *i_ptr + 1 < file_bytes_global && buffer_global[*i_ptr + 1] == '[') { /* This entire escape sequence takes no screen space. */ *i_ptr += 2; while (*i_ptr < file_bytes_global && buffer_global[*i_ptr] != 'm' && buffer_global[*i_ptr] != '\n') (*i_ptr)++; if (buffer_global[*i_ptr] == '\n') /* This was a malformed escape sequence. Back up to the character before the '\n' -- we'll skip past it with the (*i_ptr)++ below and then handle the '\n' with the special case above. */ (*i_ptr)--; } } else /* We'll escape this character, so it will take one column. */ chars_moved_down++; } else chars_moved_down++; if (chars_moved_down == columns && (*i_ptr + 1 >= file_bytes_global || buffer_global[*i_ptr + 1] != '\n')) { lines_moved_down++; chars_moved_down = 0; } (*i_ptr)++; } return lines_moved_down; } int index_up(int* i_ptr, int number) { int chars_moved_up, lines_moved_up = 0; while (lines_moved_up < number && *i_ptr >= 1) { if (buffer_global[*i_ptr - 1] != '\n') { /* We're in the middle of a multi-screen-line file line. */ chars_moved_up = 0; while (*i_ptr >= 1 && buffer_global[*i_ptr - 1] != '\n' && chars_moved_up < columns) { /* Move back until we've gone 'columns' or hit a newline. */ (*i_ptr)--; if (buffer_global[*i_ptr] == '\b') { if (chars_moved_up >= 1) chars_moved_up--; } else if (!isgraph(buffer_global[*i_ptr]) && !isspace(buffer_global[*i_ptr])) { if (!raw_unprintables_global) chars_moved_up++; } else if (raw_unprintables_global && buffer_global[*i_ptr] == 'm') { /* See if this is the end of an escape sequence. TBD: This code is long and is duplicated below... */ bool m_was_end_of_escape_sequence = FALSE; int i_rover = *i_ptr; while (i_rover - 2 >= 0 && buffer_global[i_rover - 1] != '\n' && !m_was_end_of_escape_sequence) { if (buffer_global[i_rover - 2] == ESC && buffer_global[i_rover - 1] == '[') { *i_ptr = i_rover - 2; m_was_end_of_escape_sequence = TRUE; } else i_rover--; } if (!m_was_end_of_escape_sequence) /* It was just an "innocent" 'm'. ;^> */ chars_moved_up++; } else chars_moved_up++; } } else { /* Moving back into the end of a file line that may span multiple screen lines. */ (*i_ptr)--; /* now buffer_global[*i_ptr] == '\n' */ chars_moved_up = 1; if (squeeze_global) /* If we're in -s mode, then while either the previous two characters are also newlines or the previous character is a newline and it's the first character in the file, skip back a character. */ while (*i_ptr - 1 >= 0 && buffer_global[*i_ptr - 1] == '\n' && (*i_ptr - 2 < 0 || buffer_global[*i_ptr - 2] == '\n')) (*i_ptr)--; /* Go all the way to the beginning of the file line. */ while (*i_ptr >= 1 && buffer_global[*i_ptr - 1] != '\n') { (*i_ptr)--; if (buffer_global[*i_ptr] == '\b') { if (chars_moved_up >= 1) chars_moved_up--; } else if (!isgraph(buffer_global[*i_ptr]) && !isspace(buffer_global[*i_ptr])) { if (!raw_unprintables_global) chars_moved_up++; } else if (raw_unprintables_global && buffer_global[*i_ptr] == 'm') { /* See if this is the end of an escape sequence. TBD: This code is long and is duplicated above... */ bool m_was_end_of_escape_sequence = FALSE; int i_rover = *i_ptr; while (i_rover - 2 >= 0 && buffer_global[i_rover - 1] != '\n' && !m_was_end_of_escape_sequence) { if (buffer_global[i_rover - 2] == ESC && buffer_global[i_rover - 1] == '[') { *i_ptr = i_rover - 2; m_was_end_of_escape_sequence = TRUE; } else i_rover--; } if (!m_was_end_of_escape_sequence) /* It was just an "innocent" 'm'. ;^> */ chars_moved_up++; } else chars_moved_up++; } /* Now, if the line was a wrapping one, we can go back forward until the beginning of the last, partial screen line. */ if ((chars_moved_up == columns + 1 && buffer_global[*i_ptr + columns] != '\n') || chars_moved_up > columns + 1) index_down(i_ptr, ((chars_moved_up + (columns - 1)) / columns) - 1); } lines_moved_up++; } return lines_moved_up; } void init_terminfo() { if (!initialized_terminfo_global) { setupterm(NULL, STDOUT_FILENO, NULL); /* enter terminfo mode */ if (!suppress_term_init_global) putp(enter_ca_mode); /* allow cursor addressing */ if (tcgetattr(fileno(terminal_global), &original_termios_global) != 0) die_syscall("tcgetattr(...original_termios_global)"); my_cbreak(); initialized_terminfo_global = TRUE; install_signal_handlers(); } } void install_signal_handlers() { int contiguous_failures = 0, signal_num = 1; struct sigaction CONT_sigaction, fatal_sigaction, stop_sigaction, WINCH_sigaction; CONT_sigaction.sa_flags = 0; CONT_sigaction.sa_handler = signal_handler_CONT; sigemptyset(&CONT_sigaction.sa_mask); fatal_sigaction.sa_flags = 0; fatal_sigaction.sa_handler = exit_most; sigemptyset(&fatal_sigaction.sa_mask); stop_sigaction.sa_flags = 0; stop_sigaction.sa_handler = signal_handler_stop; sigemptyset(&stop_sigaction.sa_mask); WINCH_sigaction.sa_flags = 0; WINCH_sigaction.sa_handler = signal_handler_WINCH; sigemptyset(&WINCH_sigaction.sa_mask); /* Unfortunately there's no MAXSIG/SIGMAX #define that can be depended on to be there. To be portable, we'll just start installing the fatal signal handler at signal 1 (skipping non-fatal and uncatchable signals) and keep going until we've had MAX_CONTIGUOUS_SIGNAL_INSTALLATION_FAILURES. */ while (contiguous_failures < MAX_CONTIGUOUS_SIGNAL_INSTALLATION_FAILURES) { switch (signal_num) { case SIGCHLD: /* default action does not terminate */ #ifdef SIGINFO case SIGINFO: /* default action does not terminate */ #endif /* SIGINFO */ case SIGKILL: /* uncatchable */ case SIGPWR: /* default action does not terminate */ case SIGSTOP: /* uncatachable */ case SIGURG: /* default action does not terminate */ /* Don't install a handler for this signal. */ break; case SIGCONT: if (sigaction(signal_num, &CONT_sigaction, NULL) < 0) die_syscall("sigaction(...CONT_sigaction...)"); break; case SIGTSTP: case SIGTTIN: case SIGTTOU: if (sigaction(signal_num, &stop_sigaction, NULL) < 0) die_syscall("sigaction(...stop_sigaction...)"); break; case SIGWINCH: if (sigaction(signal_num, &WINCH_sigaction, NULL) < 0) die_syscall("sigaction(...WINCH_sigaction...)"); break; default: /* Try to install the fatal signal handler. */ if (sigaction(signal_num, &fatal_sigaction, NULL) == 0) contiguous_failures = 0; else contiguous_failures++; } signal_num++; } } void mode(int change_to_mode) { if (change_to_mode == A_NORMAL) { if (display_mode_global == A_REVERSE) vidattr(A_NORMAL); display_mode_global = A_NORMAL; } else { /* change_to_mode == A_REVERSE */ if (display_mode_global == A_NORMAL) vidattr(A_REVERSE); display_mode_global = A_REVERSE; } } void my_cbreak() { struct termios modified_termios; modified_termios = original_termios_global; modified_termios.c_cc[VMIN] = 1; /* min. chars for non-canon. read */ modified_termios.c_cc[VTIME] = 0; /* time delay for non-canon. read */ modified_termios.c_lflag = modified_termios.c_lflag & ~ECHO & ~ICANON; if (tcsetattr(fileno(terminal_global), TCSANOW, &modified_termios) != 0) die_syscall("tcsetattr(...modified_termios)"); } void my_exit_terminfo() { clear_bottom_line(); mode(A_NORMAL); if (!suppress_term_init_global) putp(exit_ca_mode); /* exit cursor addressing mode */ if (tcsetattr(fileno(terminal_global), TCSANOW, &original_termios_global) != 0) die_syscall("tcsetattr(...original_termios_global)"); fflush(stdout); } void process_file(FILE* file) { char c; int numeric_arg = 0; if (!initialized_terminfo_global) init_terminfo(); if (file == stdin) { ssize_t buffer_size = STDIN_BUFFER_INITIAL_SIZE; ssize_t read_bytes; file_bytes_global = 0; buffer_size = STDIN_BUFFER_INITIAL_SIZE; buffer_global = malloc(buffer_size); if (buffer_global == NULL) die_syscall("malloc(STDIN_BUFFER_INITIAL_SIZE)"); do { read_bytes = read(fileno(file), buffer_global + file_bytes_global, MIN(STDIN_BUFFER_CHUNK_SIZE, buffer_size - file_bytes_global)); if (read_bytes < 0) { if (errno == EINTR) /* read() got interrupted, e.g. by the user stopping and then restarting most. Set read_bytes to 1 just so we don't have to continually check for the -1 case in the while() condition below. */ read_bytes = 1; else /* read()'s error was something fatal. Die. */ die_syscall("read()"); } else { if (read_bytes == buffer_size - file_bytes_global) { /* We filled up the buffer. Double it. */ ssize_t buffer_size_doubled = buffer_size * 2; if (buffer_size_doubled < buffer_size) { fprintf(stderr, "%s: FATAL: Trying to double buffer" " size %d resulted in integer overflow to" " %d.\n", our_program_name_global, (int)buffer_size, (int)buffer_size_doubled); exit_most(EXIT_FAILURE); } buffer_size = buffer_size_doubled; buffer_global = realloc(buffer_global, buffer_size); if (buffer_global == NULL) die_syscall("realloc()"); } file_bytes_global += read_bytes; } } while (read_bytes > 0); if (file_bytes_global == 0) return; } else { struct stat file_stat; fstat(fileno(file), &file_stat); file_bytes_global = file_stat.st_size; if (file_bytes_global == 0) return; buffer_global = mmap(NULL, file_bytes_global, PROT_READ, MAP_PRIVATE, fileno(file), 0); if (buffer_global == (void*)-1) die_syscall("mmap()"); } index_global = 0; top_of_cur_page_global = 0; if (number_of_files_global <= 1) { if (number_of_files_global == 0) { putp(cursor_up); /* cover up "Processing stdin..." message */ putp(clr_eol); } display_lines(SCREENFULL + 1); } else /* number_of_files_global > 1 */ { int hyphen_columns = columns - strlen(filename_global) - 2; int j; if (file_index_global >= 2) putchar('\n'); /* skip a line between files */ vidattr(A_BOLD); for (j = 1; j <= hyphen_columns; j++) putchar('-'); printf("> %s\n", filename_global); vidattr(A_NORMAL); display_lines(SCREENFULL); } while (index_global < file_bytes_global || file_index_global < number_of_files_global) { if (numeric_arg == 0) display_prompt(); c = getc(terminal_global); switch (c) { case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': if (numeric_arg == 0) clear_bottom_line(); putchar(c); numeric_arg = (numeric_arg * 10) + (c - '0'); break; case 'b': /* back (pages) */ if (numeric_arg == 0) scroll_up(SCREENFULL); else { scroll_up(SCREENFULL * numeric_arg); numeric_arg = 0; } clear_bottom_line(); break; case 'd': case '\n': case '\r': /* down (lines) */ clear_bottom_line(); if (index_global == file_bytes_global) goto DoneWithThisFile; if (numeric_arg == 0) scroll_down(1); else { scroll_down(numeric_arg); numeric_arg = 0; } break; case 'f': case ' ': /* forward (pages) */ clear_bottom_line(); if (index_global == file_bytes_global) goto DoneWithThisFile; if (numeric_arg == 0) scroll_down(SCREENFULL); else { scroll_down(SCREENFULL * numeric_arg); numeric_arg = 0; } break; case 'q': /* quit */ clear_bottom_line(); exit_most(EXIT_SUCCESS); break; case CTRL_R: /* redraw */ case CTRL_L: clear_bottom_line(); redraw_screen(); break; case 'u': /* up (lines) */ if (numeric_arg == 0) scroll_up(1); else { scroll_up(numeric_arg); numeric_arg = 0; } clear_bottom_line(); break; default: numeric_arg = 0; clear_bottom_line(); } } DoneWithThisFile: if (file != stdin) { if (munmap(buffer_global, file_bytes_global) != 0) die_syscall("munmap()"); if (fclose(file) != 0) die_syscall("fclose()"); } } char put_line() { char last_char = '\0'; if (display_mode_global == A_REVERSE && (line_global[0] == '\n' || (line_global[0] == '\r' && line_global[1] == '\n'))) { /* If we're trying to invert a blank line, output one inverted space instead. */ puts(" "); last_char = ' '; } else { char second_to_last_char = '\0'; int j = 0; while (line_global[j] != '\0') { if (last_char == '\b') if (line_global[j] == '_') { vidattr(display_mode_global | A_UNDERLINE); putchar(second_to_last_char); vidattr(display_mode_global); /* back->A_NORMAL/A_REVERSE */ } else if (second_to_last_char == line_global[j]) { vidattr(display_mode_global | A_BOLD); putchar(line_global[j]); vidattr(display_mode_global); /* back->A_NORMAL/A_REVERSE */ } else if (second_to_last_char == '_') { vidattr(display_mode_global | A_UNDERLINE); putchar(line_global[j]); vidattr(display_mode_global); /* back->A_NORMAL/A_REVERSE */ } else putchar(line_global[j]); else putchar(line_global[j]); second_to_last_char = last_char; last_char = line_global[j]; j++; } } return last_char; } void redraw_screen() { putp(clear_screen); index_global = top_of_cur_page_global; display_lines(SCREENFULL + 1); } void scroll_down(int number) { if (number <= SCREENFULL) { if (number > 1) { /* Bottom line on the screen'll be visible -- inverse it. */ putp(cursor_up); mode(A_REVERSE); put_line(); } display_lines(number); } else { /* Need to skip some lines. */ index_down(&index_global, number - (SCREENFULL + 1)); display_lines(SCREENFULL + 1); } index_down(&top_of_cur_page_global, number); } void scroll_up(int number) { if (top_of_cur_page_global > 0) { int lines_moved_up; /* Get the top line in case it'll be showing and'll need inversing. */ index_global = top_of_cur_page_global; get_line(); /* Move 'index_global' back 'number' lines, if possible. */ index_global = top_of_cur_page_global; /* reset to here again */ lines_moved_up = index_up(&index_global, number); top_of_cur_page_global = index_global; putp(cursor_home); if (lines_moved_up > SCREENFULL) { putp(clr_eos); display_lines(SCREENFULL + 1); /* not saving bottom line */ } else { int j; if (number > 1) { /* Inverse the top line on the screen. */ mode(A_REVERSE); if (put_line() != '\n') /* If the top screen line wraps, we need to output a newline here or the cursor would be in the wrong place (still at the end of the line, apparently) when we do the scroll_reverse below and we'd end up with a blank line at the top of the screen. */ putchar('\n'); mode(A_NORMAL); putp(scroll_reverse); } /* Scroll up 'lines_moved_up' lines and fill in blank space. */ for (j = 1; j <= lines_moved_up; j++) putp(scroll_reverse); display_lines(lines_moved_up); /* Prepare for coming scroll_down() by remembering bottom line. */ index_down(&index_global, SCREENFULL - lines_moved_up); get_line(); } } } void signal_handler_CONT(int SIGCONT_signal_num) { my_cbreak(); if (!suppress_term_init_global) putp(enter_ca_mode); /* allow cursor addressing again */ vidattr(display_mode_global); /* back to A_NORMAL or A_REVERSE */ redraw_screen(); } void signal_handler_stop(int stop_signal) { vidattr(A_NORMAL); my_exit_terminfo(); /* put things back for the shell */ raise(SIGSTOP); /* now suspend most */ } void signal_handler_WINCH(int SIGWINCH_signal_num) { struct winsize new_winsize; /* ncurses only installs a WINCH handler as a 'configure'-time option, so we need to handle the signal and reset ncurses' 'columns' and 'lines' variables manually. */ if (ioctl(fileno(terminal_global), TIOCGWINSZ, &new_winsize) != 0) die_syscall("ioctl(...TIOCGWINSZ...)"); columns = new_winsize.ws_col; lines = new_winsize.ws_row; redraw_screen(); } int main(int argc, char** argv) { int arg_index; FILE* file; our_program_name_global = argv[0]; if (setlocale(LC_ALL, "") == NULL && errno != ENOENT) die_syscall("setlocale()"); /* TBD: Rewrite to use getopt() to allow option grouping (a bit of work to do that, due to our argv[] munging and our use of file_index_global). Also, add a MOST environment variable for always-on options? */ for (arg_index = 1; arg_index < argc; arg_index++) /* Process and purge all the command-line options from argv. */ if (argv[arg_index][0] == '-') switch (argv[arg_index][1]) { case 'r': raw_unprintables_global = TRUE; break; case 's': squeeze_global = TRUE; break; case 'X': suppress_term_init_global = TRUE; break; default: fprintf(stderr, "%s: FATAL: Unknown option '-%c'.\n", our_program_name_global, argv[arg_index][1]); fprintf(stderr, "Usage: %s [-r] [-s] [...]\n", our_program_name_global); return EXIT_FAILURE; } else { argv[file_index_global] = argv[arg_index]; number_of_files_global = file_index_global; file_index_global++; } argv[file_index_global] = NULL; /* cap off the argv array */ if (!isatty(STDOUT_FILENO)) /* If stdout isn't a terminal, just exec cat on the file(s) or stdin. */ execvp("cat", argv); else { terminal_global = fdopen(STDOUT_FILENO, "r"); /* open tty for reading */ if (number_of_files_global == 0) { /* Reading from stdin. */ fprintf(stderr, "%s: Processing stdin...\n", our_program_name_global); filename_global = ""; process_file(stdin); } else /* number_of_files_global >= 1 */ for (file_index_global = 1; file_index_global <= number_of_files_global; file_index_global++) { file = fopen(argv[file_index_global], "r"); if (file == NULL) { if (file_index_global >= 2) putc('\n', stderr); die_syscall(argv[file_index_global]); } else { filename_global = argv[file_index_global]; process_file(file); } } } exit_most(EXIT_SUCCESS); return EXIT_FAILURE; /* not reached; here to make compiler happpy */ }