├── Makefile ├── ansi.h ├── blob.c ├── blob.h ├── common.c ├── common.h ├── history.c ├── history.h ├── hyx.c ├── input.c ├── input.h ├── license.txt ├── view.c └── view.h /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: CFLAGS ?= -O2 \ 3 | -pedantic -Wall -Wextra -DNDEBUG \ 4 | -D_FORTIFY_SOURCE=2 -fstack-protector-all 5 | all: CFLAGS += -std=c99 6 | all: hyx 7 | 8 | debug: CFLAGS ?= -O0 -g \ 9 | -fsanitize=undefined \ 10 | -std=c99 -pedantic -Wall -Wextra -Werror \ 11 | -fstack-protector-all 12 | debug: CFLAGS += -std=c99 13 | debug: hyx 14 | 15 | hyx: *.h *.c 16 | $(CC) \ 17 | $(CFLAGS) \ 18 | $(LDFLAGS) \ 19 | hyx.c common.c blob.c history.c view.c input.c \ 20 | -o hyx 21 | 22 | clean: 23 | rm -f hyx 24 | 25 | -------------------------------------------------------------------------------- /ansi.h: -------------------------------------------------------------------------------- 1 | #ifndef ANSI_H 2 | #define ANSI_H 3 | 4 | /* ANSI terminal escape sequences. */ 5 | 6 | static char const bold_on[] = "\x1b[1m", bold_off[] = "\x1b[22m"; /* not a typo */ 7 | static char const underline_on[] = "\x1b[4m", underline_off[] = "\x1b[24m"; 8 | static char const inverse_video_on[] = "\x1b[7m", inverse_video_off[] = "\x1b[27m"; 9 | static char const clear_screen[] = "\x1b[2J"; 10 | static char const clear_line[] = "\x1b[K"; 11 | static inline void cursor_line(unsigned n) { printf("\x1b[%uH", n + 1); } 12 | static inline void cursor_column(unsigned n) { printf("\x1b[%uG", n + 1); } 13 | static char const show_cursor[] = "\x1b[?25h", hide_cursor[] = "\x1b[?25l"; 14 | static char const color_black[] = "\x1b[30m"; 15 | static char const color_red[] = "\x1b[31m"; 16 | static char const color_green[] = "\x1b[32m"; 17 | static char const color_yellow[] = "\x1b[33m"; 18 | static char const color_blue[] = "\x1b[34m"; 19 | static char const color_purple[] = "\x1b[35m"; 20 | static char const color_cyan[] = "\x1b[36m"; 21 | static char const color_white[] = "\x1b[37m"; 22 | static char const color_normal[] = "\x1b[39m"; 23 | 24 | static char const enter_alternate_screen[] = "\x1b[?1049h\x1b[0;0H"; 25 | static char const leave_alternate_screen[] = "\x1b[?1049l"; 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /blob.c: -------------------------------------------------------------------------------- 1 | 2 | #include "common.h" 3 | #include "blob.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | void blob_init(struct blob *blob) 14 | { 15 | memset(blob, 0, sizeof(*blob)); 16 | history_init(&blob->undo); 17 | history_init(&blob->redo); 18 | } 19 | 20 | void blob_replace(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history) 21 | { 22 | assert(pos + len <= blob->len); 23 | 24 | if (save_history) { 25 | history_free(&blob->redo); 26 | history_save(&blob->undo, REPLACE, blob, pos, len); 27 | ++blob->saved_dist; 28 | } 29 | 30 | if (blob->dirty) 31 | for (size_t i = pos / 0x1000; i < (pos + len + 0xfff) / 0x1000; ++i) 32 | blob->dirty[i / 8] |= 1 << i % 8; 33 | 34 | memcpy(blob->data + pos, data, len); 35 | } 36 | 37 | void blob_insert(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history) 38 | { 39 | assert(pos <= blob->len); 40 | assert(blob_can_move(blob)); 41 | assert(len); 42 | assert(!blob->dirty); /* not implemented */ 43 | 44 | if (save_history) { 45 | history_free(&blob->redo); 46 | history_save(&blob->undo, INSERT, blob, pos, len); 47 | ++blob->saved_dist; 48 | } 49 | 50 | blob->data = realloc_strict(blob->data, blob->len += len); 51 | 52 | memmove(blob->data + pos + len, blob->data + pos, blob->len - pos - len); 53 | memcpy(blob->data + pos, data, len); 54 | } 55 | 56 | void blob_delete(struct blob *blob, size_t pos, size_t len, bool save_history) 57 | { 58 | assert(pos + len <= blob->len); 59 | assert(blob_can_move(blob)); 60 | assert(len); 61 | assert(!blob->dirty); /* not implemented */ 62 | 63 | if (save_history) { 64 | history_free(&blob->redo); 65 | history_save(&blob->undo, DELETE, blob, pos, len); 66 | ++blob->saved_dist; 67 | } 68 | 69 | memmove(blob->data + pos, blob->data + pos + len, (blob->len -= len) - pos); 70 | blob->data = realloc_strict(blob->data, blob->len); 71 | } 72 | 73 | void blob_free(struct blob *blob) 74 | { 75 | free(blob->filename); 76 | 77 | switch (blob->alloc) { 78 | case BLOB_MALLOC: 79 | free(blob->data); 80 | break; 81 | case BLOB_MMAP: 82 | free(blob->dirty); 83 | munmap_strict(blob->data, blob->len); 84 | break; 85 | } 86 | 87 | free(blob->clipboard.data); 88 | 89 | history_free(&blob->undo); 90 | history_free(&blob->redo); 91 | } 92 | 93 | bool blob_can_move(struct blob const *blob) 94 | { 95 | return blob->alloc == BLOB_MALLOC; 96 | } 97 | 98 | bool blob_undo(struct blob *blob, size_t *pos) 99 | { 100 | bool r = history_step(&blob->undo, blob, &blob->redo, pos); 101 | blob->saved_dist -= r; 102 | return r; 103 | } 104 | 105 | bool blob_redo(struct blob *blob, size_t *pos) 106 | { 107 | bool r = history_step(&blob->redo, blob, &blob->undo, pos); 108 | blob->saved_dist += r; 109 | return r; 110 | } 111 | 112 | void blob_yank(struct blob *blob, size_t pos, size_t len) 113 | { 114 | free(blob->clipboard.data); 115 | blob->clipboard.data = NULL; 116 | 117 | if (pos < blob_length(blob)) { 118 | blob->clipboard.data = malloc_strict(blob->clipboard.len = len); 119 | blob_read_strict(blob, pos, blob->clipboard.data, blob->clipboard.len); 120 | } 121 | } 122 | 123 | size_t blob_paste(struct blob *blob, size_t pos, enum op_type type) 124 | { 125 | if (!blob->clipboard.data) return 0; 126 | 127 | switch (type) { 128 | case REPLACE: 129 | blob_replace(blob, pos, blob->clipboard.data, min(blob->clipboard.len, blob->len - pos), true); 130 | break; 131 | case INSERT: 132 | blob_insert(blob, pos, blob->clipboard.data, blob->clipboard.len, true); 133 | break; 134 | default: 135 | die("bad operation"); 136 | } 137 | 138 | return blob->clipboard.len; 139 | } 140 | 141 | #define DD(F,B) (dir > 0 ? (F) : (B)) 142 | 143 | /* modified Boyer-Moore-Horspool algorithm. */ 144 | 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]) 145 | { 146 | size_t blen = blob_length(blob); 147 | 148 | assert(start < blen && end >= -1 && end <= (ssize_t) blen); 149 | assert(DD((ssize_t) start <= end, end <= (ssize_t) start)); 150 | 151 | if (len > DD(end-start, start-end)) /* needle longer than range */ 152 | return -1; 153 | 154 | for (ssize_t i = start; DD(i < end, i > end) ; ) { 155 | 156 | if (i + len > blen) { 157 | /* not enough space for pattern: skip */ 158 | i += dir; 159 | continue; 160 | } 161 | assert(i >= 0 && i + len <= blen); 162 | 163 | bool found = true; 164 | for (ssize_t j = DD(len-1, 0); found && j >= 0 && (size_t) j < len; j -= dir) 165 | found = blob_at(blob, i + j) == needle[j]; 166 | if (found) 167 | return i; 168 | 169 | i += dir * (ssize_t) tab[blob_at(blob, i + (dir > 0 ? len - 1 : 0))]; 170 | 171 | } 172 | 173 | /* not found */ 174 | return -1; 175 | } 176 | 177 | ssize_t blob_search(struct blob *blob, byte const *needle, size_t len, size_t start, ssize_t dir) 178 | { 179 | size_t blen = blob_length(blob); 180 | 181 | if (!len || len > blen) 182 | return -1; 183 | 184 | assert(start < blen); 185 | assert(dir == +1 || dir == -1); 186 | 187 | /* could do preprocessing once per needle/dir pair, but patterns are usually short */ 188 | size_t tab[256]; 189 | for (size_t j = 0; j < 256; ++j) 190 | tab[j] = len; 191 | for (size_t j = 0; j < len-1; ++j) 192 | tab[needle[DD(j, len-1-j)]] = len-1-j; 193 | 194 | ssize_t r = blob_search_range(blob, needle, len, start, DD((ssize_t) blen, -1), dir, tab); 195 | if (r < 0) /* wrap around */ 196 | r = blob_search_range(blob, needle, len, DD(0, blen-1), start, dir, tab); 197 | 198 | return r; 199 | } 200 | 201 | #undef DD 202 | 203 | 204 | /* blob_load* functions must be called with a fresh struct from blob_init() */ 205 | 206 | void blob_load(struct blob *blob, char const *filename) 207 | { 208 | struct stat st; 209 | int fd; 210 | void *ptr = NULL; 211 | 212 | if (!filename) 213 | return; /* We are creating a new (still unnamed) file */ 214 | 215 | blob->filename = strdup(filename); 216 | 217 | errno = 0; 218 | if (stat(filename, &st)) { 219 | if (errno != ENOENT) 220 | pdie("stat"); 221 | return; /* We are creating a new file with given name */ 222 | } 223 | 224 | if (0 > (fd = open(filename, O_RDONLY))) 225 | pdie("open"); 226 | 227 | switch (st.st_mode & S_IFMT) { 228 | case S_IFREG: 229 | blob->len = st.st_size; 230 | blob->alloc = blob->len >= CONFIG_LARGE_FILESIZE ? BLOB_MMAP : BLOB_MALLOC; 231 | break; 232 | case S_IFBLK: 233 | blob->len = lseek_strict(fd, 0, SEEK_END); 234 | blob->alloc = BLOB_MMAP; 235 | break; 236 | default: 237 | die("unsupported file type"); 238 | } 239 | 240 | if (blob->len) 241 | ptr = mmap_strict(NULL, 242 | blob->len, 243 | PROT_READ | PROT_WRITE, 244 | MAP_PRIVATE | MAP_NORESERVE, 245 | fd, 246 | 0); 247 | 248 | switch (blob->alloc) { 249 | 250 | case BLOB_MMAP: 251 | assert(ptr); 252 | blob->data = ptr; 253 | if (!(blob->dirty = calloc(((blob->len + 0xfff) / 0x1000 + 7) / 8, sizeof(*blob->dirty)))) 254 | pdie("calloc"); 255 | break; 256 | 257 | case BLOB_MALLOC: 258 | blob->data = malloc_strict(blob->len); 259 | if (ptr) { 260 | memcpy(blob->data, ptr, blob->len); 261 | munmap_strict(ptr, blob->len); 262 | } 263 | break; 264 | 265 | default: 266 | die("bad blob type"); 267 | } 268 | 269 | if (close(fd)) 270 | pdie("close"); 271 | } 272 | 273 | void blob_load_stream(struct blob *blob, FILE *fp) 274 | { 275 | const size_t alloc_size = 0x1000; 276 | size_t n = 0; 277 | 278 | while (true) { 279 | assert(n <= blob->len); 280 | 281 | if (blob->len - n < alloc_size) 282 | blob->data = realloc_strict(blob->data, (blob->len += alloc_size)); 283 | 284 | size_t r = fread(blob->data + n, 1, blob->len - n, fp); 285 | if (!r) { 286 | if (feof(fp)) break; 287 | pdie("could not read data from stream"); 288 | } 289 | n += r; 290 | } 291 | blob->data = realloc(blob->data, (blob->len = n)); 292 | } 293 | 294 | enum blob_save_error blob_save(struct blob *blob, char const *filename) 295 | { 296 | int fd; 297 | struct stat st; 298 | byte const *ptr; 299 | 300 | if (filename) { 301 | free(blob->filename); 302 | blob->filename = strdup(filename); 303 | } 304 | else if (blob->filename) 305 | filename = blob->filename; 306 | else 307 | return BLOB_SAVE_FILENAME; 308 | 309 | errno = 0; 310 | if (0 > (fd = open(filename, 311 | O_WRONLY | O_CREAT, 312 | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))) { 313 | switch (errno) { 314 | case ENOENT: return BLOB_SAVE_NONEXISTENT; 315 | case EACCES: return BLOB_SAVE_PERMISSIONS; 316 | case ETXTBSY: return BLOB_SAVE_BUSY; 317 | default: pdie("open"); 318 | } 319 | } 320 | 321 | if (fstat(fd, &st)) 322 | pdie("fstat"); 323 | 324 | if ((st.st_mode & S_IFMT) == S_IFREG && ftruncate(fd, blob->len)) 325 | pdie("ftruncate"); 326 | 327 | for (size_t i = 0, n; i < blob->len; i += n) { 328 | 329 | if (blob->dirty && !(blob->dirty[i / 0x1000 / 8] & (1 << i / 0x1000 % 8))) { 330 | n = 0x1000 - i % 0x1000; 331 | continue; 332 | } 333 | 334 | ptr = blob_lookup(blob, i, &n); 335 | if (blob->dirty) 336 | n = min(0x1000 - i % 0x1000, n); 337 | 338 | if ((ssize_t) i != lseek(fd, i, SEEK_SET)) 339 | pdie("lseek"); 340 | 341 | if (0 >= (n = write(fd, ptr, n))) 342 | pdie("write"); 343 | } 344 | 345 | if (close(fd)) 346 | pdie("close"); 347 | 348 | blob->saved_dist = 0; 349 | 350 | return BLOB_SAVE_OK; 351 | } 352 | 353 | bool blob_is_saved(struct blob const *blob) 354 | { 355 | return !blob->saved_dist; 356 | } 357 | 358 | byte const *blob_lookup(struct blob const *blob, size_t pos, size_t *len) 359 | { 360 | assert(pos < blob->len); 361 | 362 | if (len) 363 | *len = blob->len - pos; 364 | return blob->data + pos; 365 | } 366 | 367 | void blob_read_strict(struct blob *blob, size_t pos, byte *buf, size_t len) 368 | { 369 | byte const *ptr; 370 | for (size_t i = 0, n; i < len; i += n) { 371 | ptr = blob_lookup(blob, pos, &n); 372 | memcpy(buf + i, ptr, (n = min(len - i, n))); 373 | } 374 | } 375 | 376 | -------------------------------------------------------------------------------- /blob.h: -------------------------------------------------------------------------------- 1 | #ifndef BLOB_H 2 | #define BLOB_H 3 | 4 | #include "common.h" 5 | #include "history.h" 6 | 7 | enum blob_alloc { 8 | BLOB_MALLOC = 0, 9 | BLOB_MMAP, 10 | }; 11 | 12 | struct blob { 13 | enum blob_alloc alloc; 14 | 15 | size_t len; 16 | byte *data; 17 | 18 | char *filename; 19 | 20 | uint8_t *dirty; 21 | 22 | struct diff *undo, *redo; 23 | ssize_t saved_dist; 24 | 25 | struct { 26 | size_t len; 27 | byte *data; 28 | } clipboard; 29 | }; 30 | 31 | void blob_init(struct blob *blob); 32 | void blob_replace(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history); 33 | void blob_insert(struct blob *blob, size_t pos, byte const *data, size_t len, bool save_history); 34 | void blob_delete(struct blob *blob, size_t pos, size_t len, bool save_history); 35 | void blob_free(struct blob *blob); 36 | 37 | bool blob_can_move(struct blob const *blob); 38 | 39 | bool blob_undo(struct blob *blob, size_t *pos); 40 | bool blob_redo(struct blob *blob, size_t *pos); 41 | 42 | void blob_yank(struct blob *blob, size_t pos, size_t len); 43 | size_t blob_paste(struct blob *blob, size_t pos, enum op_type type); 44 | 45 | ssize_t blob_search(struct blob *blob, byte const *needle, size_t len, size_t start, ssize_t dir); 46 | 47 | void blob_load(struct blob *blob, char const *filename); 48 | void blob_load_stream(struct blob *blob, FILE *fp); 49 | enum blob_save_error { 50 | BLOB_SAVE_OK = 0, 51 | BLOB_SAVE_FILENAME, 52 | BLOB_SAVE_NONEXISTENT, 53 | BLOB_SAVE_PERMISSIONS, 54 | BLOB_SAVE_BUSY, 55 | } blob_save(struct blob *blob, char const *filename); 56 | bool blob_is_saved(struct blob const *blob); 57 | 58 | static inline size_t blob_length(struct blob const *blob) 59 | { return blob->len; } 60 | byte const *blob_lookup(struct blob const *blob, size_t pos, size_t *len); 61 | static inline byte blob_at(struct blob const *blob, size_t pos) 62 | { return *blob_lookup(blob, pos, NULL); } 63 | void blob_read_strict(struct blob *blob, size_t pos, byte *buf, size_t len); 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /common.c: -------------------------------------------------------------------------------- 1 | 2 | #include "common.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | unsigned long bit_length(unsigned long n) 11 | { 12 | unsigned long r = 0; 13 | do ++r; while (n >>= 1); 14 | return r; 15 | } 16 | 17 | 18 | void *malloc_strict(size_t len) 19 | { 20 | void *ptr; 21 | errno = 0; 22 | if (!(ptr = malloc(len)) && errno) 23 | pdie("malloc"); 24 | return ptr; 25 | } 26 | 27 | void *realloc_strict(void *ptr, size_t len) 28 | { 29 | errno = 0; 30 | if (!(ptr = realloc(ptr, len)) && errno) 31 | pdie("realloc"); 32 | return ptr; 33 | } 34 | 35 | void *mmap_strict(void *addr, size_t len, int prot, int flags, int fildes, off_t off) 36 | { 37 | void *ptr; 38 | if (MAP_FAILED == (ptr = mmap(addr, len, prot, flags, fildes, off))) 39 | pdie("mmap"); 40 | return ptr; 41 | } 42 | 43 | void munmap_strict(void *addr, size_t len) 44 | { 45 | if (munmap(addr, len)) 46 | pdie("munmap"); 47 | } 48 | 49 | off_t lseek_strict(int fildes, off_t offset, int whence) 50 | { 51 | off_t ret; 52 | if (0 >= (ret = lseek(fildes, offset, whence))) 53 | pdie("lseek"); 54 | return ret; 55 | } 56 | 57 | char *fgets_retry(char *s, int size, FILE *stream) 58 | { 59 | char *ret; 60 | retry: 61 | errno = 0; 62 | if (!(ret = fgets(s, size, stream))) { 63 | if (errno == EINTR) 64 | goto retry; 65 | pdie("fgets"); 66 | } 67 | return ret; 68 | } 69 | 70 | uint64_t monotonic_microtime() 71 | { 72 | struct timespec t; 73 | if (clock_gettime(CLOCK_MONOTONIC, &t)) 74 | pdie("clock_gettime"); 75 | return (uint64_t) t.tv_sec * 1000000 + t.tv_nsec / 1000; 76 | } 77 | 78 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #define _GNU_SOURCE 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | /* round columns to a multiple of this */ 15 | #define CONFIG_ROUND_COLS 0x8 16 | 17 | /* mmap files larger than this */ 18 | #define CONFIG_LARGE_FILESIZE (256 * (1 << 20)) /* 256 megabytes */ 19 | 20 | /* microseconds to wait for the rest of what could be an escape sequence */ 21 | #define CONFIG_WAIT_ESCAPE (10000) /* 10 milliseconds */ 22 | 23 | 24 | typedef uint8_t byte; 25 | 26 | void die(char const *s) __attribute__((noreturn)); /* hyx.c */ 27 | void pdie(char const *s) __attribute__((noreturn)); /* hyx.c */ 28 | 29 | static inline size_t min(size_t x, size_t y) 30 | { return x < y ? x : y; } 31 | static inline size_t max(size_t x, size_t y) 32 | { return x > y ? x : y; } 33 | static inline size_t absdiff(size_t x, size_t y) 34 | { return x > y ? x - y : y - x; } 35 | 36 | unsigned long bit_length(unsigned long n); 37 | 38 | void *malloc_strict(size_t len); 39 | void *realloc_strict(void *ptr, size_t len); 40 | 41 | void *mmap_strict(void *addr, size_t len, int prot, int flags, int fildes, off_t off); 42 | void munmap_strict(void *addr, size_t len); 43 | 44 | off_t lseek_strict(int fildes, off_t offset, int whence); 45 | 46 | char *fgets_retry(char *s, int size, FILE *stream); 47 | 48 | uint64_t monotonic_microtime(); 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /history.c: -------------------------------------------------------------------------------- 1 | 2 | #include "common.h" 3 | #include "history.h" 4 | #include "blob.h" 5 | 6 | #include 7 | 8 | struct diff { 9 | enum op_type type; 10 | size_t pos; 11 | byte *data; 12 | size_t len; 13 | struct diff *next; 14 | }; 15 | 16 | static void diff_apply(struct blob *blob, struct diff *diff) 17 | { 18 | switch (diff->type) { 19 | case REPLACE: 20 | blob_replace(blob, diff->pos, diff->data, diff->len, false); 21 | break; 22 | case INSERT: 23 | blob_insert(blob, diff->pos, diff->data, diff->len, false); 24 | break; 25 | case DELETE: 26 | blob_delete(blob, diff->pos, diff->len, false); 27 | break; 28 | default: 29 | die("unknown operation"); 30 | } 31 | } 32 | 33 | void history_init(struct diff **history) 34 | { 35 | *history = NULL; 36 | } 37 | 38 | void history_free(struct diff **history) 39 | { 40 | struct diff *tmp, *cur = *history; 41 | while (cur) { 42 | tmp = cur; 43 | cur = cur->next; 44 | free(tmp->data); 45 | free(tmp); 46 | } 47 | *history = NULL; 48 | } 49 | 50 | /* pushes a diff that _undoes_ the passed operation */ 51 | void history_save(struct diff **history, enum op_type type, struct blob *blob, size_t pos, size_t len) 52 | { 53 | struct diff *diff = malloc_strict(sizeof(*diff)); 54 | diff->type = type; 55 | diff->pos = pos; 56 | diff->len = len; 57 | diff->next = *history; 58 | 59 | switch (type) { 60 | case DELETE: 61 | diff->type = INSERT; 62 | /* fall-through */ 63 | case REPLACE: 64 | blob_read_strict(blob, pos, diff->data = malloc_strict(len), len); 65 | break; 66 | case INSERT: 67 | diff->type = DELETE; 68 | diff->data = NULL; 69 | break; 70 | default: 71 | die("unknown operation"); 72 | } 73 | 74 | *history = diff; 75 | } 76 | 77 | bool history_step(struct diff **from, struct blob *blob, struct diff **to, size_t *pos) 78 | { 79 | struct diff *diff = *from; 80 | 81 | if (!diff) 82 | return false; 83 | 84 | if (pos) 85 | *pos = diff->pos; 86 | 87 | if (to) 88 | history_save(to, diff->type, blob, diff->pos, diff->len); 89 | 90 | *from = diff->next; 91 | diff_apply(blob, diff); 92 | free(diff->data); 93 | free(diff); 94 | 95 | return true; 96 | } 97 | 98 | -------------------------------------------------------------------------------- /history.h: -------------------------------------------------------------------------------- 1 | #ifndef HISTORY_H 2 | #define HISTORY_H 3 | 4 | struct blob; 5 | 6 | enum op_type { 7 | REPLACE, 8 | INSERT, 9 | DELETE, 10 | }; 11 | 12 | struct diff; 13 | 14 | void history_init(struct diff **history); 15 | void history_free(struct diff **history); 16 | void history_save(struct diff **history, enum op_type type, struct blob *blob, size_t pos, size_t len); 17 | bool history_step(struct diff **history, struct blob *blob, struct diff **target, size_t *pos); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /hyx.c: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright (c) 2016-2024 Lorenz Panny 4 | * 5 | * This is hyx version 2024.02.29. 6 | * Check for newer versions at https://yx7.cc/code. 7 | * Please report bugs to lorenz@yx7.cc. 8 | * 9 | * Contributors: 10 | * 2018, anonymous Faster search algorithm. 11 | * 2020, Leah Neukirchen Suspend on ^Z. File information on ^G. 12 | * 2022, Jeffrey H. Johnson Makefile tweaks for MacOS. 13 | * 2022, Mario Haustein Makefile tweaks for Gentoo. 14 | * 2023, Josef Schönberger Use alternate screen. Home/End keys. 15 | * 16 | * This program is released under the MIT license; see license.txt. 17 | * 18 | */ 19 | 20 | #include "common.h" 21 | #include "blob.h" 22 | #include "view.h" 23 | #include "input.h" 24 | #include "ansi.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | 33 | struct blob blob; 34 | struct view view; 35 | struct input input; 36 | 37 | bool quit; 38 | 39 | jmp_buf jmp_mainloop; 40 | 41 | 42 | void die(char const *s) 43 | { 44 | view_text(&view, true); 45 | fprintf(stderr, "%s\n", s); 46 | exit(EXIT_FAILURE); 47 | } 48 | 49 | void pdie(char const *s) 50 | { 51 | view_text(&view, true); 52 | perror(s); 53 | exit(EXIT_FAILURE); 54 | } 55 | 56 | static void sighdlr(int num) 57 | { 58 | switch (num) { 59 | case SIGWINCH: 60 | view.winch = true; 61 | break; 62 | case SIGTSTP: 63 | view.tstp = true; 64 | break; 65 | case SIGCONT: 66 | view.cont = true; 67 | break; 68 | case SIGALRM: 69 | /* This is used in parsing escape sequences, 70 | * but we don't need to do anything here. */ 71 | break; 72 | case SIGINT: 73 | /* ignore */ 74 | break; 75 | default: 76 | die("unrecognized signal"); 77 | } 78 | } 79 | 80 | __attribute__((noreturn)) void version() 81 | { 82 | printf("This is hyx version 2024.02.29.\n"); 83 | exit(EXIT_SUCCESS); 84 | } 85 | 86 | __attribute__((noreturn)) void help(int st) 87 | { 88 | bool tty = isatty(fileno(stdout)); 89 | 90 | printf("\n"); 91 | printf(" %shyx: a minimalistic hex editor%s\n", 92 | tty ? color_green : "", tty ? color_normal : ""); 93 | printf(" ------------------------------\n\n"); 94 | 95 | printf(" %sinvocation:%s hyx [filename]\n", 96 | tty ? color_yellow : "", tty ? color_normal : ""); 97 | 98 | printf(" %sinvocation:%s [command] | hyx\n\n", 99 | tty ? color_yellow : "", tty ? color_normal : ""); 100 | 101 | printf(" %skeys:%s\n\n", 102 | tty ? color_yellow : "", tty ? color_normal : ""); 103 | printf("q quit\n"); 104 | printf("\n"); 105 | printf("h, j, k, l move cursor\n"); 106 | printf("(hex digits) edit bytes (in hex mode)\n"); 107 | printf("(printable) edit bytes (in ascii mode)\n"); 108 | printf("i switch between replace and insert modes\n"); 109 | printf("tab switch between hex and ascii input\n"); 110 | printf("\n"); 111 | printf("u undo\n"); 112 | printf("ctrl+r redo\n"); 113 | printf("\n"); 114 | printf("v start a selection\n"); 115 | printf("escape abort a selection\n"); 116 | printf("x delete current byte or selection\n"); 117 | printf("s substitute current byte or selection\n"); 118 | printf("y copy current byte or selection to clipboard\n"); 119 | printf("p paste\n"); 120 | printf("P paste and move cursor\n"); 121 | printf("\n"); 122 | printf("], [ increase/decrease number of columns\n"); 123 | printf("\n"); 124 | printf("ctrl+u, ctrl+d scroll up/down one page\n"); 125 | printf("g, G jump to start/end of screen or file\n"); 126 | printf("^, $ jump to start/end of current line\n"); 127 | printf("\n"); 128 | printf(": enter command (see below)\n"); 129 | printf("\n"); 130 | printf("/x (hex string) search for hexadecimal bytes\n"); 131 | printf("/s (characters) search for unicode string (utf8)\n"); 132 | printf("/w (characters) search for unicode string (ucs2)\n"); 133 | printf("n, N jump to next/previous match\n"); 134 | printf("\n"); 135 | printf("ctrl+a, ctrl+x increment/decrement current byte\n"); 136 | printf("\n"); 137 | printf("ctrl+g show file name and current position\n"); 138 | printf("ctrl+z suspend editor; use \"fg\" to continue\n"); 139 | printf("\n"); 140 | 141 | printf(" %scommands:%s\n\n", 142 | tty ? color_yellow : "", tty ? color_normal : ""); 143 | printf("$offset jump to offset (supports hex/dec/oct)\n"); 144 | printf("q quit\n"); 145 | printf("w [$filename] save\n"); 146 | printf("wq [$filename] save and quit\n"); 147 | printf("color y/n toggle colors\n"); 148 | 149 | printf("\n"); 150 | 151 | exit(st); 152 | } 153 | 154 | int main(int argc, char **argv) 155 | { 156 | struct sigaction sigact; 157 | 158 | char *filename = NULL; 159 | 160 | for (size_t i = 1; i < (size_t) argc; ++i) { 161 | if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) 162 | help(0); 163 | else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--version")) 164 | version(); 165 | else if (!filename) 166 | filename = argv[i]; 167 | else 168 | help(EXIT_FAILURE); 169 | } 170 | 171 | blob_init(&blob); 172 | if (!isatty(fileno(stdin))) { 173 | if (filename) help(EXIT_FAILURE); 174 | blob_load_stream(&blob, stdin); 175 | if (!freopen("/dev/tty", "r", stdin)) 176 | pdie("could not reopen controlling TTY"); 177 | } 178 | else { 179 | blob_load(&blob, filename); 180 | } 181 | 182 | view_init(&view, &blob, &input); 183 | input_init(&input, &view); 184 | 185 | /* set up signal handler */ 186 | memset(&sigact, 0, sizeof(sigact)); 187 | sigact.sa_handler = sighdlr; 188 | sigaction(SIGWINCH, &sigact, NULL); 189 | sigaction(SIGTSTP, &sigact, NULL); 190 | sigaction(SIGCONT, &sigact, NULL); 191 | sigaction(SIGALRM, &sigact, NULL); 192 | sigaction(SIGINT, &sigact, NULL); 193 | 194 | view_recompute(&view, true); 195 | view_visual(&view); 196 | 197 | do { 198 | /* This is used to redraw immediately when the window size changes. */ 199 | setjmp(jmp_mainloop); 200 | 201 | if (view.winch) { 202 | view_recompute(&view, true); 203 | view.winch = false; 204 | } 205 | if (view.tstp) { 206 | view_text(&view, true); 207 | view.tstp = false; 208 | raise(SIGSTOP); 209 | /* should continue with view.cont == true */ 210 | } 211 | if (view.cont) { 212 | view_recompute(&view, true); 213 | view_dirty_from(&view, 0); 214 | view_visual(&view); 215 | view.cont = false; 216 | } 217 | assert(input.cur >= view.start && input.cur < view.start + view.rows * view.cols); 218 | view_update(&view); 219 | 220 | input_get(&input, &quit); 221 | 222 | } while (!quit); 223 | 224 | view_text(&view, true); 225 | 226 | input_free(&input); 227 | view_free(&view); 228 | blob_free(&blob); 229 | } 230 | 231 | -------------------------------------------------------------------------------- /input.c: -------------------------------------------------------------------------------- 1 | 2 | #include "common.h" 3 | #include "blob.h" 4 | #include "view.h" 5 | #include "input.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | extern jmp_buf jmp_mainloop; /* hyx.c */ 18 | 19 | void input_init(struct input *input, struct view *view) 20 | { 21 | memset(input, 0, sizeof(*input)); 22 | input->view = view; 23 | } 24 | 25 | void input_free(struct input *input) 26 | { 27 | free(input->search.needle); 28 | } 29 | 30 | /* 31 | * The C standard forbids assigning arbitrary integers to an enum, 32 | * hence we do it the other way round: assign enumerated constants 33 | * to a general integer type. 34 | */ 35 | typedef uint16_t key; 36 | enum { 37 | KEY_INTERRUPTED = 0x1000, 38 | KEY_SPECIAL_ESCAPE, 39 | KEY_SPECIAL_DELETE, 40 | KEY_SPECIAL_UP, KEY_SPECIAL_DOWN, KEY_SPECIAL_RIGHT, KEY_SPECIAL_LEFT, 41 | KEY_SPECIAL_PGUP, KEY_SPECIAL_PGDOWN, 42 | KEY_SPECIAL_HOME, KEY_SPECIAL_END, 43 | }; 44 | 45 | static key getch() 46 | { 47 | int c; 48 | errno = 0; 49 | if (EOF == (c = getc(stdin))) { 50 | if (errno == EINTR) 51 | return KEY_INTERRUPTED; 52 | pdie("getc"); 53 | } 54 | return c; 55 | } 56 | 57 | static void ungetch(int c) 58 | { 59 | if (c != ungetc(c, stdin)) 60 | pdie("ungetc"); 61 | } 62 | 63 | static key get_key() 64 | { 65 | static const struct itimerval timeout = {{0}, {CONFIG_WAIT_ESCAPE / 1000000, CONFIG_WAIT_ESCAPE % 1000000}}; 66 | static const struct itimerval stop = {0}; 67 | 68 | /* FIXME Perhaps it'd be easier to just memorize everything after an escape 69 | * key until the timeout hits and parse it once we got the whole sequence? */ 70 | static enum { 71 | none, 72 | discard, 73 | have_escape, 74 | have_bracket, 75 | need_tilde, 76 | } state = none; 77 | static key r; 78 | static uint64_t tick; 79 | 80 | key k; 81 | 82 | /* If we already saw an escape key and we're at the beginning of the 83 | * function again, it is likely we were interrupted by the timer. 84 | * Check if we've waited long enough for the rest of an escape sequence; 85 | * if yes, we either return the escape key or stop discarding input. */ 86 | if ((state == have_escape || state == discard) 87 | && monotonic_microtime() - tick >= CONFIG_WAIT_ESCAPE) { 88 | switch (state) { 89 | case have_escape: 90 | state = none; 91 | r = KEY_SPECIAL_ESCAPE; 92 | goto stop_timer; 93 | case discard: 94 | state = none; 95 | if (setitimer(ITIMER_REAL, &stop, NULL)) 96 | pdie("setitimer"); 97 | break; 98 | default: 99 | die("unexpected state"); 100 | } 101 | } 102 | 103 | next: 104 | 105 | /* This might be a window size change or a timer interrupt, so we need to 106 | * go up to the main loop. The state machine is untouched by this; we 107 | * can simply continue where we were as soon as we're called again. */ 108 | if ((k = getch()) == KEY_INTERRUPTED) 109 | longjmp(jmp_mainloop, 0); 110 | 111 | switch (state) { 112 | 113 | case none: 114 | if (k != 0x1b) 115 | return k; 116 | 117 | state = have_escape; 118 | start_timer: 119 | tick = monotonic_microtime(); 120 | if (setitimer(ITIMER_REAL, &timeout, NULL)) 121 | pdie("setitimer"); 122 | goto next; 123 | 124 | case discard: 125 | goto next; 126 | 127 | case have_escape: 128 | if (k != '[') { 129 | ungetch(k); 130 | state = none; 131 | r = KEY_SPECIAL_ESCAPE; 132 | goto stop_timer; 133 | } 134 | state = have_bracket; 135 | goto next; 136 | 137 | case have_bracket: 138 | switch (k) { 139 | case 'A': state = none; r = KEY_SPECIAL_UP; goto stop_timer; 140 | case 'B': state = none; r = KEY_SPECIAL_DOWN; goto stop_timer; 141 | case 'C': state = none; r = KEY_SPECIAL_RIGHT; goto stop_timer; 142 | case 'D': state = none; r = KEY_SPECIAL_LEFT; goto stop_timer; 143 | case 'F': state = none; r = KEY_SPECIAL_END; goto stop_timer; 144 | case 'H': state = none; r = KEY_SPECIAL_HOME; goto stop_timer; 145 | case '3': state = need_tilde; r = KEY_SPECIAL_DELETE; goto next; 146 | case '5': state = need_tilde; r = KEY_SPECIAL_PGUP; goto next; 147 | case '6': state = need_tilde; r = KEY_SPECIAL_PGDOWN; goto next; 148 | case '7': state = need_tilde; r = KEY_SPECIAL_HOME; goto next; 149 | case '8': state = need_tilde; r = KEY_SPECIAL_END; goto next; 150 | default: 151 | discard_sequence: 152 | /* We don't know this one. Enter discarding state and 153 | * wait for all the characters to come in. */ 154 | state = discard; 155 | goto start_timer; 156 | } 157 | 158 | case need_tilde: 159 | if (k != '~') 160 | goto discard_sequence; 161 | state = none; 162 | stop_timer: 163 | setitimer(ITIMER_REAL, &stop, NULL); 164 | return r; 165 | } 166 | 167 | __builtin_unreachable(); 168 | } 169 | 170 | static void do_reset_soft(struct input *input) 171 | { 172 | input->low_nibble = 0; 173 | input->cur_val = 0; 174 | } 175 | 176 | static void toggle_mode_select(struct input *input) 177 | { 178 | struct view *V = input->view; 179 | struct blob *B = V->blob; 180 | 181 | switch (input->mode) { 182 | case INPUT: 183 | if (!blob_length(B)) 184 | break; 185 | input->mode = SELECT; 186 | input->sel = (input->cur -= (input->cur >= blob_length(B))); 187 | view_dirty_at(V, input->cur); 188 | break; 189 | case SELECT: 190 | input->mode = INPUT; 191 | view_dirty_fromto(input->view, input->sel, input->cur + 1); 192 | view_dirty_fromto(input->view, input->cur, input->sel + 1); 193 | break; 194 | } 195 | } 196 | 197 | static size_t cur_bound(struct input const *input) 198 | { 199 | size_t bound = blob_length(input->view->blob); 200 | bound += !bound || (input->mode == INPUT && input->input_mode.insert); 201 | assert(bound >= 1); 202 | return bound; 203 | } 204 | 205 | static size_t sat_sub_step(size_t x, size_t y, size_t z, size_t _) 206 | { 207 | (void) _; assert(z); 208 | return x >= y * z ? x - y * z : x % z; 209 | } 210 | 211 | static size_t sat_add_step(size_t x, size_t y, size_t z, size_t b) 212 | { 213 | assert(z); assert(x < b); 214 | return x + y * z < b ? x + y * z : b - 1 - (b - 1 - x) % z; 215 | } 216 | 217 | enum cur_move_direction { MOVE_LEFT, MOVE_RIGHT }; 218 | static void cur_move_rel(struct input *input, enum cur_move_direction dir, size_t off, size_t step) 219 | { 220 | assert(input->cur <= cur_bound(input)); 221 | 222 | struct view *V = input->view; 223 | 224 | do_reset_soft(input); 225 | view_dirty_at(V, input->cur); 226 | switch (dir) { 227 | case MOVE_LEFT: input->cur = sat_sub_step(input->cur, off, step, cur_bound(input)); break; 228 | case MOVE_RIGHT: input->cur = sat_add_step(input->cur, off, step, cur_bound(input)); break; 229 | default: die("unexpected direction"); 230 | } 231 | assert(input->cur < cur_bound(input)); 232 | view_dirty_at(V, input->cur); 233 | view_adjust(V); 234 | } 235 | 236 | static void cur_adjust(struct input *input) 237 | { 238 | struct view *V = input->view; 239 | 240 | do_reset_soft(input); 241 | if (input->cur >= cur_bound(input)) { 242 | view_dirty_at(V, input->cur); 243 | input->cur = cur_bound(input) - 1; 244 | view_dirty_at(V, input->cur); 245 | view_adjust(V); 246 | } 247 | } 248 | 249 | static void do_reset_hard(struct input *input) 250 | { 251 | if (input->mode == SELECT) 252 | toggle_mode_select(input); 253 | input->input_mode.insert = input->input_mode.ascii = false; 254 | cur_adjust(input); 255 | view_dirty_at(input->view, input->cur); 256 | } 257 | 258 | static void toggle_mode_insert(struct input *input) 259 | { 260 | struct view *V = input->view; 261 | 262 | if (!blob_can_move(V->blob)) { 263 | view_error(V, "can't insert: file is memory-mapped."); 264 | return; 265 | } 266 | 267 | if (input->mode != INPUT) 268 | return; 269 | input->input_mode.insert = !input->input_mode.insert; 270 | cur_adjust(input); 271 | view_dirty_at(V, input->cur); 272 | } 273 | 274 | static void toggle_mode_ascii(struct input *input) 275 | { 276 | struct view *V = input->view; 277 | 278 | if (input->mode != INPUT) 279 | return; 280 | input->input_mode.ascii = !input->input_mode.ascii; 281 | view_dirty_at(V, input->cur); 282 | } 283 | 284 | static void do_yank(struct input *input) 285 | { 286 | switch (input->mode) { 287 | case INPUT: 288 | input->sel = input->cur; 289 | input->mode = SELECT; 290 | /* fall-through */ 291 | case SELECT: 292 | blob_yank(input->view->blob, min(input->sel, input->cur), absdiff(input->sel, input->cur) + 1); 293 | toggle_mode_select(input); 294 | } 295 | } 296 | 297 | static size_t do_paste(struct input *input) 298 | { 299 | struct view *V = input->view; 300 | struct blob *B = V->blob; 301 | size_t retval; 302 | 303 | if (input->mode != INPUT) 304 | return 0; 305 | view_adjust(input->view); 306 | do_reset_soft(input); 307 | retval = blob_paste(B, input->cur, input->input_mode.insert ? INSERT : REPLACE); 308 | view_recompute(V, false); 309 | if (input->input_mode.insert) 310 | view_dirty_from(input->view, input->cur); 311 | else 312 | view_dirty_fromto(input->view, input->cur, input->cur + input->view->blob->clipboard.len); 313 | 314 | return retval; 315 | } 316 | 317 | static bool do_delete(struct input *input, bool back) 318 | { 319 | struct view *V = input->view; 320 | struct blob *B = V->blob; 321 | 322 | if (!blob_can_move(B)) { 323 | view_error(V, "can't delete: file is memory-mapped."); 324 | return false; 325 | } 326 | if (!blob_length(B)) 327 | return false; 328 | 329 | if (back) { 330 | if (!input->cur) 331 | return false; 332 | cur_move_rel(input, MOVE_LEFT, 1, 1); 333 | if (!input->input_mode.insert) 334 | return true; 335 | } 336 | 337 | switch (input->mode) { 338 | case INPUT: 339 | input->mode = SELECT; 340 | cur_adjust(input); 341 | input->sel = input->cur; 342 | /* fall-through */ 343 | case SELECT: 344 | do_reset_soft(input); 345 | do_yank(input); 346 | if (input->cur > input->sel) { 347 | size_t tmp = input->cur; 348 | input->cur = input->sel; 349 | input->sel = tmp; 350 | } 351 | blob_delete(B, input->cur, input->sel - input->cur + 1, true); 352 | view_recompute(V, false); 353 | cur_adjust(input); 354 | view_dirty_from(V, input->cur); 355 | view_adjust(V); 356 | } 357 | return true; 358 | } 359 | 360 | static void do_quit(struct input *input, bool *quit, bool force) 361 | { 362 | struct view *V = input->view; 363 | if (force || blob_is_saved(V->blob)) 364 | *quit = true; 365 | else 366 | view_error(V, "unsaved changes! use :q! if you are sure."); 367 | } 368 | 369 | static void do_search_cont(struct input *input, ssize_t dir) 370 | { 371 | struct view *V = input->view; 372 | size_t blen = blob_length(V->blob); 373 | 374 | if (!blen) 375 | return; 376 | 377 | size_t cur = dir > 0 ? min(input->cur, blen-1) : input->cur; 378 | ssize_t pos = blob_search(V->blob, input->search.needle, input->search.len, (cur + blen + dir) % blen, dir); 379 | 380 | if (pos < 0) 381 | return; 382 | 383 | view_dirty_at(V, input->cur); 384 | input->cur = pos; 385 | view_dirty_at(V, input->cur); 386 | view_adjust(V); 387 | } 388 | 389 | static void do_inc_dec(struct input *input, byte diff) 390 | { 391 | struct view *V = input->view; 392 | struct blob *B = V->blob; 393 | 394 | /* should we do anything for selections? */ 395 | if (input->mode != INPUT) 396 | return; 397 | 398 | if (input->cur >= blob_length(B)) 399 | return; 400 | 401 | byte b = blob_at(B, input->cur) + diff; 402 | blob_replace(input->view->blob, input->cur, &b, 1, true); 403 | view_dirty_at(V, input->cur); 404 | } 405 | 406 | void do_home_end(struct input *input, size_t soft, size_t hard) 407 | { 408 | assert(soft <= cur_bound(input)); 409 | assert(hard <= cur_bound(input)); 410 | 411 | struct view *V = input->view; 412 | 413 | do_reset_soft(input); 414 | view_dirty_at(V, input->cur); 415 | input->cur = input->cur == soft ? hard : soft; 416 | view_dirty_at(V, input->cur); 417 | if (input->mode == SELECT) 418 | view_dirty_from(V, 0); /* FIXME suboptimal */ 419 | view_adjust(V); 420 | } 421 | 422 | void do_pgup_pgdown(struct input *input, size_t (*f)(size_t, size_t, size_t, size_t)) 423 | { 424 | struct view *V = input->view; 425 | 426 | do_reset_soft(input); 427 | input->cur = f(input->cur, V->rows, V->cols, cur_bound(input)); 428 | V->start = f(V->start, V->rows, V->cols, cur_bound(input)); 429 | view_dirty_from(V, 0); 430 | view_adjust(V); 431 | } 432 | 433 | 434 | void input_cmd(struct input *input, bool *quit); 435 | void input_search(struct input *input); 436 | 437 | void input_get(struct input *input, bool *quit) 438 | { 439 | key k; 440 | byte b; 441 | 442 | struct view *V = input->view; 443 | struct blob *B = V->blob; 444 | 445 | k = get_key(); 446 | 447 | if (input->mode == INPUT) { 448 | 449 | if (input->input_mode.ascii && isprint(k)) { 450 | 451 | /* ascii input */ 452 | 453 | if (!blob_length(B)) 454 | input->input_mode.insert = true; 455 | 456 | b = k; 457 | if (input->input_mode.insert) { 458 | blob_insert(B, input->cur, &b, sizeof(b), true); 459 | view_recompute(V, false); 460 | view_dirty_from(V, input->cur); 461 | } 462 | else { 463 | blob_replace(B, input->cur, &b, sizeof(b), true); 464 | view_dirty_at(V, input->cur); 465 | } 466 | 467 | cur_move_rel(input, MOVE_RIGHT, 1, 1); 468 | return; 469 | } 470 | 471 | if ((k >= '0' && k <= '9') || (k >= 'a' && k <= 'f')) { 472 | 473 | /* hex input */ 474 | 475 | if (!blob_length(B)) 476 | input->input_mode.insert = true; 477 | 478 | if (input->input_mode.insert) { 479 | if (!input->low_nibble) 480 | input->cur_val = 0; 481 | input->cur_val |= (k > '9' ? k - 'a' + 10 : k - '0') << 4 * (input->low_nibble = !input->low_nibble); 482 | if (input->low_nibble) { 483 | blob_insert(B, input->cur, &input->cur_val, sizeof(input->cur_val), true); 484 | view_recompute(V, false); 485 | view_dirty_from(V, input->cur); 486 | } 487 | else { 488 | blob_replace(B, input->cur, &input->cur_val, sizeof(input->cur_val), true); 489 | view_dirty_at(V, input->cur); 490 | cur_move_rel(input, MOVE_RIGHT, 1, 1); 491 | return; 492 | } 493 | } 494 | else { 495 | input->cur_val = input->cur < blob_length(B) ? blob_at(B, input->cur) : 0; 496 | input->cur_val = input->cur_val & 0xf << 4 * input->low_nibble; 497 | input->cur_val |= (k > '9' ? k - 'a' + 10 : k - '0') << 4 * (input->low_nibble = !input->low_nibble); 498 | blob_replace(B, input->cur, &input->cur_val, sizeof(input->cur_val), true); 499 | view_dirty_at(V, input->cur); 500 | 501 | if (!input->low_nibble) { 502 | cur_move_rel(input, MOVE_RIGHT, 1, 1); 503 | return; 504 | } 505 | 506 | } 507 | 508 | view_adjust(V); 509 | return; 510 | } 511 | 512 | } 513 | 514 | /* function keys */ 515 | 516 | switch (k) { 517 | 518 | case KEY_SPECIAL_ESCAPE: 519 | do_reset_hard(input); 520 | break; 521 | 522 | case 0x7f: /* backspace */ 523 | do_delete(input, true); 524 | break; 525 | 526 | case 'x': 527 | case KEY_SPECIAL_DELETE: 528 | do_delete(input, false); 529 | break; 530 | 531 | case 'q': 532 | do_quit(input, quit, false); 533 | break; 534 | 535 | case 'v': 536 | toggle_mode_select(input); 537 | break; 538 | 539 | case 'y': 540 | do_yank(input); 541 | break; 542 | 543 | case 's': 544 | if (input->mode == SELECT && input->cur > input->sel) { 545 | size_t tmp = input->sel; 546 | input->sel = input->cur; 547 | input->cur = tmp; 548 | } 549 | if (do_delete(input, false) && !input->input_mode.insert) 550 | toggle_mode_insert(input); 551 | break; 552 | 553 | case 'p': 554 | do_paste(input); 555 | break; 556 | 557 | case 'P': 558 | cur_move_rel(input, MOVE_RIGHT, do_paste(input), 1); 559 | break; 560 | 561 | case 'i': 562 | toggle_mode_insert(input); 563 | break; 564 | 565 | case '\t': 566 | toggle_mode_ascii(input); 567 | break; 568 | 569 | case 'u': 570 | if (input->mode != INPUT) break; 571 | if (!blob_undo(B, &input->cur)) 572 | break; 573 | view_recompute(V, false); 574 | cur_adjust(input); 575 | view_adjust(V); 576 | view_dirty_from(V, 0); /* FIXME suboptimal */ 577 | break; 578 | 579 | case 0x12: /* ctrl + R */ 580 | if (input->mode != INPUT) break; 581 | if (!blob_redo(B, &input->cur)) 582 | break; 583 | view_recompute(V, false); 584 | cur_adjust(input); 585 | view_adjust(V); 586 | view_dirty_from(V, 0); /* FIXME suboptimal */ 587 | break; 588 | 589 | case 0x7: /* ctrl + G */ 590 | { 591 | char buf[256]; 592 | snprintf(buf, sizeof(buf), "\"%s\" %s%s %zd/%zd bytes --%zd%%--", 593 | input->view->blob->filename, 594 | input->view->blob->alloc == BLOB_MMAP ? "[mmap]" : "", 595 | input->view->blob->saved_dist ? "[modified]" : "[saved]", 596 | input->cur, 597 | blob_length(input->view->blob), 598 | ((input->cur+1) * 100) / blob_length(input->view->blob)); 599 | view_message(V, buf, NULL); 600 | } 601 | break; 602 | 603 | case 0xc: /* ctrl + L */ 604 | view_dirty_from(V, 0); 605 | break; 606 | 607 | case ':': 608 | printf("\x1b[%uH", V->rows); /* move to last line */ 609 | view_text(V, false); 610 | printf(":"); 611 | input_cmd(input, quit); 612 | view_dirty_from(V, 0); 613 | view_visual(V); 614 | break; 615 | 616 | case '/': 617 | printf("\x1b[%uH", V->rows); /* move to last line */ 618 | view_text(V, false); 619 | printf("/"); 620 | input_search(input); 621 | view_dirty_from(V, 0); 622 | view_visual(V); 623 | break; 624 | 625 | case 'n': 626 | do_search_cont(input, +1); 627 | break; 628 | 629 | case 'N': 630 | do_search_cont(input, -1); 631 | break; 632 | 633 | case 0x1: /* ctrl + A */ 634 | do_inc_dec(input, 1); 635 | break; 636 | 637 | case 0x18: /* ctrl + X */ 638 | do_inc_dec(input, -1); 639 | break; 640 | 641 | case 'j': 642 | case KEY_SPECIAL_DOWN: 643 | cur_move_rel(input, MOVE_RIGHT, 1, V->cols); 644 | break; 645 | 646 | case 'k': 647 | case KEY_SPECIAL_UP: 648 | cur_move_rel(input, MOVE_LEFT, 1, V->cols); 649 | break; 650 | 651 | case 'l': 652 | case KEY_SPECIAL_RIGHT: 653 | cur_move_rel(input, MOVE_RIGHT, 1, 1); 654 | break; 655 | 656 | case 'h': 657 | case KEY_SPECIAL_LEFT: 658 | cur_move_rel(input, MOVE_LEFT, 1, 1); 659 | break; 660 | 661 | case '^': 662 | cur_move_rel(input, MOVE_LEFT, (input->cur - V->start) % V->cols, 1); 663 | break; 664 | 665 | case '$': 666 | cur_move_rel(input, MOVE_RIGHT, V->cols-1 - (input->cur - V->start) % V->cols, 1); 667 | break; 668 | 669 | case 'g': 670 | case KEY_SPECIAL_HOME: 671 | do_home_end(input, min(V->start, cur_bound(input) - 1), 0); 672 | break; 673 | 674 | case 'G': 675 | case KEY_SPECIAL_END: 676 | do_home_end(input, min(V->start + V->rows * V->cols - 1, cur_bound(input) - 1), cur_bound(input) - 1); 677 | break; 678 | 679 | case 0x15: /* ctrl + U */ 680 | case KEY_SPECIAL_PGUP: 681 | do_pgup_pgdown(input, sat_sub_step); 682 | break; 683 | 684 | case 0x4: /* ctrl + D */ 685 | case KEY_SPECIAL_PGDOWN: 686 | do_pgup_pgdown(input, sat_add_step); 687 | break; 688 | 689 | case '[': 690 | view_set_cols(V, true, -1); 691 | break; 692 | 693 | case ']': 694 | view_set_cols(V, true, +1); 695 | break; 696 | 697 | } 698 | } 699 | 700 | void input_cmd(struct input *input, bool *quit) 701 | { 702 | char buf[0x100], *p; 703 | unsigned long long n; 704 | 705 | if (!fgets_retry(buf, sizeof(buf), stdin)) 706 | pdie("fgets"); 707 | 708 | if ((p = strchr(buf, '\n'))) 709 | *p = 0; 710 | 711 | if (!(p = strtok(buf, " "))) 712 | return; 713 | else if (!strcmp(p, "w") || !strcmp(p, "wq")) { 714 | switch (blob_save(input->view->blob, strtok(NULL, " "))) { 715 | case BLOB_SAVE_OK: 716 | if (!strcmp(p, "wq")) 717 | do_quit(input, quit, false); 718 | break; 719 | case BLOB_SAVE_FILENAME: 720 | view_error(input->view, "can't save: no filename."); 721 | break; 722 | case BLOB_SAVE_NONEXISTENT: 723 | view_error(input->view, "can't save: nonexistent path."); 724 | break; 725 | case BLOB_SAVE_PERMISSIONS: 726 | view_error(input->view, "can't save: insufficient permissions."); 727 | break; 728 | case BLOB_SAVE_BUSY: 729 | view_error(input->view, "can't save: file is busy."); 730 | break; 731 | default: 732 | die("can't save: unknown error"); 733 | } 734 | } 735 | else if (!strcmp(p, "q") || !strcmp(p, "q!")) { 736 | do_quit(input, quit, !strcmp(p, "q!")); 737 | } 738 | else if (!strcmp(p, "color")) { 739 | if ((p = strtok(NULL, " "))) 740 | input->view->color = *p == '1' || *p == 'y'; 741 | } 742 | else if (!strcmp(p, "columns")) { 743 | if ((p = strtok(NULL, " "))) { 744 | if (!strcmp(p, "auto")) { 745 | view_set_cols(input->view, false, 0); 746 | } 747 | else { 748 | n = strtoull(p, &p, 0); 749 | if (!*p) 750 | view_set_cols(input->view, false, n); 751 | } 752 | } 753 | } 754 | else { 755 | /* try to interpret the input as an offset */ 756 | n = strtoull(p, &p, 0); 757 | if (!*p) { 758 | view_dirty_at(input->view, input->cur); 759 | if (n < cur_bound(input)) 760 | input->cur = n; 761 | view_dirty_at(input->view, input->cur); 762 | view_adjust(input->view); 763 | } 764 | } 765 | } 766 | 767 | static unsigned unhex_digit(char c) 768 | { 769 | assert(isxdigit(c)); 770 | if (c >= '0' && c <= '9') 771 | return c - '0'; 772 | else if (c >= 'a' && c <= 'f') 773 | return c - 'a' + 10; 774 | else if (c >= 'A' && c <= 'F') 775 | return c - 'A' + 10; 776 | die("not a hex digit"); 777 | } 778 | 779 | static size_t unhex(byte **ret, char const *hex) 780 | { 781 | size_t len = 0; 782 | *ret = malloc_strict(strlen(hex) / 2); 783 | for (char const *p = hex; *p; ) { 784 | while (isspace(*p)) ++p; 785 | if (!(isxdigit(p[0]) && isxdigit(p[1]))) { 786 | free(*ret); 787 | *ret = NULL; 788 | return 0; 789 | } 790 | (*ret)[len] = unhex_digit(*p++) << 4; 791 | (*ret)[len++] |= unhex_digit(*p++); 792 | } 793 | *ret = realloc_strict(*ret, len); /* shrink to what we actually used */ 794 | return len; 795 | } 796 | 797 | /* NB: this accepts some technically invalid inputs */ 798 | static size_t utf8_to_ucs2(byte **ret, char const *str) 799 | { 800 | size_t len = 0; 801 | *ret = malloc_strict(2 * strlen(str)); 802 | for (uint32_t c, b; (c = *str++); ) { 803 | if (!(c & 0x80)) b = 0; 804 | else if ((c & 0xe0) == 0xc0) c &= 0x1f, b = 1; 805 | else if ((c & 0xf0) == 0xe0) c &= 0x0f, b = 2; 806 | else if ((c & 0xf8) == 0xf0) c &= 0x07, b = 3; 807 | else { 808 | bad: 809 | free(*ret); 810 | *ret = NULL; 811 | return 0; 812 | } 813 | while (b--) { 814 | if ((*str & 0xc0) != 0x80) goto bad; 815 | c <<= 6, c |= (*str++ & 0x3f); 816 | } 817 | if (c >> 16) goto bad; /* not representable */ 818 | (*ret)[len++] = c >> 0; 819 | (*ret)[len++] = c >> 8; 820 | } 821 | *ret = realloc_strict(*ret, len); /* shrink to what we actually used */ 822 | return len; 823 | } 824 | 825 | void input_search(struct input *input) 826 | { 827 | char buf[0x100], *p, *q; 828 | 829 | if (!fgets_retry(buf, sizeof(buf), stdin)) 830 | pdie("fgets"); 831 | 832 | if ((p = strchr(buf, '\n'))) 833 | *p = 0; 834 | 835 | input->search.len = 0; 836 | free(input->search.needle); 837 | input->search.needle = NULL; 838 | 839 | if (!(p = strtok(buf, " "))) 840 | return; 841 | else if (!strcmp(p, "x") || !strcmp(p, "w")) { 842 | size_t (*fun)(byte **, char const *) = (*p == 'x') ? unhex : utf8_to_ucs2; 843 | if (!(q = strtok(NULL, " "))) { 844 | q = p; 845 | goto str; 846 | } 847 | input->search.len = fun(&input->search.needle, q); 848 | } 849 | else if (!strcmp(p, "s")) { 850 | if (!(q = strtok(NULL, ""))) 851 | q = p; 852 | str: 853 | input->search.len = strlen(q); 854 | input->search.needle = (byte *) strdup(q); 855 | } 856 | else if (!(input->search.len = unhex(&input->search.needle, p))) { 857 | q = p; 858 | goto str; 859 | } 860 | 861 | do_search_cont(input, +1); 862 | } 863 | 864 | -------------------------------------------------------------------------------- /input.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_H 2 | #define INPUT_H 3 | 4 | #include "view.h" 5 | 6 | struct input { 7 | struct view *view; 8 | 9 | enum mode { 10 | INPUT, 11 | SELECT, 12 | } mode; 13 | struct { 14 | bool insert: 1; 15 | bool ascii: 1; 16 | } input_mode; 17 | 18 | size_t cur, sel; 19 | bool low_nibble; 20 | byte cur_val; 21 | 22 | struct { 23 | size_t len; 24 | byte *needle; 25 | } search; 26 | 27 | bool quit; 28 | }; 29 | 30 | void input_init(struct input *input, struct view *view); 31 | void input_free(struct input *input); 32 | 33 | void input_get(struct input *input, bool *quit); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2024 Lorenz Panny 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /view.c: -------------------------------------------------------------------------------- 1 | 2 | #include "common.h" 3 | #include "blob.h" 4 | #include "view.h" 5 | #include "input.h" 6 | #include "ansi.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | static void fprint(FILE *fp, char const *s) { fprintf(fp, "%s", s); } 14 | static void print(char const *s) { fprint(stdout, s); } 15 | 16 | static size_t view_end(struct view const *view) 17 | { 18 | return view->start + view->rows * view->cols; 19 | } 20 | 21 | void view_init(struct view *view, struct blob *blob, struct input *input) 22 | { 23 | memset(view, 0, sizeof(*view)); 24 | view->blob = blob; 25 | view->input = input; 26 | view->pos_digits = 4; /* rather arbitrary */ 27 | view->color = true; 28 | if (tcgetattr(fileno(stdin), &view->term)) 29 | pdie("tcgetattr"); 30 | view->initialized = true; 31 | } 32 | 33 | void view_text(struct view *view, bool leave_alternate) 34 | { 35 | if (!view->initialized) return; 36 | 37 | if (leave_alternate) 38 | print(leave_alternate_screen); 39 | cursor_column(0); 40 | print(clear_line); 41 | print(show_cursor); 42 | fflush(stdout); 43 | 44 | if (tcsetattr(fileno(stdin), TCSANOW, &view->term)) { 45 | /* can't pdie() because that risks infinite recursion */ 46 | perror("tcsetattr"); 47 | exit(EXIT_FAILURE); 48 | } 49 | } 50 | 51 | void view_visual(struct view *view) 52 | { 53 | assert(view->initialized); 54 | 55 | struct termios term = view->term; 56 | term.c_lflag &= ~ICANON & ~ECHO; 57 | if (tcsetattr(fileno(stdin), TCSANOW, &term)) 58 | pdie("tcsetattr"); 59 | 60 | print(enter_alternate_screen); 61 | print(hide_cursor); 62 | fflush(stdout); 63 | } 64 | 65 | void view_set_cols(struct view *view, bool relative, int cols) 66 | { 67 | if (relative) { 68 | if (cols >= 0 || (unsigned) -cols <= view->cols) 69 | cols += view->cols; 70 | else 71 | cols = view->cols; 72 | } 73 | 74 | if (!cols) { 75 | if (view->cols_fixed) { 76 | view->cols_fixed = false; 77 | view_recompute(view, true); 78 | } 79 | return; 80 | } 81 | 82 | view->cols_fixed = true; 83 | 84 | if ((unsigned) cols != view->cols) { 85 | view->cols = cols; 86 | view_dirty_from(view, 0); 87 | } 88 | } 89 | 90 | void view_recompute(struct view *view, bool winch) 91 | { 92 | struct winsize winsz; 93 | unsigned old_rows = view->rows, old_cols = view->cols; 94 | unsigned digs = (bit_length(max(2, blob_length(view->blob)) - 1) + 3) / 4; 95 | 96 | if (digs > view->pos_digits) { 97 | view->pos_digits = digs; 98 | view_dirty_from(view, 0); 99 | } 100 | else if (!winch) 101 | return; 102 | 103 | if (-1 == ioctl(fileno(stdout), TIOCGWINSZ, &winsz)) 104 | pdie("ioctl"); 105 | 106 | view->rows = winsz.ws_row; 107 | if (!view->cols_fixed) { 108 | view->cols = (winsz.ws_col - (view->pos_digits + strlen(": ") + strlen("||"))) / strlen("xx c"); 109 | 110 | if (view->cols > CONFIG_ROUND_COLS) 111 | view->cols -= view->cols % CONFIG_ROUND_COLS; 112 | } 113 | 114 | if (!view->rows || !view->cols) 115 | die("window too small."); 116 | 117 | if (view->rows == old_rows && view->cols == old_cols) 118 | return; 119 | 120 | /* update dirtiness array */ 121 | if ((view->dirty = realloc_strict(view->dirty, view->rows * sizeof(*view->dirty)))) 122 | memset(view->dirty, 0, view->rows * sizeof(*view->dirty)); 123 | view_dirty_from(view, 0); 124 | 125 | view_adjust(view); 126 | 127 | print(clear_screen); 128 | } 129 | 130 | void view_free(struct view *view) 131 | { 132 | free(view->dirty); 133 | } 134 | 135 | void view_message(struct view *view, char const *msg, char const *color) 136 | { 137 | cursor_line(view->rows - 1); 138 | print(clear_line); 139 | if (view->color && color) print(color); 140 | printf("%*c %s", view->pos_digits, ' ', msg); 141 | if (view->color && color) print(color_normal); 142 | fflush(stdout); 143 | view->dirty[view->rows - 1] = 2; /* redraw at the next keypress */ 144 | } 145 | 146 | void view_error(struct view *view, char const *msg) 147 | { 148 | view_message(view, msg, color_red); 149 | } 150 | 151 | /* FIXME hex and ascii mode look very similar */ 152 | static void render_line(struct view *view, size_t off, size_t last) 153 | { 154 | byte b; 155 | char digits[0x10], *asciiptr; 156 | size_t asciilen; 157 | FILE *asciifp; 158 | struct input *I = view->input; 159 | 160 | size_t sel_start = min(I->cur, I->sel), sel_end = max(I->cur, I->sel); 161 | char const *last_color = NULL, *next_color; 162 | 163 | if (!(asciifp = open_memstream(&asciiptr, &asciilen))) 164 | pdie("open_memstream"); 165 | #define BOTH(EX) for (FILE *fp; ; ) { fp = stdout; EX; fp = asciifp; EX; break; } 166 | 167 | if (off <= I->cur && I->cur < off + view->cols) { 168 | /* cursor in current line */ 169 | if (view->color) print(color_yellow); 170 | printf("%0*zx%c ", view->pos_digits, I->cur, I->input_mode.insert ? '+' : '>'); 171 | if (view->color) print(color_normal); 172 | } 173 | else { 174 | printf("%0*zx: ", view->pos_digits, off); 175 | } 176 | 177 | if (I->mode == SELECT && off > sel_start && off <= sel_end) 178 | print(underline_on); 179 | 180 | for (size_t j = 0, len = blob_length(view->blob); j < view->cols; ++j) { 181 | 182 | if (off + j < len) { 183 | sprintf(digits, "%02hhx", b = blob_at(view->blob, off + j)); 184 | } 185 | else { 186 | b = 0; 187 | strcpy(digits, " "); 188 | } 189 | 190 | if (I->mode == SELECT && off + j == sel_start) 191 | print(underline_on); 192 | 193 | if (off + j >= last) { 194 | for (size_t p = j; p < view->cols; ++p) 195 | printf(" "); 196 | break; 197 | } 198 | 199 | if (off + j == I->cur) { 200 | next_color = I->cur >= blob_length(view->blob) ? color_red : color_yellow; 201 | BOTH( 202 | if (view->color && next_color != last_color) fprint(fp, next_color); 203 | fprint(fp, inverse_video_on); 204 | ); 205 | 206 | if (!I->input_mode.ascii) { 207 | print(bold_on); 208 | if (I->mode == INPUT && !I->low_nibble) print(underline_on); 209 | putchar(digits[0]); 210 | if (I->mode == INPUT) print(I->low_nibble ? underline_on : underline_off); 211 | putchar(digits[1]); 212 | if (I->mode == INPUT && I->low_nibble) print(underline_off); 213 | print(bold_off); 214 | } 215 | else 216 | printf("%s", digits); 217 | 218 | if (I->mode == INPUT && I->input_mode.ascii) 219 | fprintf(asciifp, "%s%s%c%s%s", bold_on, underline_on, isprint(b) ? b : '.', underline_off, bold_off); 220 | else 221 | fputc(isprint(b) ? b : '.', asciifp); 222 | 223 | BOTH( 224 | fprint(fp, inverse_video_off); 225 | ); 226 | } 227 | else { 228 | next_color = isalnum(b) ? color_cyan 229 | : isprint(b) ? color_blue 230 | : !b ? color_red 231 | : color_normal; 232 | BOTH( 233 | if (view->color && next_color != last_color) 234 | fprint(fp, next_color); 235 | ); 236 | fputc(isprint(b) ? b : '.', asciifp); 237 | printf("%s", digits); 238 | } 239 | last_color = next_color; 240 | 241 | if (I->mode == SELECT && (off + j == sel_end || j == view->cols - 1)) 242 | print(underline_off); 243 | 244 | putchar(' '); 245 | } 246 | if (view->color) print(color_normal); 247 | 248 | #undef BOTH 249 | if (fclose(asciifp)) 250 | pdie("fclose"); 251 | putchar('|'); 252 | printf("%s", asciiptr); 253 | free(asciiptr); 254 | if (view->color) print(color_normal); 255 | putchar('|'); 256 | } 257 | 258 | void view_update(struct view *view) 259 | { 260 | size_t last = max(blob_length(view->blob), view->input->cur + 1); 261 | 262 | if (view->scroll) { 263 | printf("\x1b[%ld%c", labs(view->scroll), view->scroll > 0 ? 'S' : 'T'); 264 | view->scroll = 0; 265 | } 266 | 267 | for (size_t i = view->start, l = 0; i < view_end(view); i += view->cols, ++l) { 268 | /* dirtiness counter enables displaying messages until keypressed; */ 269 | if (!view->dirty[l] || --view->dirty[l]) 270 | continue; 271 | cursor_line(l); 272 | print(clear_line); 273 | if (i < last) 274 | render_line(view, i, last); 275 | } 276 | 277 | fflush(stdout); 278 | } 279 | 280 | void view_dirty_at(struct view *view, size_t pos) 281 | { 282 | view_dirty_fromto(view, pos, pos + 1); 283 | } 284 | 285 | void view_dirty_from(struct view *view, size_t from) 286 | { 287 | view_dirty_fromto(view, from, view_end(view)); 288 | } 289 | 290 | void view_dirty_fromto(struct view *view, size_t from, size_t to) 291 | { 292 | size_t lfrom, lto; 293 | from = max(view->start, from); 294 | to = min(view_end(view), to); 295 | if (from < to) { 296 | lfrom = (from - view->start) / view->cols; 297 | lto = (to - view->start + view->cols - 1) / view->cols; 298 | for (size_t i = lfrom; i < lto; ++i) 299 | view->dirty[i] = max(view->dirty[i], 1); 300 | } 301 | } 302 | 303 | static size_t satadd(size_t x, size_t y, size_t b) 304 | { assert(b >= 1); return min(b - 1, x + y); } 305 | static size_t satsub(size_t x, size_t y, size_t a) 306 | { return x < y || x - y < a ? a : x - y; } 307 | 308 | void view_adjust(struct view *view) 309 | { 310 | size_t old_start = view->start; 311 | 312 | assert(view->input->cur <= blob_length(view->blob)); 313 | 314 | if (view->input->cur >= view_end(view)) 315 | view->start = satadd(view->start, 316 | (view->input->cur + 1 - view_end(view) + view->cols - 1) / view->cols * view->cols, 317 | blob_length(view->blob)); 318 | 319 | if (view->input->cur < view->start) 320 | view->start = satsub(view->start, 321 | (view->start - view->input->cur + view->cols - 1) / view->cols * view->cols, 322 | 0); 323 | 324 | assert(view->input->cur >= view->start); 325 | assert(view->input->cur < view_end(view)); 326 | 327 | /* scrolling */ 328 | if (view->start != old_start) { 329 | if (!(((ssize_t) view->start - (ssize_t) old_start) % (ssize_t) view->cols)) { 330 | view->scroll = ((ssize_t) view->start - (ssize_t) old_start) / (ssize_t) view->cols; 331 | if (view->scroll > (signed) view->rows || -view->scroll > (signed) view->rows) { 332 | view->scroll = 0; 333 | view_dirty_from(view, 0); 334 | return; 335 | } 336 | if (view->scroll > 0) { 337 | memmove( 338 | view->dirty, 339 | view->dirty + view->scroll, 340 | (view->rows - view->scroll) * sizeof(*view->dirty) 341 | ); 342 | for (size_t i = view->rows - view->scroll; i < view->rows; ++i) 343 | view->dirty[i] = 1; 344 | } 345 | else { 346 | memmove( 347 | view->dirty + (-view->scroll), 348 | view->dirty, 349 | (view->rows - (-view->scroll)) * sizeof(*view->dirty) 350 | ); 351 | for (size_t i = 0; i < (size_t) -view->scroll; ++i) 352 | view->dirty[i] = 1; 353 | } 354 | } 355 | else 356 | view_dirty_from(view, 0); 357 | } 358 | 359 | } 360 | 361 | -------------------------------------------------------------------------------- /view.h: -------------------------------------------------------------------------------- 1 | #ifndef VIEW_H 2 | #define VIEW_H 3 | 4 | #include "blob.h" 5 | 6 | #include 7 | #include 8 | 9 | struct input; 10 | struct view { 11 | bool initialized; 12 | 13 | struct blob *blob; 14 | struct input *input; /* FIXME hack */ 15 | 16 | size_t start; 17 | 18 | uint8_t *dirty; 19 | signed scroll; 20 | 21 | bool cols_fixed; 22 | unsigned rows, cols; 23 | unsigned pos_digits; 24 | bool color; 25 | bool winch; 26 | bool tstp, cont; 27 | 28 | struct termios term; 29 | }; 30 | 31 | void view_init(struct view *view, struct blob *blob, struct input *input); 32 | void view_text(struct view *view, bool leave_alternate); 33 | void view_visual(struct view *view); 34 | void view_recompute(struct view *view, bool winch); 35 | void view_set_cols(struct view *view, bool relative, int cols); 36 | void view_free(struct view *view); 37 | 38 | void view_message(struct view *view, char const *msg, char const *color); 39 | void view_error(struct view *view, char const *msg); 40 | 41 | void view_update(struct view *view); 42 | 43 | void view_dirty_at(struct view *view, size_t pos); 44 | void view_dirty_from(struct view *view, size_t from); 45 | void view_dirty_fromto(struct view *view, size_t from, size_t to); 46 | void view_adjust(struct view *view); 47 | 48 | #endif 49 | --------------------------------------------------------------------------------