summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--Makefile22
-rw-r--r--TODO2
-rw-r--r--blkseekr.c63
-rw-r--r--blkshowr.c988
-rw-r--r--config.mk2
6 files changed, 1051 insertions, 27 deletions
diff --git a/.gitignore b/.gitignore
index 031be57..13c38d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
*.gcno
*.gcda
/blkseekr
+/blkshowr
diff --git a/Makefile b/Makefile
index 99af225..b21a637 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..31f9fc2
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+Add support for NUL bytes in regex (once supported in libautomata)
+On SIGINT stop and print last completed block
diff --git a/blkseekr.c b/blkseekr.c
index aa55f0b..94d4369 100644
--- a/blkseekr.c
+++ b/blkseekr.c
@@ -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, &current_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;
+}
diff --git a/config.mk b/config.mk
index ee7fff4..22ca619 100644
--- a/config.mk
+++ b/config.mk
@@ -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