/* See LICENSE file for copyright and license details. */ #include #include #include #include USAGE("[-b block-size] file [block-list]"); #define IS_KEY(INPUT, KEY)\ ((INPUT)->keypress.key == LIBTERMINPUT_SYMBOL &&\ toupper((INPUT)->keypress.symbol[0]) == toupper((KEY)) &&\ !(INPUT)->keypress.symbol[1]) #define IS_CTRL_KEY(INPUT, KEY)\ (((INPUT)->keypress.mods & (enum libterminput_mod)~LIBTERMINPUT_SHIFT) == LIBTERMINPUT_CTRL && IS_KEY((INPUT), (KEY))) static void configure_terminal(void); static void restore_terminal(void); enum component { BLOCK_SELECTOR, BLOCK_DISPLAY, NCOMPONENTS }; static size_t blksize = 4ul << 10; static off_t *blocks = NULL; static size_t blocks_size = 0u; static size_t nblocks = 0u; static int max_block_len; static volatile sig_atomic_t term_resized = 0; static struct termios term_attributes; static int term_attributes_saved = 0; static int term_entered_submode_stdout = 0; static int ttyfd = -1; static size_t term_width; static size_t term_height; static int exiting = 0; static int interrupt_deferred = 0; static size_t selected_block = 0u; static size_t first_visible_block = 0u; static size_t first_visible_line = 0u; static size_t line_count = 0u; static size_t *line_map = NULL; static size_t line_map_size = 0u; static enum component active_focus = BLOCK_SELECTOR; static int textfd; static const char *path; static off_t current_block_off = -1; static unsigned char *current_block_text = NULL; static size_t current_block_text_size = 0u; static size_t current_block_text_len = 0u; static size_t display_width = 0u; static int display_as_text = 0; static int limited_block_drawing; static const char *const scroll_blocks8[] = {" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇"}; static const char *const scroll_blocks2[] = {scroll_blocks8[0], scroll_blocks8[4]}; static const char *const scroll_blocks3[] = {" ", "\xf0\x9f\xac\xad", "\xf0\x9f\xac\xb9"}; /* emacs has broken support for these, its the block equivalents of "⠤" and "⠶" */ static const char *const scroll_blocks1_3[] = {"\xf0\x9f\xac\xad", "\xf0\x9f\xac\x8b", "\xf0\x9f\xac\x82"}; /* emacs has broken support for these, its the block equivalents of "⠤", "⠒", and "⠉" */ #define S(X)\ X"0", X"1", X"2", X"3", X"4", X"5", X"6", X"7",\ X"8", X"9", X"A", X"B", X"C", X"D", X"E", X"F" #if defined(__GNUC__) __attribute__((__nonstring__)) #endif static const char hex[256][2] = { S("0"), S("1"), S("2"), S("3"), S("4"), S("5"), S("6"), S("7"), S("8"), S("9"), S("A"), S("B"), S("C"), S("D"), S("E"), S("F") }; #undef S #define PRINT(...) dprintf(ttyfd, __VA_ARGS__) static void do_redraw(void) { size_t last_visible_block = first_visible_block + term_height - 2u; size_t blocks_top, blocks_bottom; int block_pane_width = MAX(max_block_len, (int)sizeof("Block:") - 1) + 1; size_t i, j, end, vis_end; size_t new_display_width; size_t right_pad; size_t text_top, text_bottom; if (current_block_off != blocks[selected_block]) { off_t off; ssize_t r; if (current_block_text_size < blksize) { current_block_text = erealloc(current_block_text, blksize); current_block_text_size = blksize; } off = current_block_off = blocks[selected_block]; off *= (off_t)blksize; for (i = 0u; i < blksize;) { r = pread(textfd, ¤t_block_text[i], blksize - i, off); if (r <= 0) { if (!r) break; if (errno == EINTR) { interrupt_deferred = 1; continue; } eprintf("pread %s %zu %ji:", path, blksize - i, (intmax_t)off); } off += (off_t)r; i += (size_t)r; } current_block_text_len = i; } new_display_width = term_width; new_display_width -= MIN(new_display_width, (size_t)block_pane_width); new_display_width -= MIN(new_display_width, 2u); /* scrollbar for block list */ new_display_width -= MIN(new_display_width, 3u); /* scrollbar for display incl. spacing */ right_pad = new_display_width; new_display_width /= 4u; if (!display_as_text) { if (first_visible_line && new_display_width && new_display_width != display_width) { first_visible_line *= display_width; first_visible_line /= new_display_width; } if (line_count == 0u && new_display_width) { line_count = current_block_text_len / new_display_width; line_count += current_block_text_len % new_display_width ? 1u : 0u; } right_pad -= new_display_width * 4u; } display_width = new_display_width; if (display_as_text && !line_count) {; for (i = 0u; i + 1u < current_block_text_len; i++) if (current_block_text[i] == '\n') line_count += 1u; if (current_block_text_len) line_count += 1u; if (line_map_size < line_count) { line_map_size = line_count; line_map = ereallocarray(line_map, line_map_size, sizeof(*line_map)); line_map[0u] = 0u; } for (i = 0u, j = 1u; j < line_count; i++) if (current_block_text[i] == '\n') line_map[j++] = i + 1u; } /* TODO what if terminal is too small? */ /* TODO make size stable */ blocks_top = first_visible_block * (term_height - 2u) / nblocks; blocks_bottom = (last_visible_block + 1u) * (term_height - 2u) / nblocks; if (blocks_bottom == blocks_top) blocks_bottom += 1u; /* TODO make size stable */ if (line_count) { size_t last_visible_line = first_visible_line + term_height - 2u; last_visible_line = MIN(last_visible_line, line_count) - 1u; text_top = first_visible_line * (term_height - 2u) / line_count; text_bottom = (last_visible_line + 1u) * (term_height - 2u) / line_count; if (text_bottom == text_top) text_bottom += 1u; } PRINT("\033[H"); PRINT("\033[7;1m%-*s\033[m\n", (int)term_width, "Block:"); /* TODO add caption for display area */ /* TODO draw breaks if display_width is too large */ for (i = 0u; i + 2u < term_height; i++) { j = i + first_visible_block; if (j == selected_block && active_focus == BLOCK_SELECTOR) PRINT("\033[7;1;34m%-*ji\033[m", block_pane_width, (intmax_t)blocks[j]); else if (j == selected_block) PRINT("\033[34m%-*ji\033[m", block_pane_width, (intmax_t)blocks[j]); else if (j < nblocks) PRINT("%-*ji", block_pane_width, (intmax_t)blocks[j]); else PRINT("%*s", block_pane_width, ""); /* TODO need to fix colour support for less capable terminals */ /* TODO make more granular */ if (i < blocks_top || i >= blocks_bottom) PRINT("\033[100m%s\033[m", " "); else PRINT("\033[107m%s\033[m", " "); PRINT("\033[m"); if (display_as_text) { int truncated = 1; size_t cols = 0; if (i + first_visible_line >= line_count) { PRINT("\033[1;30m~\033[m"); cols += 1u; truncated = 0; goto end_of_line; } j = line_map[i + first_visible_line]; for (; j < current_block_text_len && cols < right_pad; j++) { unsigned char c = current_block_text[j]; const char *ctext = NULL; char buf[8]; switch (c) { case '\a': PRINT("\033[36m"); ctext = "\\a"; break; case '\b': PRINT("\033[36m"); ctext = "\\b"; break; case '\033': PRINT("\033[36m"); ctext = "\\e"; break; case '\r': PRINT("\033[36m"); ctext = "\\r"; break; case '\v': PRINT("\033[36m"); ctext = "\\v"; break; case '\f': PRINT("\033[36m"); ctext = "^L"; break; case 0x7Fu: PRINT("\033[36m"); ctext = "^?"; break; case ' ': PRINT("\033[90m.\033[m"); break; case '\t': PRINT("\033[90m→"); memset(buf, ' ', 7u); buf[7u - cols % 8u] = '\0'; cols += 1u; ctext = buf; break; case '\n': PRINT("\033[90m\\"); cols += 1u; if (cols < right_pad) { PRINT("n"); cols += 1u; } PRINT("\033[m"); truncated = 0; goto end_of_line; default: if (c & 0x80u) { /* TODO add support for UTF-8 */ PRINT("\033[31m"); ctext = buf; buf[0u] = '\\'; buf[1u] = 'x'; buf[2u] = hex[c][0]; buf[3u] = hex[c][1]; buf[4u] = '\0'; } else { if (c < (unsigned char)' ') { PRINT("\033[36m"); ctext = buf; buf[0u] = '^'; buf[1u] = (char)(c + '@'); buf[2u] = '\0'; } else { PRINT("%c", (char)c); } } break; } if (ctext) { size_t len = strlen(ctext); int n = (int)MIN(len, right_pad - cols); PRINT("%.*s\033[m", n, ctext); cols += (size_t)n; } else { cols += 1u; } } if (j == current_block_text_len) truncated = 0; end_of_line: if (right_pad - cols) PRINT("%*s", (int)(right_pad - cols), ""); if (truncated) PRINT("\033[1;33m$"); else PRINT(" "); goto next; } j = (i + first_visible_line) * display_width; vis_end = j + display_width; end = MIN(vis_end, current_block_text_len); for (; j < end; j++) { unsigned char c = current_block_text[j]; switch (c) { case '\a': PRINT("\033[35ma\033[m"); break; case '\b': PRINT("\033[35mb\033[m"); break; case '\033': PRINT("\033[35me\033[m"); break; case '\f': PRINT("\033[35mf\033[m"); break; case '\n': PRINT("\033[1;35mn\033[m"); break; case '\r': PRINT("\033[35mr\033[m"); break; case '\t': PRINT("\033[35mt\033[m"); break; case '\v': PRINT("\033[35mv\033[m"); break; case ' ': PRINT("\033[35m.\033[m"); break; case 0x7Fu: PRINT("\033[36m?\033[m"); break; default: if (c & 0x80u) { c &= 0x7Fu; if (c == 0x7Fu) PRINT("\033[32m?\033[m"); else if (c == (unsigned char)' ') PRINT("\033[32m.\033[m"); else if (c < (unsigned char)' ') PRINT("\033[32m%c\033[m", (char)(c + '@')); else PRINT("\033[33m%c\033[m", (char)c); } else { if (c < (unsigned char)' ') PRINT("\033[36m%c\033[m", (char)(c + '@')); else PRINT("%c", (char)c); } break; } } if (end != vis_end) PRINT("%*s", (int)(vis_end - end), ""); j = (i + first_visible_line) * display_width; for (; j < end; j++) { unsigned char c = current_block_text[j]; switch (c) { case '\n': PRINT("\033[1;35m"); break; case '\a': case '\b': case '\033': case '\f': case '\r': case '\t': case '\v': case ' ': PRINT("\033[35m"); break; case 0x7Fu: PRINT("\033[36m"); break; default: if (c & 0x80u) { if (c == 0xFFu || c <= (unsigned char)' ' + 0x80u) PRINT("\033[32m"); else PRINT("\033[33m"); } else { if (c < (unsigned char)' ') PRINT("\033[36m"); } break; } PRINT(" %.2s\033[m", hex[c]); } if (end != vis_end || right_pad) PRINT("%*s", (int)((vis_end - end) * 3u + right_pad), ""); PRINT(" "); next: /* TODO need to fix colour support for less capable terminals */ /* TODO make more granular */ if (line_count == 0u) PRINT("\033[100m%s\033[m", " "); else if (i < text_top || i >= text_bottom) PRINT("\033[100m%s\033[m", " "); else if (active_focus == BLOCK_DISPLAY) PRINT("\033[104m%s\033[m", " "); else PRINT("\033[107m%s\033[m", " "); PRINT("\033[K\n"); } PRINT("\033[7;1m%*s\033[m\033[H", (int)term_width, ""); } static int ensure_selected_block_visible(void) { if (selected_block < first_visible_block) { first_visible_block = selected_block; return 1; } else if (term_height < 3u) { first_visible_block = selected_block; return 0; } else if (selected_block - first_visible_block > term_height - 3u) { first_visible_block = selected_block - (term_height - 3u); return 1; } else { return 0; } } static int handle_keyboard_input_global(union libterminput_input *input) { if (input->type != LIBTERMINPUT_KEYPRESS) return 0; if (IS_CTRL_KEY(input, 'Z')) { restore_terminal(); raise(SIGTSTP); configure_terminal(); redraw: interrupt_deferred = 1; term_resized = 1; } else if (IS_CTRL_KEY(input, 'L')) { goto redraw; } else if (IS_CTRL_KEY(input, 'C') || IS_CTRL_KEY(input, 'Q')) { exiting = 1; } else if (IS_KEY(input, 't')) { /* TODO need a horizontal display offset */ display_as_text ^= 1; line_count = 0u; do_redraw(); } else if (input->keypress.key == LIBTERMINPUT_TAB) { if (input->keypress.mods & LIBTERMINPUT_SHIFT) goto backtab; active_focus += 1; active_focus %= NCOMPONENTS; do_redraw(); } else if (input->keypress.key == LIBTERMINPUT_BACKTAB) { backtab: active_focus -= 1; if (active_focus < 0) active_focus += NCOMPONENTS; do_redraw(); } else { return 0; } /* TODO delete key should unlist a block */ /* TODO should be able to add prior/next block */ /* TODO should be able to save blocks to file */ return 1; } static void handle_keyboard_input_to_block_selector(union libterminput_input *input) { if (input->type != LIBTERMINPUT_KEYPRESS) return; /* TODO optimise redraw to only affected areas */ /* TODO defer redraw until all repeations have been processed */ if (input->keypress.key == LIBTERMINPUT_UP) { if (selected_block > 0u) { first_visible_line = 0u; line_count = 0u; selected_block -= 1u; ensure_selected_block_visible(); do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_DOWN) { if (selected_block < nblocks - 1u) { first_visible_line = 0u; line_count = 0u; selected_block += 1u; ensure_selected_block_visible(); do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_HOME) { if (selected_block > 0u) { first_visible_line = 0u; line_count = 0u; selected_block = 0u; ensure_selected_block_visible(); do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_END) { if (selected_block < nblocks - 1u) { first_visible_line = 0u; line_count = 0u; selected_block = nblocks - 1u; ensure_selected_block_visible(); do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_PRIOR) { if (selected_block > 0u && term_height > 2u) { first_visible_line = 0u; line_count = 0u; if (selected_block != first_visible_block) { selected_block = first_visible_block; do_redraw(); } else { if (selected_block > term_height - 2u) selected_block -= term_height - 2u; else selected_block = 0u; ensure_selected_block_visible(); do_redraw(); } } } else if (input->keypress.key == LIBTERMINPUT_NEXT) { if (selected_block < nblocks - 1u && term_height > 2u) { size_t last = first_visible_block + (term_height - 3u); first_visible_line = 0u; line_count = 0u; if (selected_block != last) { selected_block = last; do_redraw(); } else { if (term_height - 2u > nblocks - 1u - selected_block) selected_block = nblocks - 1u; else selected_block += term_height - 2u; ensure_selected_block_visible(); do_redraw(); } } } } static void handle_keyboard_input_to_block_display(union libterminput_input *input) { if (input->type != LIBTERMINPUT_KEYPRESS) return; /* TODO optimise redraw to only affected areas */ /* TODO defer redraw until all repeations have been processed */ if (input->keypress.key == LIBTERMINPUT_UP) { if (first_visible_line > 0u) { first_visible_line -= 1u; do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_DOWN) { if (line_count > first_visible_line && term_height > 2u) { if (term_height - 2u < line_count - first_visible_line) { first_visible_line += 1; do_redraw(); } } } else if (input->keypress.key == LIBTERMINPUT_HOME) { if (first_visible_line != 0u) { first_visible_line = 0u; do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_END) { if (line_count && term_height > 2u && line_count > term_height - 1u) { if (first_visible_line != line_count - (term_height - 2u)) { first_visible_line = line_count - (term_height - 2u); do_redraw(); } } } else if (input->keypress.key == LIBTERMINPUT_PRIOR) { if (first_visible_line != 0u && term_height > 2u) { if (first_visible_line < term_height - 2u) first_visible_line = 0u; else first_visible_line -= term_height - 2u; do_redraw(); } } else if (input->keypress.key == LIBTERMINPUT_NEXT) { if (term_height > 2u) { size_t prev = first_visible_line; first_visible_line += term_height - 2u; if (first_visible_line + term_height - 2u > line_count) { if (line_count > term_height - 2u) first_visible_line = line_count - (term_height - 2u); else first_visible_line = 0u; } if (first_visible_line != prev) do_redraw(); } } } static void handle_keyboard_input(union libterminput_input *input) { if (handle_keyboard_input_global(input)) return; if (active_focus == BLOCK_SELECTOR) handle_keyboard_input_to_block_selector(input); else if (active_focus == BLOCK_DISPLAY) handle_keyboard_input_to_block_display(input); } static void enter_subterminal(void) { if (dprintf(ttyfd, "\033[?1049h" /* enter subterminal */ "\033[?25l" /* hide cursor */ "\033[H\033[2J" /* clear terminal */ ) < 0) eprintf("dprintf /dev/tty:"); term_entered_submode_stdout = 1; } static void exit_subterminal(void) { if (term_entered_submode_stdout) { term_entered_submode_stdout = 0; dprintf(ttyfd, "\033[H\033[2J" /* clear terminal */ "\033[?25h" /* show cursor */ "\033[?1049l" /* exit subterminal */ ); } } static void set_term_attributes(void) { struct termios new_term_attributes; if (tcgetattr(ttyfd, &term_attributes)) eprintf("tcgetattr /dev/tty:"); term_attributes_saved = 1; memcpy(&new_term_attributes, &term_attributes, sizeof(term_attributes)); new_term_attributes.c_iflag &= (tcflag_t)~(IXON | IXANY | IXOFF); new_term_attributes.c_lflag &= (tcflag_t)~(ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHONL); if (tcsetattr(ttyfd, TCSANOW, &new_term_attributes)) weprintf("tcsetattr /dev/tty TCSANOW:"); } static void restore_term_attributes(void) { if (term_attributes_saved) { term_attributes_saved = 0; if (tcsetattr(ttyfd, TCSAFLUSH, &term_attributes)) weprintf("tcsetattr /dev/tty TCSAFLUSH:"); } } static void configure_terminal(void) { set_term_attributes(); enter_subterminal(); } static void restore_terminal(void) { restore_term_attributes(); exit_subterminal(); } static void cleanup(void) { restore_terminal(); close(ttyfd); ttyfd = -1; } static void term_resized_callback(int signo) { (void) signo; term_resized = 1; } static void subscribe_term_resize(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = &term_resized_callback; if (sigaction(SIGWINCH, &sa, NULL)) eprintf("sigaction SIGWINCH:"); } static void update_term_size(void) { struct winsize ws; if (ioctl(ttyfd, TIOCGWINSZ, &ws)) eprintf("ioclt /dev/tty TIOCGWINSZ:"); term_width = ws.ws_col; term_height = ws.ws_row; } #if defined(__GNUC__) __attribute__((__pure__)) #endif static uintmax_t parse_uint_arg(const char *s, uintmax_t min, uintmax_t max) { uintmax_t v = 0u, d; if (!*s) usage(); while ('0' <= *s && *s <= '9') { d = (uintmax_t)(*s++ - '0'); if (d > 9u || v > (max - d) / 10u) /* we know that max>d */ usage(); v = v * 10u + d; } if (*s || v < min) usage(); return v; } #if defined(__GNUC__) __attribute__((__pure__)) #endif static int get_fd(const char *s) { int fd, d; if (!strcmp(s, "/dev/stdin")) return STDIN_FILENO; if (!strcmp(s, "/dev/stdout")) return STDOUT_FILENO; if (!strcmp(s, "/dev/stderr")) return STDERR_FILENO; if (!strstarts(s, "/proc/self/fd/")) s = &s[sizeof("/proc/self/fd/") - 1u]; else if (!strstarts(s, "/dev/fd/")) s = &s[sizeof("/dev/fd/") - 1u]; else return -1; if ('0' > *s || *s > '9') return -1; fd = 0; while ('0' <= *s && *s <= '9') { d = *s++ - '0'; if (d < 0 || d > 9 || fd > (INT_MAX - d) / 10) return -1; fd = fd * 10 + d; } return fd; } static int offpcmp(const void *a_, const void *b_) { const off_t *a = a_, *b = b_; return *a < *b ? -1 : *a > *b; } static void add_to_block_list(off_t block) { if (nblocks == blocks_size) blocks = ereallocarray(blocks, blocks_size += 64u, sizeof(*blocks)); blocks[nblocks++] = block; } static void read_block_list(int fd, const char *fname) { char buf[4096]; ssize_t r; size_t i, n; off_t v = 0, d; int have_v = 0; for (;;) { r = read(fd, buf, sizeof(buf)); if (r <= 0) { if (!r) break; if (errno == EINTR) { interrupt_deferred = 1; continue; } eprintf("read %s:", fname); } n = (size_t)r; for (i = 0u; i < n; i++) { if (buf[i] == '\n') { if (!have_v) continue; have_v = 0; add_to_block_list(v); v = 0; } else if ('0' <= buf[i] && buf[i] <= '9') { have_v = 1; d = (off_t)(buf[i] - '0'); if (v > (OFF_MAX - d) / 10) goto inval; v = v * 10 + d; } else { inval: eprintf("%s: invalid file content", fname); } } } if (have_v) add_to_block_list(v); if (!nblocks) goto inval; qsort(blocks, nblocks, sizeof(*blocks), &offpcmp); for (i = n = 1u; i < nblocks; i++) if (blocks[i] != blocks[n - 1]) blocks[n++] = blocks[i]; nblocks = n; } int main(int argc, char *argv[]) { struct libterminput_state input_ctx; union libterminput_input input; const char *listpath, *env; int listfd; ARGBEGIN { case 'b': blksize = (size_t)parse_uint_arg(ARG(), 1u, SIZE_MAX); break; default: usage(); } ARGEND; if (argc < 1 || argc > 2) usage(); path = argv[0]; if (!*path) usage(); listpath = argv[1]; if (!listpath || !strcmp(listpath, "-")) listpath = "/dev/stdin"; else if (!*listpath) usage(); /* Open terminal and ensure process is running in the foreground */ ttyfd = open("/dev/tty", O_RDWR); if (ttyfd < 0) eprintf("open /dev/tty O_RDWR:"); if (getpgrp() != tcgetpgrp(ttyfd)) eprintf("process not running in the foreground"); /* Open block list */ listfd = get_fd(listpath); if (listfd < 0) { listfd = open(listpath, O_RDONLY); if (listfd < 0) eprintf("open %s O_RDONLY", listpath); } /* Open file to display */ textfd = open(path, O_RDONLY); if (textfd < 0) eprintf("open %s O_RDONLY", path); /* Advise the kernel about the access pattern */ posix_fadvise(textfd, 0, 0, POSIX_FADV_RANDOM); /* Set up cleanup on exit */ atexit(&cleanup); libsimple_eprintf_preprint = &cleanup; /* Read block list */ read_block_list(listfd, listpath); close(listfd); max_block_len = snprintf(NULL, 0u, "%ji", (intmax_t)blocks[nblocks - 1u]); if (max_block_len < 1) eprintf("snprintf %%ji:"); /* Configure UI */ env = getenv("TERM"); limited_block_drawing = (!env || !*env || !strcasecmp(env, "linux")); /* UI loop */ update_term_size(); subscribe_term_resize(); configure_terminal(); memset(&input_ctx, 0, sizeof(input_ctx)); while (libterminput_init(&input_ctx, ttyfd)) { if (errno == EINTR) { interrupt_deferred = 1; continue; } eprintf("libterminput_init /dev/tty:"); } do_redraw(); do { if (interrupt_deferred) { interrupt_deferred = 0; if (term_resized) { term_resized = 0; update_term_size(); do_redraw(); } } switch (libterminput_read(ttyfd, &input, &input_ctx)) { case 1: handle_keyboard_input(&input); break; case 0: exiting = 1; break; default: if (errno == EINTR) interrupt_deferred = 1; else eprintf("libterminput_read /dev/tty:"); break; } } while (!exiting); libterminput_destroy(&input_ctx); close(textfd); free(current_block_text); free(line_map); return 0; }