#define _POSIX_C_SOURCE 202511L #include #include #include #include #include #include #include #include #include #define ARENA_IMPL #define UTF8_IMPL #include "wrmr.h" #include "arena.h" #include "txt.h" #include "vui.h" #include "utf8.h" Arena scratch = { 0 }; Txt txt = { 0 }; TxtLoc cur = { 0 }; int mode = 0; u32 count = 0; static inline int is_space(u32 c) { return c <= 0x20 && c != 0; } TxtLoc next_word(TxtLoc l) { while (!at_end(l) && is_space(txt_chr(l))) l = cnext(l); while (!at_end(l) && !is_space(txt_chr(l))) l = cnext(l); return l; } TxtLoc prev_word(TxtLoc l) { while (!at_start(l)) { TxtLoc n = bprev(l); if (!is_space(txt_chr(n))) break; l = n; } while (!at_start(l)) { TxtLoc n = bprev(l); if (is_space(txt_chr(n))) break; l = n; } return l; } u32 move_word_back(Txt *t, u32 cur) { return txt_ofs(prev_word(txt_at(t, cur))); } u32 move_word_fwd(Txt *t, u32 cur) { return txt_ofs(next_word(txt_at(t, cur))); } u32 move_char_back(Txt *t, u32 cur) { return txt_ofs(cprev(txt_at(t, cur))); } u32 move_char_fwd(Txt *t, u32 cur) { return txt_ofs(cnext(txt_at(t, cur))); } u32 del_between(Txt *t, u32 a, u32 b) { if (b < a) { u32 t = a; a = b; b = t; } return txt_ofs(txt_delete(txt_at(t, b), b - a)); } u32 move_line_up(Txt *t, u32 cur) { return txt_ofs(prev_line(txt_at(t, cur))); } u32 move_line_down(Txt *t, u32 cur) { return txt_ofs(next_line(txt_at(t, cur))); } int empty_line(TxtLoc l) { u8 b = txt_byte(start_of_line(l)); return b == '\n' || b == 0 /* last line of buffer */; } TxtLoc next_par(TxtLoc l) { while (!at_end(l) && empty_line(l)) l = next_line(l); while (!at_end(l) && !empty_line(l)) l = next_line(l); return l; } TxtLoc prev_par(TxtLoc l) { while (!at_start(l) && empty_line(l)) l = prev_line(l); while (!at_start(l) && !empty_line(l)) l = prev_line(l); return l; } u32 move_par_up(Txt *t, u32 cur) { return txt_ofs(prev_par(txt_at(t, cur))); } u32 move_par_down(Txt *t, u32 cur) { return txt_ofs(next_par(txt_at(t, cur))); } typedef struct { pid_t pid; int from, to; } ChildProcess; int popen2(const char *cmd, int *in, int *out) { int rd[2], wr[2]; if(pipe(rd)) return -1; if(pipe(wr)) return -1; pid_t p = fork(); if(p < 0) return -1; if(p == 0) { /* child */ close(rd[1]); close(wr[0]); dup2(rd[0], STDIN_FILENO); dup2(wr[1], STDOUT_FILENO); close(rd[0]); close(wr[1]); if (execl("/bin/sh", "sh", "-c", cmd, 0)) { err(1, "execl"); } } close(rd[0]); close(wr[1]); *in = rd[1]; *out = wr[0]; return p; } int shell_replace(TxtLoc start, TxtLoc end, const char *cmd) { if (txt_before(end, start)) { TxtLoc t = start; start = end; end = t; } int in, out; int p = popen2(cmd, &in, &out); if (p < 0) return -1; struct pollfd pfds[2] = { { .fd = in, .events = POLLOUT }, { .fd = out, .events = POLLIN } }; Str wrs = txt_collect_range(start, end, &scratch); DYNARR(char) rds = { 0 }; u32 wr_i = 0; while ((pfds[0].fd >= 0 || pfds[1].fd >= 0) && poll(pfds, 2, 200) >= 0) { const u32 chunksz = 8L << 10; if (pfds[0].revents & POLLOUT) { u32 wr_n = wrs.n - wr_i; if (wr_n > chunksz) wr_n = chunksz; isize r = write(in, wrs.s + wr_i, wr_n); if (r > 0) { wr_i += wr_n; } else { close(in); pfds[0].fd = -1; } } else if (pfds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) { close(in); pfds[0].fd = -1; } if (pfds[1].revents & POLLIN) { DA_AFIT(&rds, &scratch, rds.n + (-rds.n & (chunksz-1)) + chunksz); isize r = read(out, &rds.v[rds.n], chunksz); if (r > 0) { rds.n += r; } else { close(out); pfds[1].fd = -1; } } else if (pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) { close(out); pfds[1].fd = -1; } } /* TODO: wrap this logic in a general txt_replace() function, * and make it so it will avoid deleting as much as possible; * reflowing text often only changes lines towards the end, * for example. maybe do something like skip as many pieces * as match 100%, then iteratively halve the length of the * final one until it matches or gets too small. */ if (!txt_range_equal(start, end, (Str){rds.v,rds.n})) { u32 b = txt_ofs(cur); start = txt_delete_range(start, end); end = txt_insert(start, rds.v, rds.n); cur = txt_at(start.t, b); } return 0; } /* main */ #define ODD_ATTR (FG_CYAN | BG_BLACK) #define EVEN_ATTR (FG_WHITE | BG_BLACK) void find_view_window(TxtLoc l, TxtLoc *start, TxtLoc *end) { u32 u = LINES / 2; TxtLoc a = l; for (u32 i = 0; i < u; i++) a = prev_newline(a); u32 n = 0; TxtLoc b = a; while (!at_end(b) && n++ < LINES) b = next_newline(b); while (!at_start(a) && n++ < LINES) a = prev_newline(a); if (!at_start(a) && txt_byte(a) == '\n') a = bnext(a); *start = a; *end = b; } void draw(void *ctx) { (void)ctx; vui_clear(); int lmarg = 0; int x = lmarg, y = 0; TxtLoc start, end; find_view_window(cur, &start, &end); vui_aprintf(-1, 0, ODD_ATTR, "%u piece(s)", txt.ptbl.n); for (u32 i = 0; i < txt.ptbl.n; i++) { TxtPiece *p = &txt.ptbl.v[i]; VuiAttr a = i&1 ? ODD_ATTR : EVEN_ATTR; vui_aprintf(-1, i+1, a, "%u, %u (%s)", p->ofs, p->n, p->buf == TXT_ADD ? "add" : "src"); } TxtLoc l = cur; int cur_found = 0; while (txt_before(start, end)) { if (l.p == start.p && l.i == start.i) { cur_found = 1; vui_curs_pos(x, y); } u32 c = txt_chr_next(&start); if (c == '\n') { x = lmarg; y++; } else if (c == '\t') { x++; x += (-x & 7); } else if (c) { vui_chr(x++, y, c); } } ASSERT(start.i <= txt.ptbl.v[start.p].n); if (!cur_found) vui_curs_pos(x, y); u32 c = txt_chr(l); vui_printf(-1, -4, "%u", count); vui_aprintf(-1, -3, mode ? FG_BCYAN : FG_CYAN, "%s", mode ? "INSERT" : "NORMAL"); vui_printf(-1, -1, "%u - %u.%u - %02x (%c)", txt_ofs(cur), l.p, l.i, c, (c < 0x20 || c > 0x7e) ? ' ' : c); u32 used = scratch.beg - scratch.start, max = scratch.end - scratch.start; vui_printf(-1, -2, "scratch %.02f/%.02fk", used/1024.0, max/1024.0); } int motion(TxtLoc *lp, u32 c) { TxtLoc l = *lp; TxtLoc last_loc = l; loop: switch (c) { case KEY_LEFT: case 'h': l = cprev(l); break; case KEY_RIGHT: case 'l': l = cnext(l); break; case KEY_LEFT | KEY_CTRL_BIT: case 'b': l = prev_word(l); break; case KEY_RIGHT | KEY_CTRL_BIT: case 'w': l = next_word(l); break; case KEY_UP: case 'k': l = prev_line(l); break; case KEY_DOWN: case 'j': l = next_line(l); break; case KEY_UP | KEY_CTRL_BIT: case '{': l = prev_par(l); break; case KEY_DOWN | KEY_CTRL_BIT: case '}': l = next_par(l); break; case KEY_PGUP: for (u32 i = 0; i < LINES; i += 3) l = prev_line(l); break; case KEY_PGDN: for (u32 i = 0; i < LINES; i += 3) l = next_line(l); break; case KEY_HOME: case '^': case '0': l = start_of_line(l); break; case KEY_END: case '$': l = end_of_line(l); break; case KEY_HOME | KEY_CTRL_BIT: case 'g': l = txt_start(&txt); break; case KEY_END | KEY_CTRL_BIT: case 'G': l = txt_end(&txt); break; case 'f': { u32 k = vui_key(); TxtLoc t = cnext(l); for (TxtLoc n = l;;) { u32 x = txt_chr_next(&n); if (x == '\n' || x == 0) break; if (x == k) { l = t; break; } t = n; } } break; case 'F': { u32 k = vui_key(); TxtLoc t = cprev(l); for (;;) { u32 x = txt_chr(l); if (x == '\n') break; if (x == k) { l = t; break; } t = bprev(t); } } break; default: return 0; } if (count > 1 && (txt_before(l, last_loc) || txt_after(l, last_loc))) { count--; goto loop; } count = 0; *lp = l; return 1; } #include void test_edits(void) { for (u32 iter = 0; iter < 8192; iter++) { ASSERT(iter != 385); u32 n = rand() % 100; cur = txt_at(&txt, rand() % txt.len); if (rand() & 1) { cur = txt_delete(cur, n); } else { for (u32 j = 0; j < n; j++) { const char ch[] = { 'a','b','c','d','e','f','g','h','j','k','l','i', 'm','o','p','q','r','s','t','u','v','w','x','y','z','\n', 'A','B','C','D','E','F','G','H','J','K','L','I', 'M','O','P','Q','R','S','T','U','V','W','X','Y','Z', }; cur = txt_insert_c(cur, ch[rand() % COUNTOF(ch)]); } } if (txt.ptbl.n > 1) { for (u32 i = 0; i < txt.ptbl.n; i++) { ASSERT(txt.ptbl.v[i].n > 0); } } } } int main(int argc, const char **argv) { scratch = arena_init(128L << 20); vui_init(); vui_curs_vis(1); vui_redraw_fn(draw); //const char *path = "test.txt"; const char *path = "/usr/share/dict/words"; if (argc > 1) path = argv[1]; if (txt_load(&txt, path)) err(1, "couldn't open file"); cur = txt_end(&txt); //srand(clock()); srand(123); for (;;) { scratch.beg = scratch.start; draw(NULL); vui_curs_shape(mode ? VUI_CURS_BAR : VUI_CURS_BLOCK); vui_blit(); u32 c = vui_key(); switch (mode) { case 0: switch (c) { case 'q': goto brk; case 'z': case 'Z': case 0x13 /* ^S */: txt_save(&txt, path); //txt_load(&txt, "test.txt"); if (c == 'Z') goto brk; break; case 'i': mode = 1; break; case 'a': { mode = 1; TxtLoc l = cur; TxtLoc e = end_of_line(l); l = txt_before(l, e) ? cnext(l) : e; cur = l; } break; case 'I': mode = 1; cur = start_of_line(cur); break; case 'A': mode = 1; cur = end_of_line(cur); break; case 'o': mode = 1; cur = end_of_line(cur); goto ins_newline; case 'O': mode = 1; cur = bprev(start_of_line(cur)); goto ins_newline; case 'd': { TxtLoc before = cur; if (motion(&cur, vui_key())) { cur = txt_delete_range(before, cur); } } break; case 'D': cur = txt_delete_range(cur, end_of_line(cur)); break; case 'C': cur = txt_delete_range(cur, end_of_line(cur)); mode = 1; break; case 'S': { TxtLoc start = start_of_line(cur); TxtLoc end = end_of_line(cur); cur = txt_delete_range(start, end); mode = 1; } break; case 'c': { TxtLoc before = cur; if (motion(&cur, vui_key())) { cur = txt_delete_range(before, cur); mode = 1; } break; } case 'x': cur = txt_delete_c(cnext(cur)); break; case 's': cur = txt_delete_c(cnext(cur)); mode = 1; break; case '0': if (!count) { motion(&cur, c); break; } /* fallthrough */ case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': count = (count % 100000000) * 10 + c - '0'; break; case 'M': { TxtLoc start = prev_par(cur); TxtLoc end = next_par(cur); if (shell_replace(start, end, "fmt -w80 -u")) { err(1, "shell_replace"); } }; default: motion(&cur, c); break; } break; case 1: switch (c) { case KEY_ESC: if (txt_after(cur, start_of_line(cur))) { cur = cprev(cur); } mode = 0; break; case KEY_BKSP: cur = txt_delete_c(cur); break; case 0x17 /* ^W */: cur = txt_delete_range(prev_word(cur), cur); break; case 0x05 /* ^E */: cur = txt_delete_range(cur, next_word(cur)); break; case '\r': { ins_newline:; u32 tabs = 0; TxtLoc start = start_of_line(cur); for (TxtLoc l = start; txt_byte(l) == '\t'; l = bnext(l)) tabs++; while (txt_byte(cprev(cur)) == '\t') cur = txt_delete(cur, 1); cur = txt_insert_c(cur, '\n'); while (tabs--) cur = txt_insert_c(cur, '\t'); } break; default: if ((c == '\t' || c >= ' ') && c <= KEY_UTF8_MAX) cur = txt_insert_c(cur, c); break; } break; } if (mode == 1) count = 0; ASSERT(txt_valid_loc(cur)); if (txt.ptbl.n > 1) { for (u32 i = 0; i < txt.ptbl.n; i++) { ASSERT(txt.ptbl.v[i].n > 0); } } } brk: vui_fini(); arena_free(&scratch); return 0; }