hyx

Minimalist but powerful terminal hex editor
git clone https://git.sinitax.com/yx7/hyx
Log | Files | Refs | sfeed.txt

commit 6d739dcd7fccb99171dde3bafa3cdca043105dcd
Author: Louis Burda <quent.burda@gmail.com>
Date:   Tue, 10 Dec 2024 08:18:27 +0100

Add version 2024.02.29

Diffstat:
A.gitignore | 1+
AMakefile | 24++++++++++++++++++++++++
Aansi.h | 27+++++++++++++++++++++++++++
Ablob.c | 375+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ablob.h | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon.c | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Ahistory.c | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahistory.h | 19+++++++++++++++++++
Ahyx.c | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainput.c | 863+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainput.h | 35+++++++++++++++++++++++++++++++++++
Alicense.txt | 19+++++++++++++++++++
Aview.c | 360+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aview.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 2290 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +hyx diff --git a/Makefile b/Makefile @@ -0,0 +1,24 @@ + +all: CFLAGS ?= -O2 \ + -pedantic -Wall -Wextra -DNDEBUG \ + -D_FORTIFY_SOURCE=2 -fstack-protector-all +all: CFLAGS += -std=c99 +all: hyx + +debug: CFLAGS ?= -O0 -g \ + -fsanitize=undefined \ + -std=c99 -pedantic -Wall -Wextra -Werror \ + -fstack-protector-all +debug: CFLAGS += -std=c99 +debug: hyx + +hyx: *.h *.c + $(CC) \ + $(CFLAGS) \ + $(LDFLAGS) \ + hyx.c common.c blob.c history.c view.c input.c \ + -o hyx + +clean: + rm -f hyx + diff --git a/ansi.h b/ansi.h @@ -0,0 +1,27 @@ +#ifndef ANSI_H +#define ANSI_H + +/* ANSI terminal escape sequences. */ + +static char const bold_on[] = "\x1b[1m", bold_off[] = "\x1b[22m"; /* not a typo */ +static char const underline_on[] = "\x1b[4m", underline_off[] = "\x1b[24m"; +static char const inverse_video_on[] = "\x1b[7m", inverse_video_off[] = "\x1b[27m"; +static char const clear_screen[] = "\x1b[2J"; +static char const clear_line[] = "\x1b[K"; +static inline void cursor_line(unsigned n) { printf("\x1b[%uH", n + 1); } +static inline void cursor_column(unsigned n) { printf("\x1b[%uG", n + 1); } +static char const show_cursor[] = "\x1b[?25h", hide_cursor[] = "\x1b[?25l"; +static char const color_black[] = "\x1b[30m"; +static char const color_red[] = "\x1b[31m"; +static char const color_green[] = "\x1b[32m"; +static char const color_yellow[] = "\x1b[33m"; +static char const color_blue[] = "\x1b[34m"; +static char const color_purple[] = "\x1b[35m"; +static char const color_cyan[] = "\x1b[36m"; +static char const color_white[] = "\x1b[37m"; +static char const color_normal[] = "\x1b[39m"; + +static char const enter_alternate_screen[] = "\x1b[?1049h\x1b[0;0H"; +static char const leave_alternate_screen[] = "\x1b[?1049l"; + +#endif diff --git a/blob.c b/blob.c @@ -0,0 +1,375 @@ + +#include "common.h" +#include "blob.h" + +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/mman.h> + +void blob_init(struct blob *blob) +{ + memset(blob, 0, sizeof(*blob)); + history_init(&blob->undo); + history_init(&blob->redo); +} + +void blob_replace(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history) +{ + assert(pos + len <= blob->len); + + if (save_history) { + history_free(&blob->redo); + history_save(&blob->undo, REPLACE, blob, pos, len); + ++blob->saved_dist; + } + + if (blob->dirty) + for (size_t i = pos / 0x1000; i < (pos + len + 0xfff) / 0x1000; ++i) + blob->dirty[i / 8] |= 1 << i % 8; + + memcpy(blob->data + pos, data, len); +} + +void blob_insert(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history) +{ + assert(pos <= blob->len); + assert(blob_can_move(blob)); + assert(len); + assert(!blob->dirty); /* not implemented */ + + if (save_history) { + history_free(&blob->redo); + history_save(&blob->undo, INSERT, blob, pos, len); + ++blob->saved_dist; + } + + blob->data = realloc_strict(blob->data, blob->len += len); + + memmove(blob->data + pos + len, blob->data + pos, blob->len - pos - len); + memcpy(blob->data + pos, data, len); +} + +void blob_delete(struct blob *blob, size_t pos, size_t len, bool save_history) +{ + assert(pos + len <= blob->len); + assert(blob_can_move(blob)); + assert(len); + assert(!blob->dirty); /* not implemented */ + + if (save_history) { + history_free(&blob->redo); + history_save(&blob->undo, DELETE, blob, pos, len); + ++blob->saved_dist; + } + + memmove(blob->data + pos, blob->data + pos + len, (blob->len -= len) - pos); + blob->data = realloc_strict(blob->data, blob->len); +} + +void blob_free(struct blob *blob) +{ + free(blob->filename); + + switch (blob->alloc) { + case BLOB_MALLOC: + free(blob->data); + break; + case BLOB_MMAP: + free(blob->dirty); + munmap_strict(blob->data, blob->len); + break; + } + + free(blob->clipboard.data); + + history_free(&blob->undo); + history_free(&blob->redo); +} + +bool blob_can_move(struct blob const *blob) +{ + return blob->alloc == BLOB_MALLOC; +} + +bool blob_undo(struct blob *blob, size_t *pos) +{ + bool r = history_step(&blob->undo, blob, &blob->redo, pos); + blob->saved_dist -= r; + return r; +} + +bool blob_redo(struct blob *blob, size_t *pos) +{ + bool r = history_step(&blob->redo, blob, &blob->undo, pos); + blob->saved_dist += r; + return r; +} + +void blob_yank(struct blob *blob, size_t pos, size_t len) +{ + free(blob->clipboard.data); + blob->clipboard.data = NULL; + + if (pos < blob_length(blob)) { + blob->clipboard.data = malloc_strict(blob->clipboard.len = len); + blob_read_strict(blob, pos, blob->clipboard.data, blob->clipboard.len); + } +} + +size_t blob_paste(struct blob *blob, size_t pos, enum op_type type) +{ + if (!blob->clipboard.data) return 0; + + switch (type) { + case REPLACE: + blob_replace(blob, pos, blob->clipboard.data, min(blob->clipboard.len, blob->len - pos), true); + break; + case INSERT: + blob_insert(blob, pos, blob->clipboard.data, blob->clipboard.len, true); + break; + default: + die("bad operation"); + } + + return blob->clipboard.len; +} + +#define DD(F,B) (dir > 0 ? (F) : (B)) + +/* modified Boyer-Moore-Horspool algorithm. */ +static ssize_t blob_search_range(struct blob *blob, byte const *needle, size_t len, size_t start, ssize_t end, ssize_t dir, size_t tab[256]) +{ + size_t blen = blob_length(blob); + + assert(start < blen && end >= -1 && end <= (ssize_t) blen); + assert(DD((ssize_t) start <= end, end <= (ssize_t) start)); + + if (len > DD(end-start, start-end)) /* needle longer than range */ + return -1; + + for (ssize_t i = start; DD(i < end, i > end) ; ) { + + if (i + len > blen) { + /* not enough space for pattern: skip */ + i += dir; + continue; + } + assert(i >= 0 && i + len <= blen); + + bool found = true; + for (ssize_t j = DD(len-1, 0); found && j >= 0 && (size_t) j < len; j -= dir) + found = blob_at(blob, i + j) == needle[j]; + if (found) + return i; + + i += dir * (ssize_t) tab[blob_at(blob, i + (dir > 0 ? len - 1 : 0))]; + + } + + /* not found */ + return -1; +} + +ssize_t blob_search(struct blob *blob, byte const *needle, size_t len, size_t start, ssize_t dir) +{ + size_t blen = blob_length(blob); + + if (!len || len > blen) + return -1; + + assert(start < blen); + assert(dir == +1 || dir == -1); + + /* could do preprocessing once per needle/dir pair, but patterns are usually short */ + size_t tab[256]; + for (size_t j = 0; j < 256; ++j) + tab[j] = len; + for (size_t j = 0; j < len-1; ++j) + tab[needle[DD(j, len-1-j)]] = len-1-j; + + ssize_t r = blob_search_range(blob, needle, len, start, DD((ssize_t) blen, -1), dir, tab); + if (r < 0) /* wrap around */ + r = blob_search_range(blob, needle, len, DD(0, blen-1), start, dir, tab); + + return r; +} + +#undef DD + + +/* blob_load* functions must be called with a fresh struct from blob_init() */ + +void blob_load(struct blob *blob, char const *filename) +{ + struct stat st; + int fd; + void *ptr = NULL; + + if (!filename) + return; /* We are creating a new (still unnamed) file */ + + blob->filename = strdup(filename); + + errno = 0; + if (stat(filename, &st)) { + if (errno != ENOENT) + pdie("stat"); + return; /* We are creating a new file with given name */ + } + + if (0 > (fd = open(filename, O_RDONLY))) + pdie("open"); + + switch (st.st_mode & S_IFMT) { + case S_IFREG: + blob->len = st.st_size; + blob->alloc = blob->len >= CONFIG_LARGE_FILESIZE ? BLOB_MMAP : BLOB_MALLOC; + break; + case S_IFBLK: + blob->len = lseek_strict(fd, 0, SEEK_END); + blob->alloc = BLOB_MMAP; + break; + default: + die("unsupported file type"); + } + + if (blob->len) + ptr = mmap_strict(NULL, + blob->len, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_NORESERVE, + fd, + 0); + + switch (blob->alloc) { + + case BLOB_MMAP: + assert(ptr); + blob->data = ptr; + if (!(blob->dirty = calloc(((blob->len + 0xfff) / 0x1000 + 7) / 8, sizeof(*blob->dirty)))) + pdie("calloc"); + break; + + case BLOB_MALLOC: + blob->data = malloc_strict(blob->len); + if (ptr) { + memcpy(blob->data, ptr, blob->len); + munmap_strict(ptr, blob->len); + } + break; + + default: + die("bad blob type"); + } + + if (close(fd)) + pdie("close"); +} + +void blob_load_stream(struct blob *blob, FILE *fp) +{ + const size_t alloc_size = 0x1000; + size_t n = 0; + + while (true) { + assert(n <= blob->len); + + if (blob->len - n < alloc_size) + blob->data = realloc_strict(blob->data, (blob->len += alloc_size)); + + size_t r = fread(blob->data + n, 1, blob->len - n, fp); + if (!r) { + if (feof(fp)) break; + pdie("could not read data from stream"); + } + n += r; + } + blob->data = realloc(blob->data, (blob->len = n)); +} + +enum blob_save_error blob_save(struct blob *blob, char const *filename) +{ + int fd; + struct stat st; + byte const *ptr; + + if (filename) { + free(blob->filename); + blob->filename = strdup(filename); + } + else if (blob->filename) + filename = blob->filename; + else + return BLOB_SAVE_FILENAME; + + errno = 0; + if (0 > (fd = open(filename, + O_WRONLY | O_CREAT, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) { + switch (errno) { + case ENOENT: return BLOB_SAVE_NONEXISTENT; + case EACCES: return BLOB_SAVE_PERMISSIONS; + case ETXTBSY: return BLOB_SAVE_BUSY; + default: pdie("open"); + } + } + + if (fstat(fd, &st)) + pdie("fstat"); + + if ((st.st_mode & S_IFMT) == S_IFREG && ftruncate(fd, blob->len)) + pdie("ftruncate"); + + for (size_t i = 0, n; i < blob->len; i += n) { + + if (blob->dirty && !(blob->dirty[i / 0x1000 / 8] & (1 << i / 0x1000 % 8))) { + n = 0x1000 - i % 0x1000; + continue; + } + + ptr = blob_lookup(blob, i, &n); + if (blob->dirty) + n = min(0x1000 - i % 0x1000, n); + + if ((ssize_t) i != lseek(fd, i, SEEK_SET)) + pdie("lseek"); + + if (0 >= (n = write(fd, ptr, n))) + pdie("write"); + } + + if (close(fd)) + pdie("close"); + + blob->saved_dist = 0; + + return BLOB_SAVE_OK; +} + +bool blob_is_saved(struct blob const *blob) +{ + return !blob->saved_dist; +} + +byte const *blob_lookup(struct blob const *blob, size_t pos, size_t *len) +{ + assert(pos < blob->len); + + if (len) + *len = blob->len - pos; + return blob->data + pos; +} + +void blob_read_strict(struct blob *blob, size_t pos, byte *buf, size_t len) +{ + byte const *ptr; + for (size_t i = 0, n; i < len; i += n) { + ptr = blob_lookup(blob, pos, &n); + memcpy(buf + i, ptr, (n = min(len - i, n))); + } +} + diff --git a/blob.h b/blob.h @@ -0,0 +1,65 @@ +#ifndef BLOB_H +#define BLOB_H + +#include "common.h" +#include "history.h" + +enum blob_alloc { + BLOB_MALLOC = 0, + BLOB_MMAP, +}; + +struct blob { + enum blob_alloc alloc; + + size_t len; + byte *data; + + char *filename; + + uint8_t *dirty; + + struct diff *undo, *redo; + ssize_t saved_dist; + + struct { + size_t len; + byte *data; + } clipboard; +}; + +void blob_init(struct blob *blob); +void blob_replace(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history); +void blob_insert(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history); +void blob_delete(struct blob *blob, size_t pos, size_t len, bool save_history); +void blob_free(struct blob *blob); + +bool blob_can_move(struct blob const *blob); + +bool blob_undo(struct blob *blob, size_t *pos); +bool blob_redo(struct blob *blob, size_t *pos); + +void blob_yank(struct blob *blob, size_t pos, size_t len); +size_t blob_paste(struct blob *blob, size_t pos, enum op_type type); + +ssize_t blob_search(struct blob *blob, byte const *needle, size_t len, size_t start, ssize_t dir); + +void blob_load(struct blob *blob, char const *filename); +void blob_load_stream(struct blob *blob, FILE *fp); +enum blob_save_error { + BLOB_SAVE_OK = 0, + BLOB_SAVE_FILENAME, + BLOB_SAVE_NONEXISTENT, + BLOB_SAVE_PERMISSIONS, + BLOB_SAVE_BUSY, +} blob_save(struct blob *blob, char const *filename); +bool blob_is_saved(struct blob const *blob); + +static inline size_t blob_length(struct blob const *blob) + { return blob->len; } +byte const *blob_lookup(struct blob const *blob, size_t pos, size_t *len); +static inline byte blob_at(struct blob const *blob, size_t pos) + { return *blob_lookup(blob, pos, NULL); } +void blob_read_strict(struct blob *blob, size_t pos, byte *buf, size_t len); + +#endif diff --git a/common.c b/common.c @@ -0,0 +1,77 @@ + +#include "common.h" + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <time.h> +#include <sys/mman.h> + +unsigned long bit_length(unsigned long n) +{ + unsigned long r = 0; + do ++r; while (n >>= 1); + return r; +} + + +void *malloc_strict(size_t len) +{ + void *ptr; + errno = 0; + if (!(ptr = malloc(len)) && errno) + pdie("malloc"); + return ptr; +} + +void *realloc_strict(void *ptr, size_t len) +{ + errno = 0; + if (!(ptr = realloc(ptr, len)) && errno) + pdie("realloc"); + return ptr; +} + +void *mmap_strict(void *addr, size_t len, int prot, int flags, int fildes, off_t off) +{ + void *ptr; + if (MAP_FAILED == (ptr = mmap(addr, len, prot, flags, fildes, off))) + pdie("mmap"); + return ptr; +} + +void munmap_strict(void *addr, size_t len) +{ + if (munmap(addr, len)) + pdie("munmap"); +} + +off_t lseek_strict(int fildes, off_t offset, int whence) +{ + off_t ret; + if (0 >= (ret = lseek(fildes, offset, whence))) + pdie("lseek"); + return ret; +} + +char *fgets_retry(char *s, int size, FILE *stream) +{ + char *ret; +retry: + errno = 0; + if (!(ret = fgets(s, size, stream))) { + if (errno == EINTR) + goto retry; + pdie("fgets"); + } + return ret; +} + +uint64_t monotonic_microtime() +{ + struct timespec t; + if (clock_gettime(CLOCK_MONOTONIC, &t)) + pdie("clock_gettime"); + return (uint64_t) t.tv_sec * 1000000 + t.tv_nsec / 1000; +} + diff --git a/common.h b/common.h @@ -0,0 +1,50 @@ +#ifndef COMMON_H +#define COMMON_H + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdint.h> +#include <stdbool.h> +#include <stdio.h> +#include <assert.h> +#include <unistd.h> + + +/* round columns to a multiple of this */ +#define CONFIG_ROUND_COLS 0x8 + +/* mmap files larger than this */ +#define CONFIG_LARGE_FILESIZE (256 * (1 << 20)) /* 256 megabytes */ + +/* microseconds to wait for the rest of what could be an escape sequence */ +#define CONFIG_WAIT_ESCAPE (10000) /* 10 milliseconds */ + + +typedef uint8_t byte; + +void die(char const *s) __attribute__((noreturn)); /* hyx.c */ +void pdie(char const *s) __attribute__((noreturn)); /* hyx.c */ + +static inline size_t min(size_t x, size_t y) + { return x < y ? x : y; } +static inline size_t max(size_t x, size_t y) + { return x > y ? x : y; } +static inline size_t absdiff(size_t x, size_t y) + { return x > y ? x - y : y - x; } + +unsigned long bit_length(unsigned long n); + +void *malloc_strict(size_t len); +void *realloc_strict(void *ptr, size_t len); + +void *mmap_strict(void *addr, size_t len, int prot, int flags, int fildes, off_t off); +void munmap_strict(void *addr, size_t len); + +off_t lseek_strict(int fildes, off_t offset, int whence); + +char *fgets_retry(char *s, int size, FILE *stream); + +uint64_t monotonic_microtime(); + +#endif diff --git a/history.c b/history.c @@ -0,0 +1,97 @@ + +#include "common.h" +#include "history.h" +#include "blob.h" + +#include <string.h> + +struct diff { + enum op_type type; + size_t pos; + byte *data; + size_t len; + struct diff *next; +}; + +static void diff_apply(struct blob *blob, struct diff *diff) +{ + switch (diff->type) { + case REPLACE: + blob_replace(blob, diff->pos, diff->data, diff->len, false); + break; + case INSERT: + blob_insert(blob, diff->pos, diff->data, diff->len, false); + break; + case DELETE: + blob_delete(blob, diff->pos, diff->len, false); + break; + default: + die("unknown operation"); + } +} + +void history_init(struct diff **history) +{ + *history = NULL; +} + +void history_free(struct diff **history) +{ + struct diff *tmp, *cur = *history; + while (cur) { + tmp = cur; + cur = cur->next; + free(tmp->data); + free(tmp); + } + *history = NULL; +} + +/* pushes a diff that _undoes_ the passed operation */ +void history_save(struct diff **history, enum op_type type, struct blob *blob, size_t pos, size_t len) +{ + struct diff *diff = malloc_strict(sizeof(*diff)); + diff->type = type; + diff->pos = pos; + diff->len = len; + diff->next = *history; + + switch (type) { + case DELETE: + diff->type = INSERT; + /* fall-through */ + case REPLACE: + blob_read_strict(blob, pos, diff->data = malloc_strict(len), len); + break; + case INSERT: + diff->type = DELETE; + diff->data = NULL; + break; + default: + die("unknown operation"); + } + + *history = diff; +} + +bool history_step(struct diff **from, struct blob *blob, struct diff **to, size_t *pos) +{ + struct diff *diff = *from; + + if (!diff) + return false; + + if (pos) + *pos = diff->pos; + + if (to) + history_save(to, diff->type, blob, diff->pos, diff->len); + + *from = diff->next; + diff_apply(blob, diff); + free(diff->data); + free(diff); + + return true; +} + diff --git a/history.h b/history.h @@ -0,0 +1,19 @@ +#ifndef HISTORY_H +#define HISTORY_H + +struct blob; + +enum op_type { + REPLACE, + INSERT, + DELETE, +}; + +struct diff; + +void history_init(struct diff **history); +void history_free(struct diff **history); +void history_save(struct diff **history, enum op_type type, struct blob *blob, size_t pos, size_t len); +bool history_step(struct diff **history, struct blob *blob, struct diff **target, size_t *pos); + +#endif diff --git a/hyx.c b/hyx.c @@ -0,0 +1,230 @@ +/* + * + * Copyright (c) 2016-2024 Lorenz Panny + * + * This is hyx version 2024.02.29. + * Check for newer versions at https://yx7.cc/code. + * Please report bugs to lorenz@yx7.cc. + * + * Contributors: + * 2018, anonymous Faster search algorithm. + * 2020, Leah Neukirchen Suspend on ^Z. File information on ^G. + * 2022, Jeffrey H. Johnson Makefile tweaks for MacOS. + * 2022, Mario Haustein Makefile tweaks for Gentoo. + * 2023, Josef Schönberger Use alternate screen. Home/End keys. + * + * This program is released under the MIT license; see license.txt. + * + */ + +#include "common.h" +#include "blob.h" +#include "view.h" +#include "input.h" +#include "ansi.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <signal.h> +#include <setjmp.h> + + +struct blob blob; +struct view view; +struct input input; + +bool quit; + +jmp_buf jmp_mainloop; + + +void die(char const *s) +{ + view_text(&view, true); + fprintf(stderr, "%s\n", s); + exit(EXIT_FAILURE); +} + +void pdie(char const *s) +{ + view_text(&view, true); + perror(s); + exit(EXIT_FAILURE); +} + +static void sighdlr(int num) +{ + switch (num) { + case SIGWINCH: + view.winch = true; + break; + case SIGTSTP: + view.tstp = true; + break; + case SIGCONT: + view.cont = true; + break; + case SIGALRM: + /* This is used in parsing escape sequences, + * but we don't need to do anything here. */ + break; + case SIGINT: + /* ignore */ + break; + default: + die("unrecognized signal"); + } +} + +__attribute__((noreturn)) void version() +{ + printf("This is hyx version 2024.02.29.\n"); + exit(EXIT_SUCCESS); +} + +__attribute__((noreturn)) void help(int st) +{ + bool tty = isatty(fileno(stdout)); + + printf("\n"); + printf(" %shyx: a minimalistic hex editor%s\n", + tty ? color_green : "", tty ? color_normal : ""); + printf(" ------------------------------\n\n"); + + printf(" %sinvocation:%s hyx [filename]\n", + tty ? color_yellow : "", tty ? color_normal : ""); + + printf(" %sinvocation:%s [command] | hyx\n\n", + tty ? color_yellow : "", tty ? color_normal : ""); + + printf(" %skeys:%s\n\n", + tty ? color_yellow : "", tty ? color_normal : ""); + printf("q quit\n"); + printf("\n"); + printf("h, j, k, l move cursor\n"); + printf("(hex digits) edit bytes (in hex mode)\n"); + printf("(printable) edit bytes (in ascii mode)\n"); + printf("i switch between replace and insert modes\n"); + printf("tab switch between hex and ascii input\n"); + printf("\n"); + printf("u undo\n"); + printf("ctrl+r redo\n"); + printf("\n"); + printf("v start a selection\n"); + printf("escape abort a selection\n"); + printf("x delete current byte or selection\n"); + printf("s substitute current byte or selection\n"); + printf("y copy current byte or selection to clipboard\n"); + printf("p paste\n"); + printf("P paste and move cursor\n"); + printf("\n"); + printf("], [ increase/decrease number of columns\n"); + printf("\n"); + printf("ctrl+u, ctrl+d scroll up/down one page\n"); + printf("g, G jump to start/end of screen or file\n"); + printf("^, $ jump to start/end of current line\n"); + printf("\n"); + printf(": enter command (see below)\n"); + printf("\n"); + printf("/x (hex string) search for hexadecimal bytes\n"); + printf("/s (characters) search for unicode string (utf8)\n"); + printf("/w (characters) search for unicode string (ucs2)\n"); + printf("n, N jump to next/previous match\n"); + printf("\n"); + printf("ctrl+a, ctrl+x increment/decrement current byte\n"); + printf("\n"); + printf("ctrl+g show file name and current position\n"); + printf("ctrl+z suspend editor; use \"fg\" to continue\n"); + printf("\n"); + + printf(" %scommands:%s\n\n", + tty ? color_yellow : "", tty ? color_normal : ""); + printf("$offset jump to offset (supports hex/dec/oct)\n"); + printf("q quit\n"); + printf("w [$filename] save\n"); + printf("wq [$filename] save and quit\n"); + printf("color y/n toggle colors\n"); + + printf("\n"); + + exit(st); +} + +int main(int argc, char **argv) +{ + struct sigaction sigact; + + char *filename = NULL; + + for (size_t i = 1; i < (size_t) argc; ++i) { + if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) + help(0); + else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--version")) + version(); + else if (!filename) + filename = argv[i]; + else + help(EXIT_FAILURE); + } + + blob_init(&blob); + if (!isatty(fileno(stdin))) { + if (filename) help(EXIT_FAILURE); + blob_load_stream(&blob, stdin); + if (!freopen("/dev/tty", "r", stdin)) + pdie("could not reopen controlling TTY"); + } + else { + blob_load(&blob, filename); + } + + view_init(&view, &blob, &input); + input_init(&input, &view); + + /* set up signal handler */ + memset(&sigact, 0, sizeof(sigact)); + sigact.sa_handler = sighdlr; + sigaction(SIGWINCH, &sigact, NULL); + sigaction(SIGTSTP, &sigact, NULL); + sigaction(SIGCONT, &sigact, NULL); + sigaction(SIGALRM, &sigact, NULL); + sigaction(SIGINT, &sigact, NULL); + + view_recompute(&view, true); + view_visual(&view); + + do { + /* This is used to redraw immediately when the window size changes. */ + setjmp(jmp_mainloop); + + if (view.winch) { + view_recompute(&view, true); + view.winch = false; + } + if (view.tstp) { + view_text(&view, true); + view.tstp = false; + raise(SIGSTOP); + /* should continue with view.cont == true */ + } + if (view.cont) { + view_recompute(&view, true); + view_dirty_from(&view, 0); + view_visual(&view); + view.cont = false; + } + assert(input.cur >= view.start && input.cur < view.start + view.rows * view.cols); + view_update(&view); + + input_get(&input, &quit); + + } while (!quit); + + view_text(&view, true); + + input_free(&input); + view_free(&view); + blob_free(&blob); +} + diff --git a/input.c b/input.c @@ -0,0 +1,863 @@ + +#include "common.h" +#include "blob.h" +#include "view.h" +#include "input.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <ctype.h> +#include <setjmp.h> + +#include <time.h> +#include <sys/time.h> + +extern jmp_buf jmp_mainloop; /* hyx.c */ + +void input_init(struct input *input, struct view *view) +{ + memset(input, 0, sizeof(*input)); + input->view = view; +} + +void input_free(struct input *input) +{ + free(input->search.needle); +} + +/* + * The C standard forbids assigning arbitrary integers to an enum, + * hence we do it the other way round: assign enumerated constants + * to a general integer type. + */ +typedef uint16_t key; +enum { + KEY_INTERRUPTED = 0x1000, + KEY_SPECIAL_ESCAPE, + KEY_SPECIAL_DELETE, + KEY_SPECIAL_UP, KEY_SPECIAL_DOWN, KEY_SPECIAL_RIGHT, KEY_SPECIAL_LEFT, + KEY_SPECIAL_PGUP, KEY_SPECIAL_PGDOWN, + KEY_SPECIAL_HOME, KEY_SPECIAL_END, +}; + +static key getch() +{ + int c; + errno = 0; + if (EOF == (c = getc(stdin))) { + if (errno == EINTR) + return KEY_INTERRUPTED; + pdie("getc"); + } + return c; +} + +static void ungetch(int c) +{ + if (c != ungetc(c, stdin)) + pdie("ungetc"); +} + +static key get_key() +{ + static const struct itimerval timeout = {{0}, {CONFIG_WAIT_ESCAPE / 1000000, CONFIG_WAIT_ESCAPE % 1000000}}; + static const struct itimerval stop = {0}; + + /* FIXME Perhaps it'd be easier to just memorize everything after an escape + * key until the timeout hits and parse it once we got the whole sequence? */ + static enum { + none, + discard, + have_escape, + have_bracket, + need_tilde, + } state = none; + static key r; + static uint64_t tick; + + key k; + + /* If we already saw an escape key and we're at the beginning of the + * function again, it is likely we were interrupted by the timer. + * Check if we've waited long enough for the rest of an escape sequence; + * if yes, we either return the escape key or stop discarding input. */ + if ((state == have_escape || state == discard) + && monotonic_microtime() - tick >= CONFIG_WAIT_ESCAPE) { + switch (state) { + case have_escape: + state = none; + r = KEY_SPECIAL_ESCAPE; + goto stop_timer; + case discard: + state = none; + if (setitimer(ITIMER_REAL, &stop, NULL)) + pdie("setitimer"); + break; + default: + die("unexpected state"); + } + } + +next: + + /* This might be a window size change or a timer interrupt, so we need to + * go up to the main loop. The state machine is untouched by this; we + * can simply continue where we were as soon as we're called again. */ + if ((k = getch()) == KEY_INTERRUPTED) + longjmp(jmp_mainloop, 0); + + switch (state) { + + case none: + if (k != 0x1b) + return k; + + state = have_escape; +start_timer: + tick = monotonic_microtime(); + if (setitimer(ITIMER_REAL, &timeout, NULL)) + pdie("setitimer"); + goto next; + + case discard: + goto next; + + case have_escape: + if (k != '[') { + ungetch(k); + state = none; + r = KEY_SPECIAL_ESCAPE; + goto stop_timer; + } + state = have_bracket; + goto next; + + case have_bracket: + switch (k) { + case 'A': state = none; r = KEY_SPECIAL_UP; goto stop_timer; + case 'B': state = none; r = KEY_SPECIAL_DOWN; goto stop_timer; + case 'C': state = none; r = KEY_SPECIAL_RIGHT; goto stop_timer; + case 'D': state = none; r = KEY_SPECIAL_LEFT; goto stop_timer; + case 'F': state = none; r = KEY_SPECIAL_END; goto stop_timer; + case 'H': state = none; r = KEY_SPECIAL_HOME; goto stop_timer; + case '3': state = need_tilde; r = KEY_SPECIAL_DELETE; goto next; + case '5': state = need_tilde; r = KEY_SPECIAL_PGUP; goto next; + case '6': state = need_tilde; r = KEY_SPECIAL_PGDOWN; goto next; + case '7': state = need_tilde; r = KEY_SPECIAL_HOME; goto next; + case '8': state = need_tilde; r = KEY_SPECIAL_END; goto next; + default: +discard_sequence: + /* We don't know this one. Enter discarding state and + * wait for all the characters to come in. */ + state = discard; + goto start_timer; + } + + case need_tilde: + if (k != '~') + goto discard_sequence; + state = none; +stop_timer: + setitimer(ITIMER_REAL, &stop, NULL); + return r; + } + + __builtin_unreachable(); +} + +static void do_reset_soft(struct input *input) +{ + input->low_nibble = 0; + input->cur_val = 0; +} + +static void toggle_mode_select(struct input *input) +{ + struct view *V = input->view; + struct blob *B = V->blob; + + switch (input->mode) { + case INPUT: + if (!blob_length(B)) + break; + input->mode = SELECT; + input->sel = (input->cur -= (input->cur >= blob_length(B))); + view_dirty_at(V, input->cur); + break; + case SELECT: + input->mode = INPUT; + view_dirty_fromto(input->view, input->sel, input->cur + 1); + view_dirty_fromto(input->view, input->cur, input->sel + 1); + break; + } +} + +static size_t cur_bound(struct input const *input) +{ + size_t bound = blob_length(input->view->blob); + bound += !bound || (input->mode == INPUT && input->input_mode.insert); + assert(bound >= 1); + return bound; +} + +static size_t sat_sub_step(size_t x, size_t y, size_t z, size_t _) +{ + (void) _; assert(z); + return x >= y * z ? x - y * z : x % z; +} + +static size_t sat_add_step(size_t x, size_t y, size_t z, size_t b) +{ + assert(z); assert(x < b); + return x + y * z < b ? x + y * z : b - 1 - (b - 1 - x) % z; +} + +enum cur_move_direction { MOVE_LEFT, MOVE_RIGHT }; +static void cur_move_rel(struct input *input, enum cur_move_direction dir, size_t off, size_t step) +{ + assert(input->cur <= cur_bound(input)); + + struct view *V = input->view; + + do_reset_soft(input); + view_dirty_at(V, input->cur); + switch (dir) { + case MOVE_LEFT: input->cur = sat_sub_step(input->cur, off, step, cur_bound(input)); break; + case MOVE_RIGHT: input->cur = sat_add_step(input->cur, off, step, cur_bound(input)); break; + default: die("unexpected direction"); + } + assert(input->cur < cur_bound(input)); + view_dirty_at(V, input->cur); + view_adjust(V); +} + +static void cur_adjust(struct input *input) +{ + struct view *V = input->view; + + do_reset_soft(input); + if (input->cur >= cur_bound(input)) { + view_dirty_at(V, input->cur); + input->cur = cur_bound(input) - 1; + view_dirty_at(V, input->cur); + view_adjust(V); + } +} + +static void do_reset_hard(struct input *input) +{ + if (input->mode == SELECT) + toggle_mode_select(input); + input->input_mode.insert = input->input_mode.ascii = false; + cur_adjust(input); + view_dirty_at(input->view, input->cur); +} + +static void toggle_mode_insert(struct input *input) +{ + struct view *V = input->view; + + if (!blob_can_move(V->blob)) { + view_error(V, "can't insert: file is memory-mapped."); + return; + } + + if (input->mode != INPUT) + return; + input->input_mode.insert = !input->input_mode.insert; + cur_adjust(input); + view_dirty_at(V, input->cur); +} + +static void toggle_mode_ascii(struct input *input) +{ + struct view *V = input->view; + + if (input->mode != INPUT) + return; + input->input_mode.ascii = !input->input_mode.ascii; + view_dirty_at(V, input->cur); +} + +static void do_yank(struct input *input) +{ + switch (input->mode) { + case INPUT: + input->sel = input->cur; + input->mode = SELECT; + /* fall-through */ + case SELECT: + blob_yank(input->view->blob, min(input->sel, input->cur), absdiff(input->sel, input->cur) + 1); + toggle_mode_select(input); + } +} + +static size_t do_paste(struct input *input) +{ + struct view *V = input->view; + struct blob *B = V->blob; + size_t retval; + + if (input->mode != INPUT) + return 0; + view_adjust(input->view); + do_reset_soft(input); + retval = blob_paste(B, input->cur, input->input_mode.insert ? INSERT : REPLACE); + view_recompute(V, false); + if (input->input_mode.insert) + view_dirty_from(input->view, input->cur); + else + view_dirty_fromto(input->view, input->cur, input->cur + input->view->blob->clipboard.len); + + return retval; +} + +static bool do_delete(struct input *input, bool back) +{ + struct view *V = input->view; + struct blob *B = V->blob; + + if (!blob_can_move(B)) { + view_error(V, "can't delete: file is memory-mapped."); + return false; + } + if (!blob_length(B)) + return false; + + if (back) { + if (!input->cur) + return false; + cur_move_rel(input, MOVE_LEFT, 1, 1); + if (!input->input_mode.insert) + return true; + } + + switch (input->mode) { + case INPUT: + input->mode = SELECT; + cur_adjust(input); + input->sel = input->cur; + /* fall-through */ + case SELECT: + do_reset_soft(input); + do_yank(input); + if (input->cur > input->sel) { + size_t tmp = input->cur; + input->cur = input->sel; + input->sel = tmp; + } + blob_delete(B, input->cur, input->sel - input->cur + 1, true); + view_recompute(V, false); + cur_adjust(input); + view_dirty_from(V, input->cur); + view_adjust(V); + } + return true; +} + +static void do_quit(struct input *input, bool *quit, bool force) +{ + struct view *V = input->view; + if (force || blob_is_saved(V->blob)) + *quit = true; + else + view_error(V, "unsaved changes! use :q! if you are sure."); +} + +static void do_search_cont(struct input *input, ssize_t dir) +{ + struct view *V = input->view; + size_t blen = blob_length(V->blob); + + if (!blen) + return; + + size_t cur = dir > 0 ? min(input->cur, blen-1) : input->cur; + ssize_t pos = blob_search(V->blob, input->search.needle, input->search.len, (cur + blen + dir) % blen, dir); + + if (pos < 0) + return; + + view_dirty_at(V, input->cur); + input->cur = pos; + view_dirty_at(V, input->cur); + view_adjust(V); +} + +static void do_inc_dec(struct input *input, byte diff) +{ + struct view *V = input->view; + struct blob *B = V->blob; + + /* should we do anything for selections? */ + if (input->mode != INPUT) + return; + + if (input->cur >= blob_length(B)) + return; + + byte b = blob_at(B, input->cur) + diff; + blob_replace(input->view->blob, input->cur, &b, 1, true); + view_dirty_at(V, input->cur); +} + +void do_home_end(struct input *input, size_t soft, size_t hard) +{ + assert(soft <= cur_bound(input)); + assert(hard <= cur_bound(input)); + + struct view *V = input->view; + + do_reset_soft(input); + view_dirty_at(V, input->cur); + input->cur = input->cur == soft ? hard : soft; + view_dirty_at(V, input->cur); + if (input->mode == SELECT) + view_dirty_from(V, 0); /* FIXME suboptimal */ + view_adjust(V); +} + +void do_pgup_pgdown(struct input *input, size_t (*f)(size_t, size_t, size_t, size_t)) +{ + struct view *V = input->view; + + do_reset_soft(input); + input->cur = f(input->cur, V->rows, V->cols, cur_bound(input)); + V->start = f(V->start, V->rows, V->cols, cur_bound(input)); + view_dirty_from(V, 0); + view_adjust(V); +} + + +void input_cmd(struct input *input, bool *quit); +void input_search(struct input *input); + +void input_get(struct input *input, bool *quit) +{ + key k; + byte b; + + struct view *V = input->view; + struct blob *B = V->blob; + + k = get_key(); + + if (input->mode == INPUT) { + + if (input->input_mode.ascii && isprint(k)) { + + /* ascii input */ + + if (!blob_length(B)) + input->input_mode.insert = true; + + b = k; + if (input->input_mode.insert) { + blob_insert(B, input->cur, &b, sizeof(b), true); + view_recompute(V, false); + view_dirty_from(V, input->cur); + } + else { + blob_replace(B, input->cur, &b, sizeof(b), true); + view_dirty_at(V, input->cur); + } + + cur_move_rel(input, MOVE_RIGHT, 1, 1); + return; + } + + if ((k >= '0' && k <= '9') || (k >= 'a' && k <= 'f')) { + + /* hex input */ + + if (!blob_length(B)) + input->input_mode.insert = true; + + if (input->input_mode.insert) { + if (!input->low_nibble) + input->cur_val = 0; + input->cur_val |= (k > '9' ? k - 'a' + 10 : k - '0') << 4 * (input->low_nibble = !input->low_nibble); + if (input->low_nibble) { + blob_insert(B, input->cur, &input->cur_val, sizeof(input->cur_val), true); + view_recompute(V, false); + view_dirty_from(V, input->cur); + } + else { + blob_replace(B, input->cur, &input->cur_val, sizeof(input->cur_val), true); + view_dirty_at(V, input->cur); + cur_move_rel(input, MOVE_RIGHT, 1, 1); + return; + } + } + else { + input->cur_val = input->cur < blob_length(B) ? blob_at(B, input->cur) : 0; + input->cur_val = input->cur_val & 0xf << 4 * input->low_nibble; + input->cur_val |= (k > '9' ? k - 'a' + 10 : k - '0') << 4 * (input->low_nibble = !input->low_nibble); + blob_replace(B, input->cur, &input->cur_val, sizeof(input->cur_val), true); + view_dirty_at(V, input->cur); + + if (!input->low_nibble) { + cur_move_rel(input, MOVE_RIGHT, 1, 1); + return; + } + + } + + view_adjust(V); + return; + } + + } + + /* function keys */ + + switch (k) { + + case KEY_SPECIAL_ESCAPE: + do_reset_hard(input); + break; + + case 0x7f: /* backspace */ + do_delete(input, true); + break; + + case 'x': + case KEY_SPECIAL_DELETE: + do_delete(input, false); + break; + + case 'q': + do_quit(input, quit, false); + break; + + case 'v': + toggle_mode_select(input); + break; + + case 'y': + do_yank(input); + break; + + case 's': + if (input->mode == SELECT && input->cur > input->sel) { + size_t tmp = input->sel; + input->sel = input->cur; + input->cur = tmp; + } + if (do_delete(input, false) && !input->input_mode.insert) + toggle_mode_insert(input); + break; + + case 'p': + do_paste(input); + break; + + case 'P': + cur_move_rel(input, MOVE_RIGHT, do_paste(input), 1); + break; + + case 'i': + toggle_mode_insert(input); + break; + + case '\t': + toggle_mode_ascii(input); + break; + + case 'u': + if (input->mode != INPUT) break; + if (!blob_undo(B, &input->cur)) + break; + view_recompute(V, false); + cur_adjust(input); + view_adjust(V); + view_dirty_from(V, 0); /* FIXME suboptimal */ + break; + + case 0x12: /* ctrl + R */ + if (input->mode != INPUT) break; + if (!blob_redo(B, &input->cur)) + break; + view_recompute(V, false); + cur_adjust(input); + view_adjust(V); + view_dirty_from(V, 0); /* FIXME suboptimal */ + break; + + case 0x7: /* ctrl + G */ + { + char buf[256]; + snprintf(buf, sizeof(buf), "\"%s\" %s%s %zd/%zd bytes --%zd%%--", + input->view->blob->filename, + input->view->blob->alloc == BLOB_MMAP ? "[mmap]" : "", + input->view->blob->saved_dist ? "[modified]" : "[saved]", + input->cur, + blob_length(input->view->blob), + ((input->cur+1) * 100) / blob_length(input->view->blob)); + view_message(V, buf, NULL); + } + break; + + case 0xc: /* ctrl + L */ + view_dirty_from(V, 0); + break; + + case ':': + printf("\x1b[%uH", V->rows); /* move to last line */ + view_text(V, false); + printf(":"); + input_cmd(input, quit); + view_dirty_from(V, 0); + view_visual(V); + break; + + case '/': + printf("\x1b[%uH", V->rows); /* move to last line */ + view_text(V, false); + printf("/"); + input_search(input); + view_dirty_from(V, 0); + view_visual(V); + break; + + case 'n': + do_search_cont(input, +1); + break; + + case 'N': + do_search_cont(input, -1); + break; + + case 0x1: /* ctrl + A */ + do_inc_dec(input, 1); + break; + + case 0x18: /* ctrl + X */ + do_inc_dec(input, -1); + break; + + case 'j': + case KEY_SPECIAL_DOWN: + cur_move_rel(input, MOVE_RIGHT, 1, V->cols); + break; + + case 'k': + case KEY_SPECIAL_UP: + cur_move_rel(input, MOVE_LEFT, 1, V->cols); + break; + + case 'l': + case KEY_SPECIAL_RIGHT: + cur_move_rel(input, MOVE_RIGHT, 1, 1); + break; + + case 'h': + case KEY_SPECIAL_LEFT: + cur_move_rel(input, MOVE_LEFT, 1, 1); + break; + + case '^': + cur_move_rel(input, MOVE_LEFT, (input->cur - V->start) % V->cols, 1); + break; + + case '$': + cur_move_rel(input, MOVE_RIGHT, V->cols-1 - (input->cur - V->start) % V->cols, 1); + break; + + case 'g': + case KEY_SPECIAL_HOME: + do_home_end(input, min(V->start, cur_bound(input) - 1), 0); + break; + + case 'G': + case KEY_SPECIAL_END: + do_home_end(input, min(V->start + V->rows * V->cols - 1, cur_bound(input) - 1), cur_bound(input) - 1); + break; + + case 0x15: /* ctrl + U */ + case KEY_SPECIAL_PGUP: + do_pgup_pgdown(input, sat_sub_step); + break; + + case 0x4: /* ctrl + D */ + case KEY_SPECIAL_PGDOWN: + do_pgup_pgdown(input, sat_add_step); + break; + + case '[': + view_set_cols(V, true, -1); + break; + + case ']': + view_set_cols(V, true, +1); + break; + + } +} + +void input_cmd(struct input *input, bool *quit) +{ + char buf[0x100], *p; + unsigned long long n; + + if (!fgets_retry(buf, sizeof(buf), stdin)) + pdie("fgets"); + + if ((p = strchr(buf, '\n'))) + *p = 0; + + if (!(p = strtok(buf, " "))) + return; + else if (!strcmp(p, "w") || !strcmp(p, "wq")) { + switch (blob_save(input->view->blob, strtok(NULL, " "))) { + case BLOB_SAVE_OK: + if (!strcmp(p, "wq")) + do_quit(input, quit, false); + break; + case BLOB_SAVE_FILENAME: + view_error(input->view, "can't save: no filename."); + break; + case BLOB_SAVE_NONEXISTENT: + view_error(input->view, "can't save: nonexistent path."); + break; + case BLOB_SAVE_PERMISSIONS: + view_error(input->view, "can't save: insufficient permissions."); + break; + case BLOB_SAVE_BUSY: + view_error(input->view, "can't save: file is busy."); + break; + default: + die("can't save: unknown error"); + } + } + else if (!strcmp(p, "q") || !strcmp(p, "q!")) { + do_quit(input, quit, !strcmp(p, "q!")); + } + else if (!strcmp(p, "color")) { + if ((p = strtok(NULL, " "))) + input->view->color = *p == '1' || *p == 'y'; + } + else if (!strcmp(p, "columns")) { + if ((p = strtok(NULL, " "))) { + if (!strcmp(p, "auto")) { + view_set_cols(input->view, false, 0); + } + else { + n = strtoull(p, &p, 0); + if (!*p) + view_set_cols(input->view, false, n); + } + } + } + else { + /* try to interpret the input as an offset */ + n = strtoull(p, &p, 0); + if (!*p) { + view_dirty_at(input->view, input->cur); + if (n < cur_bound(input)) + input->cur = n; + view_dirty_at(input->view, input->cur); + view_adjust(input->view); + } + } +} + +static unsigned unhex_digit(char c) +{ + assert(isxdigit(c)); + if (c >= '0' && c <= '9') + return c - '0'; + else if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + die("not a hex digit"); +} + +static size_t unhex(byte **ret, char const *hex) +{ + size_t len = 0; + *ret = malloc_strict(strlen(hex) / 2); + for (char const *p = hex; *p; ) { + while (isspace(*p)) ++p; + if (!(isxdigit(p[0]) && isxdigit(p[1]))) { + free(*ret); + *ret = NULL; + return 0; + } + (*ret)[len] = unhex_digit(*p++) << 4; + (*ret)[len++] |= unhex_digit(*p++); + } + *ret = realloc_strict(*ret, len); /* shrink to what we actually used */ + return len; +} + +/* NB: this accepts some technically invalid inputs */ +static size_t utf8_to_ucs2(byte **ret, char const *str) +{ + size_t len = 0; + *ret = malloc_strict(2 * strlen(str)); + for (uint32_t c, b; (c = *str++); ) { + if (!(c & 0x80)) b = 0; + else if ((c & 0xe0) == 0xc0) c &= 0x1f, b = 1; + else if ((c & 0xf0) == 0xe0) c &= 0x0f, b = 2; + else if ((c & 0xf8) == 0xf0) c &= 0x07, b = 3; + else { +bad: + free(*ret); + *ret = NULL; + return 0; + } + while (b--) { + if ((*str & 0xc0) != 0x80) goto bad; + c <<= 6, c |= (*str++ & 0x3f); + } + if (c >> 16) goto bad; /* not representable */ + (*ret)[len++] = c >> 0; + (*ret)[len++] = c >> 8; + } + *ret = realloc_strict(*ret, len); /* shrink to what we actually used */ + return len; +} + +void input_search(struct input *input) +{ + char buf[0x100], *p, *q; + + if (!fgets_retry(buf, sizeof(buf), stdin)) + pdie("fgets"); + + if ((p = strchr(buf, '\n'))) + *p = 0; + + input->search.len = 0; + free(input->search.needle); + input->search.needle = NULL; + + if (!(p = strtok(buf, " "))) + return; + else if (!strcmp(p, "x") || !strcmp(p, "w")) { + size_t (*fun)(byte **, char const *) = (*p == 'x') ? unhex : utf8_to_ucs2; + if (!(q = strtok(NULL, " "))) { + q = p; + goto str; + } + input->search.len = fun(&input->search.needle, q); + } + else if (!strcmp(p, "s")) { + if (!(q = strtok(NULL, ""))) + q = p; +str: + input->search.len = strlen(q); + input->search.needle = (byte *) strdup(q); + } + else if (!(input->search.len = unhex(&input->search.needle, p))) { + q = p; + goto str; + } + + do_search_cont(input, +1); +} + diff --git a/input.h b/input.h @@ -0,0 +1,35 @@ +#ifndef INPUT_H +#define INPUT_H + +#include "view.h" + +struct input { + struct view *view; + + enum mode { + INPUT, + SELECT, + } mode; + struct { + bool insert: 1; + bool ascii: 1; + } input_mode; + + size_t cur, sel; + bool low_nibble; + byte cur_val; + + struct { + size_t len; + byte *needle; + } search; + + bool quit; +}; + +void input_init(struct input *input, struct view *view); +void input_free(struct input *input); + +void input_get(struct input *input, bool *quit); + +#endif diff --git a/license.txt b/license.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016-2024 Lorenz Panny + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/view.c b/view.c @@ -0,0 +1,360 @@ + +#include "common.h" +#include "blob.h" +#include "view.h" +#include "input.h" +#include "ansi.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +static void fprint(FILE *fp, char const *s) { fprintf(fp, "%s", s); } +static void print(char const *s) { fprint(stdout, s); } + +static size_t view_end(struct view const *view) +{ + return view->start + view->rows * view->cols; +} + +void view_init(struct view *view, struct blob *blob, struct input *input) +{ + memset(view, 0, sizeof(*view)); + view->blob = blob; + view->input = input; + view->pos_digits = 4; /* rather arbitrary */ + view->color = true; + if (tcgetattr(fileno(stdin), &view->term)) + pdie("tcgetattr"); + view->initialized = true; +} + +void view_text(struct view *view, bool leave_alternate) +{ + if (!view->initialized) return; + + if (leave_alternate) + print(leave_alternate_screen); + cursor_column(0); + print(clear_line); + print(show_cursor); + fflush(stdout); + + if (tcsetattr(fileno(stdin), TCSANOW, &view->term)) { + /* can't pdie() because that risks infinite recursion */ + perror("tcsetattr"); + exit(EXIT_FAILURE); + } +} + +void view_visual(struct view *view) +{ + assert(view->initialized); + + struct termios term = view->term; + term.c_lflag &= ~ICANON & ~ECHO; + if (tcsetattr(fileno(stdin), TCSANOW, &term)) + pdie("tcsetattr"); + + print(enter_alternate_screen); + print(hide_cursor); + fflush(stdout); +} + +void view_set_cols(struct view *view, bool relative, int cols) +{ + if (relative) { + if (cols >= 0 || (unsigned) -cols <= view->cols) + cols += view->cols; + else + cols = view->cols; + } + + if (!cols) { + if (view->cols_fixed) { + view->cols_fixed = false; + view_recompute(view, true); + } + return; + } + + view->cols_fixed = true; + + if ((unsigned) cols != view->cols) { + view->cols = cols; + view_dirty_from(view, 0); + } +} + +void view_recompute(struct view *view, bool winch) +{ + struct winsize winsz; + unsigned old_rows = view->rows, old_cols = view->cols; + unsigned digs = (bit_length(max(2, blob_length(view->blob)) - 1) + 3) / 4; + + if (digs > view->pos_digits) { + view->pos_digits = digs; + view_dirty_from(view, 0); + } + else if (!winch) + return; + + if (-1 == ioctl(fileno(stdout), TIOCGWINSZ, &winsz)) + pdie("ioctl"); + + view->rows = winsz.ws_row; + if (!view->cols_fixed) { + view->cols = (winsz.ws_col - (view->pos_digits + strlen(": ") + strlen("||"))) / strlen("xx c"); + + if (view->cols > CONFIG_ROUND_COLS) + view->cols -= view->cols % CONFIG_ROUND_COLS; + } + + if (!view->rows || !view->cols) + die("window too small."); + + if (view->rows == old_rows && view->cols == old_cols) + return; + + /* update dirtiness array */ + if ((view->dirty = realloc_strict(view->dirty, view->rows * sizeof(*view->dirty)))) + memset(view->dirty, 0, view->rows * sizeof(*view->dirty)); + view_dirty_from(view, 0); + + view_adjust(view); + + print(clear_screen); +} + +void view_free(struct view *view) +{ + free(view->dirty); +} + +void view_message(struct view *view, char const *msg, char const *color) +{ + cursor_line(view->rows - 1); + print(clear_line); + if (view->color && color) print(color); + printf("%*c %s", view->pos_digits, ' ', msg); + if (view->color && color) print(color_normal); + fflush(stdout); + view->dirty[view->rows - 1] = 2; /* redraw at the next keypress */ +} + +void view_error(struct view *view, char const *msg) +{ + view_message(view, msg, color_red); +} + +/* FIXME hex and ascii mode look very similar */ +static void render_line(struct view *view, size_t off, size_t last) +{ + byte b; + char digits[0x10], *asciiptr; + size_t asciilen; + FILE *asciifp; + struct input *I = view->input; + + size_t sel_start = min(I->cur, I->sel), sel_end = max(I->cur, I->sel); + char const *last_color = NULL, *next_color; + + if (!(asciifp = open_memstream(&asciiptr, &asciilen))) + pdie("open_memstream"); +#define BOTH(EX) for (FILE *fp; ; ) { fp = stdout; EX; fp = asciifp; EX; break; } + + if (off <= I->cur && I->cur < off + view->cols) { + /* cursor in current line */ + if (view->color) print(color_yellow); + printf("%0*zx%c ", view->pos_digits, I->cur, I->input_mode.insert ? '+' : '>'); + if (view->color) print(color_normal); + } + else { + printf("%0*zx: ", view->pos_digits, off); + } + + if (I->mode == SELECT && off > sel_start && off <= sel_end) + print(underline_on); + + for (size_t j = 0, len = blob_length(view->blob); j < view->cols; ++j) { + + if (off + j < len) { + sprintf(digits, "%02hhx", b = blob_at(view->blob, off + j)); + } + else { + b = 0; + strcpy(digits, " "); + } + + if (I->mode == SELECT && off + j == sel_start) + print(underline_on); + + if (off + j >= last) { + for (size_t p = j; p < view->cols; ++p) + printf(" "); + break; + } + + if (off + j == I->cur) { + next_color = I->cur >= blob_length(view->blob) ? color_red : color_yellow; + BOTH( + if (view->color && next_color != last_color) fprint(fp, next_color); + fprint(fp, inverse_video_on); + ); + + if (!I->input_mode.ascii) { + print(bold_on); + if (I->mode == INPUT && !I->low_nibble) print(underline_on); + putchar(digits[0]); + if (I->mode == INPUT) print(I->low_nibble ? underline_on : underline_off); + putchar(digits[1]); + if (I->mode == INPUT && I->low_nibble) print(underline_off); + print(bold_off); + } + else + printf("%s", digits); + + if (I->mode == INPUT && I->input_mode.ascii) + fprintf(asciifp, "%s%s%c%s%s", bold_on, underline_on, isprint(b) ? b : '.', underline_off, bold_off); + else + fputc(isprint(b) ? b : '.', asciifp); + + BOTH( + fprint(fp, inverse_video_off); + ); + } + else { + next_color = isalnum(b) ? color_cyan + : isprint(b) ? color_blue + : !b ? color_red + : color_normal; + BOTH( + if (view->color && next_color != last_color) + fprint(fp, next_color); + ); + fputc(isprint(b) ? b : '.', asciifp); + printf("%s", digits); + } + last_color = next_color; + + if (I->mode == SELECT && (off + j == sel_end || j == view->cols - 1)) + print(underline_off); + + putchar(' '); + } + if (view->color) print(color_normal); + +#undef BOTH + if (fclose(asciifp)) + pdie("fclose"); + putchar('|'); + printf("%s", asciiptr); + free(asciiptr); + if (view->color) print(color_normal); + putchar('|'); +} + +void view_update(struct view *view) +{ + size_t last = max(blob_length(view->blob), view->input->cur + 1); + + if (view->scroll) { + printf("\x1b[%ld%c", labs(view->scroll), view->scroll > 0 ? 'S' : 'T'); + view->scroll = 0; + } + + for (size_t i = view->start, l = 0; i < view_end(view); i += view->cols, ++l) { + /* dirtiness counter enables displaying messages until keypressed; */ + if (!view->dirty[l] || --view->dirty[l]) + continue; + cursor_line(l); + print(clear_line); + if (i < last) + render_line(view, i, last); + } + + fflush(stdout); +} + +void view_dirty_at(struct view *view, size_t pos) +{ + view_dirty_fromto(view, pos, pos + 1); +} + +void view_dirty_from(struct view *view, size_t from) +{ + view_dirty_fromto(view, from, view_end(view)); +} + +void view_dirty_fromto(struct view *view, size_t from, size_t to) +{ + size_t lfrom, lto; + from = max(view->start, from); + to = min(view_end(view), to); + if (from < to) { + lfrom = (from - view->start) / view->cols; + lto = (to - view->start + view->cols - 1) / view->cols; + for (size_t i = lfrom; i < lto; ++i) + view->dirty[i] = max(view->dirty[i], 1); + } +} + +static size_t satadd(size_t x, size_t y, size_t b) + { assert(b >= 1); return min(b - 1, x + y); } +static size_t satsub(size_t x, size_t y, size_t a) + { return x < y || x - y < a ? a : x - y; } + +void view_adjust(struct view *view) +{ + size_t old_start = view->start; + + assert(view->input->cur <= blob_length(view->blob)); + + if (view->input->cur >= view_end(view)) + view->start = satadd(view->start, + (view->input->cur + 1 - view_end(view) + view->cols - 1) / view->cols * view->cols, + blob_length(view->blob)); + + if (view->input->cur < view->start) + view->start = satsub(view->start, + (view->start - view->input->cur + view->cols - 1) / view->cols * view->cols, + 0); + + assert(view->input->cur >= view->start); + assert(view->input->cur < view_end(view)); + + /* scrolling */ + if (view->start != old_start) { + if (!(((ssize_t) view->start - (ssize_t) old_start) % (ssize_t) view->cols)) { + view->scroll = ((ssize_t) view->start - (ssize_t) old_start) / (ssize_t) view->cols; + if (view->scroll > (signed) view->rows || -view->scroll > (signed) view->rows) { + view->scroll = 0; + view_dirty_from(view, 0); + return; + } + if (view->scroll > 0) { + memmove( + view->dirty, + view->dirty + view->scroll, + (view->rows - view->scroll) * sizeof(*view->dirty) + ); + for (size_t i = view->rows - view->scroll; i < view->rows; ++i) + view->dirty[i] = 1; + } + else { + memmove( + view->dirty + (-view->scroll), + view->dirty, + (view->rows - (-view->scroll)) * sizeof(*view->dirty) + ); + for (size_t i = 0; i < (size_t) -view->scroll; ++i) + view->dirty[i] = 1; + } + } + else + view_dirty_from(view, 0); + } + +} + diff --git a/view.h b/view.h @@ -0,0 +1,48 @@ +#ifndef VIEW_H +#define VIEW_H + +#include "blob.h" + +#include <termios.h> +#include <sys/ioctl.h> + +struct input; +struct view { + bool initialized; + + struct blob *blob; + struct input *input; /* FIXME hack */ + + size_t start; + + uint8_t *dirty; + signed scroll; + + bool cols_fixed; + unsigned rows, cols; + unsigned pos_digits; + bool color; + bool winch; + bool tstp, cont; + + struct termios term; +}; + +void view_init(struct view *view, struct blob *blob, struct input *input); +void view_text(struct view *view, bool leave_alternate); +void view_visual(struct view *view); +void view_recompute(struct view *view, bool winch); +void view_set_cols(struct view *view, bool relative, int cols); +void view_free(struct view *view); + +void view_message(struct view *view, char const *msg, char const *color); +void view_error(struct view *view, char const *msg); + +void view_update(struct view *view); + +void view_dirty_at(struct view *view, size_t pos); +void view_dirty_from(struct view *view, size_t from); +void view_dirty_fromto(struct view *view, size_t from, size_t to); +void view_adjust(struct view *view); + +#endif