diff options
Diffstat (limited to '')
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 22 | ||||
| -rw-r--r-- | TODO | 2 | ||||
| -rw-r--r-- | blkseekr.c | 63 | ||||
| -rw-r--r-- | blkshowr.c | 988 | ||||
| -rw-r--r-- | config.mk | 2 |
6 files changed, 1051 insertions, 27 deletions
@@ -13,3 +13,4 @@ *.gcno *.gcda /blkseekr +/blkshowr @@ -4,32 +4,38 @@ CONFIGFILE = config.mk include $(CONFIGFILE) OBJ =\ - blkseekr.o + blkseekr.o\ + blkshowr.o HDR = -all: blkseekr +all: blkseekr blkshowr $(OBJ): $(HDR) .c.o: $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) -blkseekr: $(OBJ) - $(CC) -o $@ $(OBJ) $(LDFLAGS) +blkseekr: blkseekr.o + $(CC) -o $@ $@.o $(LDFLAGS) -install: blkseekr +blkshowr: blkshowr.o + $(CC) -o $@ $@.o $(LDFLAGS) + +install: blkseekr blkshowr mkdir -p -- "$(DESTDIR)$(PREFIX)/bin" mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" - cp -- blkseekr "$(DESTDIR)$(PREFIX)/bin/" -# TODO cp -- blkseekr.1 "$(DESTDIR)$(MANPREFIX)/man1/" + cp -- blkseekr blkshowr "$(DESTDIR)$(PREFIX)/bin/" +# TODO cp -- blkseekr.1 blkshowr.1 "$(DESTDIR)$(MANPREFIX)/man1/" uninstall: -rm -f -- "$(DESTDIR)$(PREFIX)/bin/blkseekr" + -rm -f -- "$(DESTDIR)$(PREFIX)/bin/blkshowr" -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/blkseekr.1" + -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/blkshowr.1" clean: -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda - -rm -f -- blkseekr + -rm -f -- blkseekr blkshowr .SUFFIXES: .SUFFIXES: .o .c @@ -0,0 +1,2 @@ +Add support for NUL bytes in regex (once supported in libautomata) +On SIGINT stop and print last completed block @@ -1,7 +1,7 @@ /* See LICENSE file for copyright and license details. */ -#include <libsimple-arg.h> -#include <libsimple.h> #include <regex.h> +#include <libsimple.h> +#include <libsimple-arg.h> #include <libautomata.h> USAGE("[-b block-size] [-B buffer-size] [-h] " @@ -9,14 +9,6 @@ USAGE("[-b block-size] [-B buffer-size] [-h] " "file [first-block [last-block]]"); -/* - * TODO on SIGINT stop and print last completed block - * TODO show progress - * TODO make program that extracts (binary) or displays (hex + text) the blocks - * TODO add support for NUL support in regex (once supported in libautomata) - */ - - enum automata_type { FIXED, FIXED_ICASE, @@ -49,6 +41,8 @@ static char *blkmarks; static size_t nblkmarks; static size_t low_mark; static size_t high_mark; +static int stdout_is_a_tty; +static int stderr_is_a_tty; static void @@ -146,8 +140,10 @@ analyse_block(char *block, size_t start_end, size_t available) } for (i = 0u; i < nblkmarks; i++) { - printf("Found expression beginning in block %ju\n", (uintmax_t)position / blksize + i); - fflush(stdout); + if (blkmarks[i]) { + printf("%ju%s\n", (uintmax_t)position / blksize + i, stdout_is_a_tty ? "\033[K" : ""); + fflush(stdout); + } } } @@ -272,7 +268,7 @@ parse_uint_arg(const char *s, uintmax_t min, uintmax_t max) d = (uintmax_t)(*s++ - '0'); if (d > 9u || v > (max - d) / 10u) /* we know that max>d */ usage(); - v = v * 10u + v; + v = v * 10u + d; } if (*s || v < min) usage(); @@ -402,7 +398,17 @@ main(int argc, char *argv[]) if (argc < 1 || argc > 3 || have_unused_flags || !nautomata) usage(); + stderr_is_a_tty = isatty(STDERR_FILENO); + if (!stderr_is_a_tty && errno == EBADF) + exit(1); + + stdout_is_a_tty = isatty(STDOUT_FILENO); + if (!stdout_is_a_tty && errno == EBADF) + eprintf("isatty STDOUT_FILENO:"); + path = argv[0]; + if (!*path) + usage(); if (argc >= 2) first_block = (off_t)parse_uint_arg(argv[1], 1u, (uintmax_t)OFF_MAX); @@ -427,11 +433,11 @@ main(int argc, char *argv[]) /* Open file to analyse */ fd = open(path, O_RDONLY); if (fd < 0) - eprintf("open %s O_RDONLY", path); + eprintf("open %s O_RDONLY:", path); /* Advise the kernel about the access pattern */ posix_fadvise(fd, 0, first_block, POSIX_FADV_DONTNEED); - if (last_block >= 0) { + if (last_block > 0) { posix_fadvise(fd, last_block, 0, POSIX_FADV_DONTNEED); posix_fadvise(fd, first_block, last_block, POSIX_FADV_SEQUENTIAL); posix_fadvise(fd, first_block, last_block, POSIX_FADV_NOREUSE); @@ -449,6 +455,11 @@ main(int argc, char *argv[]) /* Skip to first block to analyse */ if (first_block && lseek(fd, first_block, SEEK_SET) == (off_t)-1) { off_t remaining = first_block; + if (stderr_is_a_tty) { + /* TODO show progress */ + fprintf(stderr, "Skipping...\n\033[A"); + fflush(stderr); + } while (remaining) { uintmax_t n = MIN((uintmax_t)remaining, (uintmax_t)bufsize); ssize_t r = read(fd, text, (size_t)n); @@ -466,6 +477,12 @@ main(int argc, char *argv[]) /* Analyse to last block (to end of file if last_block < 0) */ for (; last_block < 0 || first_block < last_block; first_block = data) { + if (stderr_is_a_tty) { + /* TODO improve output */ + fprintf(stderr, "Reading at %ju\033[K\n\033[A", (uintmax_t)position + available); + fflush(stderr); + } + /* Seek to data to find end of hole, and analyse the hole */ if (skip_holes) { data = lseek(fd, first_block, SEEK_DATA); @@ -474,7 +491,7 @@ main(int argc, char *argv[]) if (errno != EBADF) skip_holes = 0; else - eprintf("lseek %s %ji SEEK_DATA", path, (intmax_t)first_block); + eprintf("lseek %s %ji SEEK_DATA:", path, (intmax_t)first_block); } else { available = encountered_hole(text, available, data - first_block); } @@ -489,10 +506,10 @@ main(int argc, char *argv[]) if (errno != EBADF) skip_holes = 0; else - eprintf("lseek %s %ji SEEK_HOLE", path, (intmax_t)data); + eprintf("lseek %s %ji SEEK_HOLE:", path, (intmax_t)data); } else { if (lseek(fd, data, SEEK_SET) == (off_t)-1) - eprintf("lseek %s %ji SEEK_SET", path, (intmax_t)data); + eprintf("lseek %s %ji SEEK_SET:", path, (intmax_t)data); } } else { hole = (off_t)-1; @@ -513,6 +530,11 @@ main(int argc, char *argv[]) } else { break; } + if (stderr_is_a_tty) { + /* TODO improve output */ + fprintf(stderr, "Reading at %ju\033[K\n\033[A", (uintmax_t)position + available); + fflush(stderr); + } r = read(fd, &text[available], n); if (r <= 0) { if (!r) @@ -535,6 +557,11 @@ eof: encountered_eof(text, available); out: + if (stderr_is_a_tty) { + fprintf(stderr, "\033[K"); + fflush(stderr); + } + close(fd); while (nautomata--) { diff --git a/blkshowr.c b/blkshowr.c new file mode 100644 index 0000000..3228d12 --- /dev/null +++ b/blkshowr.c @@ -0,0 +1,988 @@ +/* See LICENSE file for copyright and license details. */ +#include <termios.h> +#include <libsimple.h> +#include <libsimple-arg.h> +#include <libterminput.h> + +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 int deferred_redraw = 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; + + deferred_redraw = 0; + + 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 */ + + if (input->keypress.key == LIBTERMINPUT_UP) { + if (selected_block > 0u) { + first_visible_line = 0u; + line_count = 0u; + selected_block -= 1u; + ensure_selected_block_visible(); + deferred_redraw |= input->keypress.times <= 1u; + } + + } 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(); + deferred_redraw |= input->keypress.times <= 1u; + } + + } 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(); + if (input->keypress.times <= 1u) + 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(); + if (input->keypress.times <= 1u) + 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; + if (input->keypress.times <= 1u) + do_redraw(); + } else { + if (selected_block > term_height - 2u) + selected_block -= term_height - 2u; + else + selected_block = 0u; + ensure_selected_block_visible(); + if (input->keypress.times <= 1u) + 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; + if (input->keypress.times <= 1u) + 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(); + if (input->keypress.times <= 1u) + 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 */ + + if (input->keypress.key == LIBTERMINPUT_UP) { + if (first_visible_line > 0u) { + first_visible_line -= 1u; + deferred_redraw |= input->keypress.times <= 1u; + } + + } 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; + deferred_redraw |= input->keypress.times <= 1u; + } + } + + } else if (input->keypress.key == LIBTERMINPUT_HOME) { + if (first_visible_line != 0u) { + first_visible_line = 0u; + if (input->keypress.times <= 1u) + 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); + if (input->keypress.times <= 1u) + 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; + if (input->keypress.times <= 1u) + 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) + if (input->keypress.times <= 1u) + 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, ttyfd_nb, fd; + + 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"); + /* O_NONBLOCK is on open file descriptor level and preadv2(2) (with RWF_NOWAIT) + * do not support TTYs (how knows why, pipes are supports, ...) */ + ttyfd_nb = open("/dev/tty", O_RDWR | O_NONBLOCK); + if (ttyfd_nb < 0) + eprintf("open /dev/tty O_RDWR|O_NONBLOCK:"); + if (getpgrp() != tcgetpgrp(ttyfd_nb)) + 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(); + } + } + + for (fd = ttyfd;; fd = ttyfd_nb) { + switch (libterminput_read(fd, &input, &input_ctx)) { + case 1: + handle_keyboard_input(&input); + break; + case 0: + exiting = 1; + goto read_blocked; + default: + if (errno == EINTR) + interrupt_deferred = 1; + else if (errno != EAGAIN) + eprintf("libterminput_read /dev/tty:"); + goto read_blocked; + } + } + + read_blocked: + if (deferred_redraw) + do_redraw(); + } while (!exiting); + libterminput_destroy(&input_ctx); + + close(textfd); + close(ttyfd_nb); + free(current_block_text); + free(line_map); + return 0; +} @@ -5,4 +5,4 @@ CC = c99 CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE CFLAGS = -LDFLAGS = -lsimple -lautomata +LDFLAGS = -lsimple -lautomata -lterminput |
