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 | + |
A | Makefile | | | 24 | ++++++++++++++++++++++++ |
A | ansi.h | | | 27 | +++++++++++++++++++++++++++ |
A | blob.c | | | 375 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | blob.h | | | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | common.c | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | common.h | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | history.c | | | 97 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | history.h | | | 19 | +++++++++++++++++++ |
A | hyx.c | | | 230 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | input.c | | | 863 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | input.h | | | 35 | +++++++++++++++++++++++++++++++++++ |
A | license.txt | | | 19 | +++++++++++++++++++ |
A | view.c | | | 360 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | view.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