#define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include "wrmr.h" #include "arena.h" #include "txt.h" #include "vui.h" #include "utf8.h" #include "str.h" #include "freelist.h" #include "regex.h" #define ED_BUF_MAX 1024 typedef enum { ED_BUF_SCRATCH, ED_BUF_FILE, ED_BUF_BIN_FILE } EditBufType; typedef struct { EditBufType type; Arena arena; Str path; Txt *txt; TxtLoc cur; } EditBuf; typedef enum { MODE_NORMAL, MODE_INSERT, MODE_REPLACE_ONE, MODE_REPLACE_MULT } EditMode; static const char *mode_key_str[] = { "N", "I", "R", "R", }; typedef struct { Arena perm, scratch; EditBuf buf[ED_BUF_MAX]; u32 bufn, bufi; Txt *txt_free; EditMode mode; u32 count; /* these unfortunately have to be global so that draw() will know what * to do when a window resizes during input */ Str msg; Str input_line, input_prompt; u32 optc, opti; Str *optv; u32 *optvi; Str search; int search_dir; u32 search_char; int search_char_dir; int search_char_incl; Str homedir; Str yank; } Editor; Editor e = { 0 }; int ed_buf_shell_replace(Editor *e, u32 bufi, TxtLoc start, TxtLoc end, const char *cmd); static Str normalize_path(Str s, Arena *a); int ed_buf_open(Editor *e, const char *path) { Str paths = { 0 }; if (path) { paths = normalize_path(str_from_cstr(path), &e->scratch); for (u32 i = 0; i < e->bufn; i++) if (str_eql(e->buf[i].path, paths)) return i; } if (e->bufn == ED_BUF_MAX) return -1; EditBuf *b = &e->buf[e->bufn]; memset(b, 0, sizeof(EditBuf)); b->arena = arena_init(1L << 30); b->txt = FREELIST_NEW(&e->txt_free, &e->perm); if (path) { b->path = str_dup(paths, &b->arena); b->type = ED_BUF_FILE; txt_load(b->txt, path); if (!utf8_validate(b->txt->buf[TXT_SRC].s, b->txt->buf[TXT_SRC].n)) { ed_buf_shell_replace(e, e->bufn, txt_start(b->txt), txt_end(b->txt), "xxd"); b->txt->readonly = 1; b->type = ED_BUF_BIN_FILE; } } else { b->path = S("*scratch*"); b->type = ED_BUF_SCRATCH; txt_load_empty(b->txt); } b->cur = txt_end(b->txt); b->txt->ptbl.dirty = 0; txt_hist_push(b->txt, b->cur); return e->bufn++; } void ed_buf_free(Editor *e, EditBuf *b) { txt_free(b->txt); FREELIST_FREE(&e->txt_free, b->txt); arena_free(&b->arena); } void ed_buf_change_path(Editor *e, u32 i, Str s) { EditBuf *eb = &e->buf[i]; eb->path = str_dup(s, &eb->arena); eb->type = ED_BUF_FILE; } void ed_init(Editor *e) { memset(e, 0, sizeof(Editor)); e->scratch = arena_init(1L << 30); e->perm = arena_init(1L << 30); e->bufi = ed_buf_open(e, NULL); Str s = S("(Scratch buffer, type whatever)"); e->buf[e->bufi].cur = txt_insert(e->buf[e->bufi].cur, s.s, s.n); txt_hist_push(e->buf[e->bufi].txt, e->buf[e->bufi].cur); } void ed_fini(Editor *e) { for (u32 i = 0; i < e->bufn; i++) ed_buf_free(e, &e->buf[i]); if (e->search.s) free(e->search.s); if (e->yank.s) free(e->yank.s); arena_free(&e->scratch); arena_free(&e->perm); } u32 ed_buf_close(Editor *e, u32 i) { ed_buf_free(e, &e->buf[i]); if (i + 1 < e->bufn) MOVE(&e->buf[i], &e->buf[i+1], e->bufn - (i + 1)); e->bufn--; return i > 0 ? i - 1 : 0; } Str str_printf(Arena *a, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); va_start(ap, fmt); char *buf = new_arr(a, char, n + 1); vsnprintf(buf, n + 1, fmt, ap); va_end(ap); return (Str) { buf, n }; } int ed_buf_save(Editor *e, u32 i) { EditBuf *b = &e->buf[i]; if (b->type == ED_BUF_FILE) { e->msg = str_printf(&e->scratch, "%.02fk written", b->txt->len / 1024.0); return txt_save(b->txt, str_to_cstr(b->path, &e->scratch)); } else { e->msg = S("unwriteable file type"); return 0; } } static inline int is_space(u32 c) { return c <= 0x20 && c != 0; } static inline int is_word_chr(u32 c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } TxtLoc next_word(TxtLoc l) { while (!at_end(l) && !is_word_chr(txt_chr(l))) l = cnext(l); while (!at_end(l) && is_word_chr(txt_chr(l))) l = cnext(l); return l; } TxtLoc next_bigword(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_word_chr(txt_chr(n))) break; l = n; } while (!at_start(l)) { TxtLoc n = bprev(l); if (!is_word_chr(txt_chr(n))) break; l = n; } return l; } TxtLoc prev_bigword(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; } TxtLoc word_start(TxtLoc l) { while (!at_start(l) && is_word_chr(txt_chr(l))) l = cprev(l); while (!at_start(l) && !is_word_chr(txt_chr(l))) l = cnext(l); return l; } TxtLoc word_end(TxtLoc l) { while (!at_start(l) && is_word_chr(txt_chr(l))) l = cnext(l); while (!at_start(l) && !is_word_chr(txt_chr(l))) l = cprev(l); return l; } TxtLoc bigword_start(TxtLoc l) { while (!at_start(l) && !is_space(txt_chr(l))) l = cprev(l); while (!at_start(l) && is_space(txt_chr(l))) l = cnext(l); return l; } TxtLoc bigword_end(TxtLoc l) { while (!at_start(l) && !is_space(txt_chr(l))) l = cnext(l); while (!at_start(l) && is_space(txt_chr(l))) l = cprev(l); return l; } static inline u32 bracket_opp(u32 c) { switch (c) { case '{': return '}'; case '}': return '{'; case '[': return ']'; case ']': return '['; case '(': return ')'; case ')': return '('; case '<': return '>'; case '>': return '<'; default: return c; } } static inline int bracket_dir(u32 c) { switch (c) { case '{': case '[': case '(': case '<': return 1; case '}': case ']': case ')': case '>': return -1; default: return 0; } } /* TODO: reintroduce match_func_end_to-start */ int match_bracket(TxtLoc l, TxtLoc *out, u32 c) { u32 depth = 1; u32 o = bracket_opp(c); if (txt_chr(l) != c) return 0; int dir = bracket_dir(c); if (!dir) return 0; while (depth > 0) { if (dir < 0 && at_start(l)) break; if (dir > 0 && at_end(l)) break; l = dir < 0 ? cprev(l) : cnext(l); u32 x = txt_chr(l); depth += (x == c) - (x == o); } if (depth == 0) { *out = l; return 1; } else { return 0; } } static inline TxtLoc inner_line_end(TxtLoc l) { l = end_of_line(l); if (txt_chr(cprev(l)) != '\n') l = cprev(l); return l; } static inline int is_func_start(TxtLoc l) { return txt_chr(inner_line_end(l)) == '{' && !is_space(txt_chr(start_of_line(l))); } static inline int is_func_end(TxtLoc l) { TxtLoc n = cnext(l); return txt_chr(l) == '}' && (txt_chr(n) == '\n' || at_end(n)); } int next_func_end(TxtLoc l, TxtLoc *out) { l = cnext(l); for (;;) { TxtLoc n = next_line_start(l); if (!txt_before(l, n)) break; if (is_func_end(l)) { *out = l; return 1; } l = n; } return 0; } int prev_func_end(TxtLoc l, TxtLoc *out) { l = cprev(l); while (!at_start(l)) { l = prev_line_start(l); if (is_func_end(l)) { *out = l; return 1; } } return 0; } int empty_line(TxtLoc l) { u8 b = txt_byte(start_of_line(l)); return b == '\n' || b == 0 /* last line of buffer */ ; } /* TODO: support having a newline before the open brace */ static inline int func_end_to_start(TxtLoc l, TxtLoc *out) { while (!at_start(l) && !is_func_start(l)) l = prev_line_start(l); if (txt_chr(start_of_line(l)) == '{') l = start_of_line(cprev(l)); l = end_of_line(l); *out = l; return 1; } int prev_func(TxtLoc start, TxtLoc *out) { TxtLoc l; if (prev_func_end(start, &l) && func_end_to_start(l, &l)) { *out = l; return 1; } return 0; } int next_func(TxtLoc start, TxtLoc *out) { start = end_of_line(start); TxtLoc l, k; if (!next_func_end(start, &l)) return 0; if (!func_end_to_start(l, &k)) return 0; if (!txt_before(start, k) && !next_func_end(l, &l)) return 0; return func_end_to_start(l, out); } TxtLoc next_par(TxtLoc l) { while (!at_end(l) && empty_line(l)) l = next_newline(l); while (!at_end(l) && !empty_line(l)) l = next_newline(l); return l; } TxtLoc prev_par(TxtLoc l) { while (!at_start(l) && empty_line(l)) l = prev_newline(l); while (!at_start(l) && !empty_line(l)) l = prev_newline(l); return l; } TxtLoc txt_at_line(Txt *t, u32 line) { TxtLoc l = txt_start(t); while (line-- > 1) l = next_line(l); return l; } 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 ed_buf_shell_replace(Editor *e, u32 bufi, 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, &e->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, &e->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})) { EditBuf *eb = &e->buf[bufi]; u32 b = txt_ofs(eb->cur); start = txt_delete_range(start, end); end = txt_insert(start, rds.v, rds.n); eb->cur = txt_at(start.t, b); } return 0; } int shell_replace(TxtLoc start, TxtLoc end, const char *cmd) { return ed_buf_shell_replace(&e, e.bufi, start, end, cmd); } #define ANY_KEY_PROMPT "[Press any key to resume editing]" static void press_any_key(void) { printf("%s", ANY_KEY_PROMPT); fflush(stdout); vui_enable(); vui_key(); vui_redraw(); } const char *expand_cmd(const char *src) { DYNARR(char) s = { 0 }; u32 n = strlen(src); for (u32 i = 0; i < n; i++) { if (src[i] == '%') { i++; if (src[i] == '%') { DA_APUSH(&s, &e.scratch, '%'); } else { DA_APUSH_MULT(&s, &e.scratch, e.buf[e.bufi].path.s, e.buf[e.bufi].path.n); if (i < n) { DA_APUSH(&s, &e.scratch, src[i]); } } } else { DA_APUSH(&s, &e.scratch, src[i]); } } DA_APUSH(&s, &e.scratch, '\0'); return s.v; } int shell_run(const char *cmd) { vui_disable(); int r = system(expand_cmd(cmd)); press_any_key(); return r; } int shell_run_no_prompt(const char *cmd) { vui_disable(); int r = system(expand_cmd(cmd)); vui_enable(); vui_redraw(); return r; } /* main */ void find_view_window(TxtLoc l, TxtLoc *start, TxtLoc *end, u32 lines) { 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; } static Str basename(Str s) { for (;;) { Cut c = str_cut(s, '/'); if (!c.tail.n) return c.head; s = c.tail; } } static Str dirname(Str s) { while (s.n > 1 && s.s[s.n - 1] == '/') s.n--; while (s.n > 0 && s.s[s.n - 1] != '/') s.n--; while (s.n > 1 && s.s[s.n - 1] == '/') s.n--; return s; } #define STYLE_BUF (A_DEFAULT) #define STYLE_BUF_SEL (STYLE_BUF | A_REVERSE) #define STYLE_TXT (A_DEFAULT) #define STYLE_TRAILSP (BG_BLUE | FG_DEFAULT) void draw_status_line(Editor *e) { int x = 0; int y = 0; x += vui_aprintf(x, y, STYLE_BUF, " %s ", mode_key_str[e->mode]); for (u32 i = 0; i < e->bufn; i++) { EditBuf *b = &e->buf[i]; VuiAttr a = i == e->bufi ? STYLE_BUF_SEL : STYLE_BUF; Str p = basename(b->path); x += vui_aprintf(x, y, a, " %.*s", (int)p.n, p.s); if (b->type != ED_BUF_SCRATCH && b->txt->ptbl.dirty) vui_chra(x++, y, '*', a); vui_chra(x++, y, ' ', a); } int n = COLS; if (e->msg.n) { x += vui_aprintf(x, y, STYLE_BUF, " %.*s", (int)e->msg.n, e->msg.s); e->msg = (Str) { 0, 0 }; } while (x < n) vui_chra(x++, y, ' ', STYLE_BUF); } void draw_buf(EditBuf *eb) { u32 lmarg = 0; u32 tmarg = 1; u32 bmarg = 0; u32 x = lmarg, y = tmarg; VuiAttr txt = STYLE_TXT; if (eb->txt->readonly) txt = FG_CYAN | BG_DEFAULT; vui_clear(); draw_status_line(&e); TxtLoc l = eb->cur; int cur_found = 0; TxtLoc start, end; find_view_window(eb->cur, &start, &end, LINES - (tmarg + bmarg)); while (txt_before(start, end)) { if (l.p == start.p && l.i == start.i) { cur_found = 1; if (txt_chr(start) == '\t') { vui_curs_pos(x + (-(x+1) & 7), y); } else { vui_curs_pos(x, y); } } u32 c = txt_chr_next(&start); if (c == '\n') { x = lmarg; y++; } else if (c == '\n' || c == '\t' || c == ' ') { int trail = txt_chr(start) == '\n' && !(l.p == start.p && l.i == start.i); VuiAttr a = trail ? STYLE_TRAILSP : txt; if (c == '\t') { u32 n = 1 + (-(x+1) & 7); while (n--) vui_chra(x++, y, ' ', a); } else { vui_chra(x++, y, ' ', a); } } else if (c < 0x20 || c == 0x7f) { x += vui_aprintf(x, y, FG_CYAN, "<%02x>", c); } else if (c) { vui_chra(x++, y, c, txt); } else if (x >= COLS) { start = next_line_start(start); x = lmarg; y++; } } ASSERT(start.i <= eb->txt->ptbl.v[start.p].n); if (!cur_found) vui_curs_pos(x, y); switch (e.mode) { case MODE_NORMAL: vui_curs_shape(VUI_CURS_BLOCK); break; case MODE_INSERT: vui_curs_shape(VUI_CURS_BAR); break; case MODE_REPLACE_ONE: case MODE_REPLACE_MULT: vui_curs_shape(VUI_CURS_UNDERLINE); break; } if (e.input_line.n > 0 || e.input_prompt.n > 0) { VuiAttr a = STYLE_BUF; u32 x = 0; u32 y = LINES - 1; x += vui_putsna(0, y, e.input_prompt.s, e.input_prompt.n, a); x += vui_putsna(x, y, e.input_line.s, e.input_line.n, a); vui_curs_pos(x, y); vui_curs_shape(VUI_CURS_BAR); while (x < COLS) vui_chra(x++, y, ' ', a); } } void draw_opts(Str *optv, u32 optc, u32 *optvi, u32 opti) { u32 x = 0; u32 y = 0; VuiAttr a = A_DEFAULT | A_ITALIC; vui_clear(); x += vui_putsna(0, y, e.input_prompt.s, e.input_prompt.n, a); x += vui_putsna(x, y, e.input_line.s, e.input_line.n, a); vui_curs_pos(x, y); vui_curs_shape(VUI_CURS_BAR); y++; u32 n = LINES; u32 tn = n / 2; u32 top = opti > tn ? opti - tn : 0; u32 bot = top + n > optc ? optc : top + n; for (u32 i = top; i < bot; i++) { VuiAttr a = A_DEFAULT; if (i == opti) a |= A_REVERSE; vui_putsna(2, y++, optv[optvi[i]].s, optv[optvi[i]].n, a); } } void draw(void *ctx) { (void)ctx; if (e.optv) { draw_opts(e.optv, e.optc, e.optvi, e.opti); } else { draw_buf(&e.buf[e.bufi]); } size_t max = e.scratch.end - e.scratch.start; size_t used = e.scratch.beg - e.scratch.start; vui_printf(-1, -1, "%.02fk (%u%%)", used/1024.0, (used * 100) / max); //FILE *f = fopen("scratch.dat", "wb"); //fwrite(e.scratch.start, 1, used, f); //fclose(f); } TxtLoc logical_line_start(TxtLoc l) { l = start_of_line(l); while (txt_chr(l) == '\t') l = cnext(l); return l; } typedef struct { Arena *a; Str prompt; DYNARR(char) s; } LineEditor; typedef enum { LEDIT_EMPTY, LEDIT_DONE, LEDIT_CONT, } LineEditStatus; static LineEditor line_editor(Str prompt, Arena *a) { e.input_line = (Str) { 0, 0 }; e.input_prompt = prompt; return (LineEditor) { a, prompt, { 0 }}; } static LineEditStatus line_edit(LineEditor *le, u32 c) { switch (c) { case '\r': e.input_line = (Str) { 0, 0 }; e.input_prompt = (Str) { 0, 0 }; return LEDIT_DONE; case KEY_ESC: e.input_line = (Str) { 0, 0 }; e.input_prompt = (Str) { 0, 0 }; return LEDIT_EMPTY; case KEY_BKSP: while (le->s.n > 0 && (le->s.v[le->s.n-1] & 0xc0) == 0x80) le->s.n--; if (le->s.n > 0) le->s.n--; break; case 0x17 /* ^W */: while (le->s.n > 0 && is_space(le->s.v[le->s.n-1])) le->s.n--; while (le->s.n > 0 && !is_space(le->s.v[le->s.n-1])) le->s.n--; break; default: if (c > 0 && c <= KEY_UTF8_MAX && c >= ' ') { u32 n = utf8_encode_len(&c, 1); DA_AFIT(&le->s, le->a, le->s.n + n); utf8_encode(&le->s.v[le->s.n], &c, 1); le->s.n += n; } e.opti = 0; break; } e.input_line = (Str) { le->s.v, le->s.n }; return LEDIT_CONT; } Str get_input_line(Str prompt) { LineEditor le = line_editor(prompt, &e.scratch); for (;;) { draw(NULL); vui_blit(); LineEditStatus st = line_edit(&le, vui_key()); if (st == LEDIT_EMPTY) return (Str) { 0, 0 }; if (st == LEDIT_DONE) return (Str) { le.s.v, le.s.n }; } } int search_next_regex(TxtLoc l, Str src, TxtLoc *out) { RegEx re = { 0 }; ReSearch s = { 0 }; int err = re_comp_ex(&re, src, &e.scratch, &e.scratch, RE_COMP_NO_GROUPS); if (err) { e.msg = str_printf(&e.scratch, "Regex error: %s", re_comp_strerror(err)); return 0; } int match_found = 0; search_from_start:; TxtLoc t = l; re_search_start(&s, &re, &e.scratch); if (at_start(t) || txt_chr(cprev(t)) == '\n') re_search_first_chunk(&s); while (!at_end(t)) { Str chnk = txt_next_chunk(&t); re_search_chunk(&s, chnk.s, chnk.n); if (at_end(t)) re_search_last_chunk(&s); ReMatch m; if (re_search_match(&s, &m)) { while (m.extent.start--) l = bnext(l); *out = l; return 1; } } if (match_found == 0) { match_found = -1; l = txt_start(l.t); e.msg = S("wrapped"); goto search_from_start; } e.msg = S("no match"); return 0; } int search_prev_regex(TxtLoc start, Str src, TxtLoc *out) { RegEx re = { 0 }; ReSearch s = { 0 }; int err = re_comp_ex(&re, src, &e.scratch, &e.scratch, RE_COMP_NO_GROUPS); if (err) { e.msg = str_printf(&e.scratch, "Regex error: %s", re_comp_strerror(err)); return 0; } int match_found = 0; TxtLoc end = txt_end(start.t); TxtLoc l = start; search_from_end: for (;;) { ReMatch m; Arena a = e.scratch; re_search_start(&s, &re, &e.scratch); TxtLoc t = l; if (at_start(t) || txt_chr(cprev(t)) == '\n') re_search_first_chunk(&s); while (!at_end(t)) { Str chnk = txt_next_chunk(&t); re_search_chunk(&s, chnk.s, chnk.n); if (at_end(t)) re_search_last_chunk(&s); if (re_search_match_at_start(&s, &m)) { *out = l; return 1; } if (~s.flags & RE_SEARCH_MID_MATCH) { if (chnk.n > 0) break; if (txt_after(t, end)) break; /* ^ how would this ever happen? */ } } e.scratch = a; if (at_start(l)) break; l = cprev(l); } if (match_found == 0) { match_found = -1; l = txt_end(l.t); end = start; e.msg = S("wrapped"); goto search_from_end; } e.msg = S("no match"); return 0; } int read_search(void) { Str src = get_input_line(S("Search: ")); if (!src.n) return 0; e.search.s = realloc(e.search.s, src.n); if (!e.search.s) FAIL_WITH_MSG("failed to allocate search"); memcpy(e.search.s, src.s, src.n); e.search.n = src.n; return 1; } int motion(TxtLoc *lp, u32 c) { TxtLoc l = *lp; TxtLoc last_loc = l; for (;;) { switch (c) { case '0': if (!e.count) goto loop; /* fallthrough */ case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': e.count = (e.count % 100000000) * 10 + c - '0'; break; case KEY_ESC: e.count = 0; return 0; default: goto loop; } draw(NULL); vui_blit(); c = vui_key(); } loop: switch (c) { case KEY_LEFT: case 'h': l = cprev(l); break; case KEY_RIGHT: case 'l': l = cnext(l); break; case KEY_DOWN: case 'j': l = next_line(l); break; case KEY_UP: case 'k': l = prev_line(l); break; case 'K': { TxtRange r = txt_range_incl(word_start(l), word_end(l)); Str s = txt_collect_range( (TxtLoc) { r.t, r.p0, r.i0 }, (TxtLoc) { r.t, r.p1, r.i1 }, &e.scratch ); Str cmd = S("man "); str_cat(&cmd, s, &e.scratch); shell_run_no_prompt(str_to_cstr(cmd, &e.scratch)); } 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 'B': l = prev_bigword(l); break; case 'W': l = next_bigword(l); break; case 'e': l = word_end(l); break; case 'E': l = bigword_end(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 '[': if (!prev_func(l, &l)) return 0; break; case ']': if (!next_func(l, &l)) return 0; break; case '%': if (!match_bracket(l, &l, txt_chr(l))) return 0; 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 '^': l = logical_line_start(l); break; 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': if (e.count) { l = txt_at_line(l.t, e.count); e.count = 0; } else { l = txt_start(l.t); } break; case KEY_END | KEY_CTRL_BIT: case 'G': if (e.count) { l = txt_at_line(l.t, e.count); e.count = 0; } else { l = txt_end(l.t); } break; case 'f': e.search_char_incl = 1; e.search_char_dir = 1; e.search_char = vui_key(); goto repeat_char_search; case 'F': e.search_char_incl = 1; e.search_char_dir = -1; e.search_char = vui_key(); goto repeat_char_search; case 't': e.search_char_incl = 0; e.search_char_dir = 1; e.search_char = vui_key(); goto repeat_char_search; case 'T': e.search_char_incl = 0; e.search_char_dir = -1; e.search_char = vui_key(); goto repeat_char_search; case ';': { repeat_char_search:; int found = 0; TxtLoc t = l; if (!e.search_char_incl) { if (e.search_char_dir < 0) { if (at_start(t)) break; t = cprev(t); } else { if (at_end(t)) break; t = cnext(t); } } for (;;) { TxtLoc p = t; if (e.search_char_dir < 0) { if (at_start(t)) break; t = cprev(t); } else { if (at_end(t)) break; t = cnext(t); } u32 x = txt_chr(t); if (x == '\n' || x == 0) break; if (x == e.search_char) { found = 1; if (e.search_char_incl) l = t; else l = p; break; } } if (!found) return 0; } break; case '/': if (read_search()) { if (!search_next_regex(l, e.search, &l)) return 0; e.search_dir = 1; } break; case '?': if (read_search()) { if (!search_prev_regex(l, e.search, &l)) return 0; e.search_dir = -1; } break; case 'n': if (!e.search.n) return 0; if (e.search_dir > 0 && !search_next_regex(cnext(l), e.search, &l)) return 0; if (e.search_dir < 0 && !search_prev_regex(cprev(l), e.search, &l)) return 0; break; case 'N': if (!e.search.n) return 0; if (e.search_dir < 0 && !search_next_regex(cnext(l), e.search, &l)) return 0; if (e.search_dir > 0 && !search_prev_regex(cprev(l), e.search, &l)) return 0; break; default: return 0; } if (e.count > 1 && (txt_before(l, last_loc) || txt_after(l, last_loc))) { e.count--; goto loop; } e.count = 0; *lp = l; return 1; } TxtLoc ins_newline(TxtLoc l) { u32 tabs = 0; TxtLoc start = start_of_line(l); for (TxtLoc t = start; txt_byte(t) == '\t'; t = bnext(t)) tabs++; while (txt_byte(cprev(l)) == '\t') l = txt_delete(l, 1); l = txt_insert_c(l, '\n'); while (tabs--) l = txt_insert_c(l, '\t'); return l; } static Str normalize_path(Str s, Arena *a) { Arena m = *a; char *p = realpath(str_to_cstr(s, a), NULL); *a = m; s = str_dup(str_from_cstr(p), a); free(p); return s; } static Str shorten_path(Str s, Arena *a) { if (str_starts(s, e.homedir)) { Str sub = str_skip(s, e.homedir.n); s = S("~"); str_cat(&s, sub, a); } return s; } static void build_file(Str path, int debugp) { (void)path; shell_run(debugp ? "make DEBUG=1" : "make"); } #define WED_DEV_DIR "/mnt/user/wrmr/src/wed/" #define WED_DEV_EXE WED_DEV_DIR "wed" int is_wed_path(Str path) { return str_starts(path, S(WED_DEV_DIR)); } void str_cat_u32(Str *out, u32 c, Arena *a) { char buf[32]; u32 n = 0; do { buf[32 - ++n] = (c % 10) + '0'; c /= 10; } while (c); str_cat(out, (Str) { &buf[sizeof(buf) - n], n }, a); } int str_to_u32(Str s, u32 *out) { u32 x = 0; for (u32 i = 0; i < s.n; i++) { char c = s.s[i]; if (!(c >= '0' && c <= '9')) return -1; x = (x * 10) + c - '0'; } *out = x; return 0; } void make_edit_args(Str *a) { for (u32 i = 0; i < e.bufn; i++) { if (e.buf[i].type != ED_BUF_FILE && e.buf[i].type != ED_BUF_BIN_FILE) continue; if (i > 0) str_catc(a, ' ', &e.scratch); str_cat(a, e.buf[i].path, &e.scratch); str_cat(a, S("@"), &e.scratch); str_cat_u32(a, txt_ofs(e.buf[i].cur), &e.scratch); if (i == e.bufi) str_cat(a, S(" -b"), &e.scratch); } } int parse_edit_args(Editor *e, int argc, const char **argv) { if (argc < 2) return 0; u32 bufi = ed_buf_close(e, 0); /* close scratch buffer */ for (int i = 1; i < argc; i++) { Str s = str_from_cstr(argv[i]); if (str_eql(s, S("-b"))) { bufi = e->bufi; continue; } Cut c = str_cut(s, '@'); e->bufi = ed_buf_open(e, str_to_cstr(c.head, &e->scratch)); if (c.tail.n > 0) { EditBuf *eb = &e->buf[e->bufi]; u32 ofs; if (str_to_u32(c.tail, &ofs)) { fprintf(stderr, "invalid byte offset in %s\n", argv[i]); return 1; } eb->cur = txt_at(eb->txt, ofs); } } e->bufi = bufi; return 0; } static void run_file(Str path, int debugp) { if (is_wed_path(path)) { vui_disable(); if (system(debugp ? "make DEBUG=1" : "make")) { press_any_key(); } else { vui_fini(); Str cmd = S("exec " WED_DEV_EXE " "); make_edit_args(&cmd); execl("/bin/sh", "sh", "-c", str_to_cstr(cmd, &e.scratch), 0); } } else { shell_run(debugp ? "make DEBUG=1 run" : "make run"); } } static void debug_file(Str path) { if (is_wed_path(path)) { vui_disable(); if (system("make DEBUG=1")) { press_any_key(); } else { vui_fini(); Str cmd = S("exec gdb -ex run --args " WED_DEV_EXE " "); make_edit_args(&cmd); execl("/bin/sh", "sh", "-c", str_to_cstr(cmd, &e.scratch), 0); } } else { Str cmd = S("gdb -ex run --args "); str_cat(&cmd, path, &e.scratch); shell_run(str_to_cstr(cmd, &e.scratch)); } } static TxtLoc indent_dedent_lines(TxtLoc start, TxtLoc end, int n) { if (txt_before(end, start)) { TxtLoc t = start; start = end; end = t; } u32 ofs = txt_ofs(start); start = start_of_line(start); u32 lines = 0; for (TxtLoc t = start; txt_before(t, end); t = next_line_start(t)) lines++; while (lines--) { if (n > 0) { if (!empty_line(start)) for (u32 j = n; j--;) start = txt_insert_c(start, '\t'); } else { for (u32 j = n; j++ && txt_byte(start) == '\t';) start = txt_delete_c(cnext(start)); } start = next_line_start(start); } return txt_at(start.t, ofs); } int *opt_sort_scr; static inline char to_lower(char c) { /* TODO: consider utf-8 support */ if (c >= 'A' && c <= 'Z') return c + ('a' - 'A'); return c; } static inline int fz_start_score(Str s, Str p) { if (!p.n || str_eql(s, p)) return 0; int n = 1; u32 pi = 0, si = 0; for (;;) { if (to_lower(p.s[pi]) == to_lower(s.s[si])) { si++; pi++; if (si == s.n) { return pi < p.n ? -1 : n; } else if (pi == p.n) { return n; } } else { si++; n++; if (si == s.n) return -1; } } } typedef struct { u16 score, start; } FuzzyScore; static inline FuzzyScore fz_score(Str s, Str p) { if (!p.n) return (FuzzyScore) { 0, 0 }; int score = -1; int score_start = 0; for (u32 i = 0; i < s.n; i++) { int f = fz_start_score(str_skip(s, i), p); if (f == -1) continue; if (score == -1 || f < score) { score = f; score_start = i; } } if (score == -1) return (FuzzyScore) { 0xffff, 0 }; return (FuzzyScore) { score, score_start }; } static inline int fz_score_cmp(FuzzyScore a, FuzzyScore b) { if (a.score < b.score) return -1; if (b.score < a.score) return 1; return (b.start < a.start) - (a.start < b.start); } void qsort_opt(u32 *dest, FuzzyScore *scr, int n) { int i = 0, j = n - 1; FuzzyScore x = scr[n / 2]; do { while (fz_score_cmp(scr[i], x) < 0) i++; while (fz_score_cmp(x, scr[j]) < 0) j--; if (i <= j) { int t0 = dest[i]; dest[i] = dest[j]; dest[j] = t0; FuzzyScore t1 = scr[i]; scr[i] = scr[j]; scr[j] = t1; i++; j--; } } while (i <= j); if (0 < j) qsort_opt(dest, scr, j + 1); if (i+1 < n) qsort_opt(dest + i, scr + i, n - i); } u32 sort_opt_idx(u32 *dest, Str *src, u32 n, Str pat, Arena *scratch) { FuzzyScore *scr = new_arr(scratch, FuzzyScore, n); u32 scrn = 0; for (u32 i = 0; i < n; i++) { FuzzyScore s = fz_score(src[i], pat); if (s.score != 0xffff) { scr[scrn] = s; dest[scrn] = i; scrn++; } } if (pat.n > 0) qsort_opt(dest, scr, scrn); return scrn; } static inline int umodi(int n, int d) { return (d + n%d) % d; } int select_opt_at(Str *optv, u32 optc, Str prompt, u32 opti) { if (!optc) { e.msg = S("no options"); return -1; } e.optc = optc; e.optvi = new_arr(&e.scratch, u32, optc); e.optv = optv; e.opti = opti; LineEditor le = line_editor(prompt, &e.scratch); int r = -1; for (;;) { Arena s = e.scratch; e.optc = sort_opt_idx(e.optvi, optv, optc, e.input_line, &e.scratch); draw(NULL); vui_blit(); e.scratch = s; u32 c = vui_key(); switch (c) { case KEY_DOWN: e.opti = umodi(e.opti + 1, e.optc); break; case KEY_UP: e.opti = umodi(e.opti - 1, e.optc); break; case KEY_PGDN: e.opti = umodi(e.opti + LINES / 3, e.optc); break; case KEY_PGUP: e.opti = umodi(e.opti - LINES / 3, e.optc); break; default: { LineEditStatus st = line_edit(&le, c); if (st == LEDIT_EMPTY) goto done; if (st == LEDIT_DONE) { r = e.optvi[e.opti]; goto done; } } break; } } done: e.optc = 0; e.optv = NULL; return r; } int select_opt(Str *optv, u32 optc, Str prompt) { return select_opt_at(optv, optc, prompt, 0); } #include Str select_file_in(const char *path) { DIR *d = opendir(path); if (!d) return (Str) { 0, 0 }; Arena perm = e.perm; Arena scratch = e.scratch; again:; struct dirent *de; DYNARR(Str) opt = { 0 }; u32 dirs = 0; while (readdir(d)) dirs++; DA_AFIT(&opt, &e.scratch, dirs); if (strcmp(path, "/")) DA_APUSH(&opt, &e.scratch, S("..")); seekdir(d, 0); while ((de = readdir(d))) { if (de->d_name[0] == '.') continue; Str dn = str_from_cstr(de->d_name); if (str_ends(dn, S(".o"))) continue; Str n = str_dup(dn, &e.scratch); if (de->d_type == DT_DIR) str_catc(&n, '/', &e.scratch); DA_APUSH(&opt, &e.scratch, n); } Str prompt = shorten_path(str_from_cstr(path), &e.scratch); str_cat(&prompt, S(": "), &e.scratch); int o = select_opt(opt.v, opt.n, prompt); if (o == -1) return (Str) { 0, 0 }; Str s = str_from_cstr(path); str_catc(&s, '/', &e.scratch); str_cat(&s, opt.v[o], &e.scratch); s = normalize_path(s, &e.scratch); e.perm = perm; path = str_to_cstr(s, &e.scratch); d = opendir(path); if (d) { path = cstr_dup(path, &e.perm); e.scratch = scratch; goto again; } return s; } Str select_file(void) { Str path = { 0 }; EditBuf *eb = &e.buf[e.bufi]; if (eb->type == ED_BUF_FILE || eb->type == ED_BUF_BIN_FILE) { path = dirname(eb->path); } else { path = normalize_path(S("."), &e.scratch); } Str s = select_file_in(str_to_cstr(path, &e.scratch)); return s; } int select_buf(void) { DYNARR(Str) opt = { 0 }; DA_AFIT(&opt, &e.scratch, e.bufn); opt.n = e.bufn; for (u32 i = 0; i < e.bufn; i++) opt.v[i] = shorten_path(e.buf[i].path, &e.scratch); int o = select_opt_at(opt.v, opt.n, S("Buffer: "), e.bufi); if (o == -1) return -1; return o; } int select_func(Txt *t, TxtLoc *out) { TxtLoc l = txt_start(t); DYNARR(TxtLoc) loc = { 0 }; while (next_func(l, &l)) DA_APUSH(&loc, &e.scratch, l); Str *opt = new_arr(&e.scratch, Str, loc.n); for (u32 i = 0; i < loc.n; i++) { opt[i] = txt_collect_range(start_of_line(loc.v[i]), end_of_line(loc.v[i]), &e.scratch); ASSERT(opt[i].n > 0); if (opt[i].s[opt[i].n-1] == '{') opt[i].n--; while (opt[i].n > 0 && is_space(opt[i].s[opt[i].n - 1])) opt[i].n--; opt[i].s[opt[i].n++] = ';'; } int o = select_opt(opt, loc.n, S("Function: ")); if (o == -1) return 0; *out = loc.v[o]; return 1; } /* TODO: int select_type(Txt *t, TxtLoc *out); */ void yank_range(Editor *e, TxtLoc start, TxtLoc end) { if (txt_before(end, start)) { TxtLoc t = start; start = end; end = t; } Str s = txt_collect_range(start, end, &e->scratch); if (!s.n) return; e->yank.s = realloc(e->yank.s, s.n); if (!e->yank.s) FAIL_WITH_MSG("couldn't reallocate clipboard"); memcpy(e->yank.s, s.s, s.n); e->yank.n = s.n; } void mode_key_normal(Editor *e, u32 c) { EditBuf *eb = &e->buf[e->bufi]; switch (c) { case 'q': e->bufi = ed_buf_close(e, e->bufi); break; case 'Q': while (e->bufn > 0) e->bufi = ed_buf_close(e, e->bufi); break; case 'H': if (e->bufi > 0) e->bufi = e->bufi - 1; else e->bufi = e->bufn - 1; break; case 'L': e->bufi = (e->bufi + 1) % e->bufn; break; case 'z': case 'Z': case 0x13 /* ^S */: ed_buf_save(e, e->bufi); if (c == 'Z') e->bufi = ed_buf_close(e, e->bufi); break; case 'i': e->mode = 1; break; case 'a': { e->mode = 1; TxtLoc l = eb->cur; TxtLoc e = end_of_line(l); l = txt_before(l, e) ? cnext(l) : e; eb->cur = l; } break; case 'I': e->mode = 1; eb->cur = logical_line_start(eb->cur); break; case 'A': e->mode = 1; eb->cur = end_of_line(eb->cur); break; case 'o': e->mode = 1; eb->cur = ins_newline(end_of_line(eb->cur)); break; case 'O': e->mode = 1; eb->cur = ins_newline(bprev(start_of_line(eb->cur))); if (at_start(bprev(eb->cur))) eb->cur = bprev(eb->cur); break; case 'p': eb->cur = cprev(txt_insert(cnext(eb->cur), e->yank.s, e->yank.n)); break; case 'P': eb->cur = txt_insert(eb->cur, e->yank.s, e->yank.n); for (u32 i = 0; i < e->yank.n; i++) eb->cur = cprev(eb->cur); break; case 'd': { TxtLoc before = eb->cur; if (motion(&eb->cur, vui_key())) { yank_range(e, before, eb->cur); eb->cur = txt_delete_range(before, eb->cur); } } break; case '!': { TxtLoc before = eb->cur, after = eb->cur; if (motion(&after, vui_key())) { Str s = get_input_line(S("Command: ")); if (s.n > 0) { yank_range(e, before, after); shell_replace(before, after, str_to_cstr(s, &e->scratch)); } } } break; case 'y': { TxtLoc l = eb->cur; if (motion(&l, vui_key())) yank_range(e, eb->cur, l); } break; case 'D': yank_range(e, eb->cur, end_of_line(eb->cur)); eb->cur = txt_delete_range(eb->cur, end_of_line(eb->cur)); break; case 'C': mode_key_normal(e, 'D'); e->mode = 1; break; case 'S': { TxtLoc start = start_of_line(eb->cur); TxtLoc end = end_of_line(eb->cur); yank_range(e, start, end); eb->cur = txt_delete_range(start, end); e->mode = 1; } break; case 'J': { TxtLoc l = end_of_line(eb->cur); if (txt_byte(l) == '\n') { do l = txt_delete_c(cnext(l)); while (is_space(txt_chr(l))); l = cprev(txt_insert_c(l, ' ')); eb->cur = l; } } break; case 'c': { TxtLoc before = eb->cur; if (motion(&eb->cur, vui_key())) { yank_range(e, before, eb->cur); eb->cur = txt_delete_range(before, eb->cur); e->mode = 1; } break; } case 'x': if (!at_end(eb->cur)) { yank_range(e, eb->cur, cnext(eb->cur)); eb->cur = txt_delete_c(cnext(eb->cur)); } break; case 's': mode_key_normal(e, 'x'); e->mode = 1; break; case 'r': e->mode = MODE_REPLACE_ONE; break; case 'R': e->mode = MODE_REPLACE_MULT; break; case 'M': { TxtLoc start = prev_par(eb->cur); TxtLoc end = next_par(eb->cur); if (shell_replace(start, end, "fmt -w80 -u")) { err(1, "shell_replace"); } } break; case ' ': switch ((u32)vui_key()) { case 'f': { Str s = select_file(); if (s.n > 0) e->bufi = ed_buf_open(e, str_to_cstr(s, &e->scratch)); } break; case 'F': { Str s = select_file(); if (s.n > 0) { e->bufi = ed_buf_close(e, e->bufi); e->bufi = ed_buf_open(e, str_to_cstr(s, &e->scratch)); } } break; case 's': { int b = select_buf(); if (b != -1) e->bufi = b; } break; case 'j': { TxtLoc l; if (select_func(eb->txt, &l)) eb->cur = l; } break; case 'm': build_file(eb->path, 1); break; case 'M': build_file(eb->path, 0); break; case 'r': run_file(eb->path, 1); break; case 'R': run_file(eb->path, 0); break; case 'D': debug_file(eb->path); break; case 'e': shell_run_no_prompt("make clean"); break; case 'l': shell_run_no_prompt("git log --oneline %"); break; case 'L': shell_run_no_prompt("git log %"); break; case 'd': shell_run_no_prompt("git diff --color=always % | less -cr"); break; case 'a': shell_run_no_prompt("git add %"); break; case 'c': shell_run_no_prompt("git commit"); break; case 'S': shell_run("git status"); break; default: /* TODO: flash */ break; } break; case '<': case '>': { TxtLoc end = eb->cur; VuiKey k = vui_key(); if (k == '<' || k == '>' || motion(&end, k)) { if (k == '<' || k == '>') end = next_line_start(end); eb->cur = indent_dedent_lines(eb->cur, end, c == '<' ? -1 : 1); } } break; case '=': { Str s = get_input_line(S("File to open: ")); if (s.n > 0) e->bufi = ed_buf_open(e, str_to_cstr(s, &e->scratch)); } break; case '+': { Str s = get_input_line(S("Replace file with: ")); if (s.n > 0) { e->bufi = ed_buf_close(e, e->bufi); e->bufi = ed_buf_open(e, str_to_cstr(s, &e->scratch)); } } break; case '-': { Str s = get_input_line(S("Change path: ")); if (s.n > 0) ed_buf_change_path(e, e->bufi, s); } break; case 'u': if (!txt_hist_back(eb->txt, &eb->cur)) e->msg = S("no previous state"); break; case 'U': if (!txt_hist_fwd(eb->txt, &eb->cur)) e->msg = S("no next state"); break; default: motion(&eb->cur, c); break; } if (e->bufn > 0) { eb = &e->buf[e->bufi]; txt_hist_push(eb->txt, eb->cur); } } void mode_key_insert(Editor *e, u32 c) { EditBuf *eb = &e->buf[e->bufi]; if (c != KEY_ESC && eb->txt->readonly) e->msg = S("readonly buffer"); switch (c) { case KEY_ESC: if (txt_after(eb->cur, start_of_line(eb->cur))) { eb->cur = cprev(eb->cur); } txt_hist_push(eb->txt, eb->cur); e->mode = 0; break; case KEY_BKSP: eb->cur = txt_delete_c(eb->cur); break; case 0x17 /* ^W */: eb->cur = txt_delete_range(prev_word(eb->cur), eb->cur); break; case 0x05 /* ^E */: eb->cur = txt_delete_range(eb->cur, next_word(eb->cur)); break; case 0x0c /* ^L */: vui_redraw(); break; case '\r': eb->cur = ins_newline(eb->cur); break; default: if ((c == '\t' || c >= ' ') && c <= KEY_UTF8_MAX) eb->cur = txt_insert_c(eb->cur, c); break; } } void edit_key(Editor *e, u32 c) { EditBuf *eb = &e->buf[e->bufi]; switch (e->mode) { case MODE_NORMAL: mode_key_normal(e, c); break; case MODE_INSERT: mode_key_insert(e, c); break; case MODE_REPLACE_ONE: if (txt_byte(eb->cur) != '\n' && c != KEY_BKSP && c <= KEY_UTF8_MAX) { eb->cur = cprev(txt_insert_c(txt_delete_c(cnext(eb->cur)), c)); } e->mode = MODE_NORMAL; break; case MODE_REPLACE_MULT: if (c == '\x1b') { e->mode = MODE_NORMAL; eb->cur = cprev(eb->cur); } else if (c == KEY_BKSP) { if (txt_byte(bprev(eb->cur)) != '\n') eb->cur = cprev(eb->cur); } else if (c <= KEY_UTF8_MAX) { if (txt_byte(eb->cur) == '\n') { eb->cur = txt_insert_c(eb->cur, c); } else { eb->cur = txt_insert_c(txt_delete_c(cnext(eb->cur)), c); } } break; } } int main(int argc, const char **argv) { ed_init(&e); e.homedir = normalize_path(str_from_cstr(getenv("HOME")), &e.perm); if (parse_edit_args(&e, argc, argv)) return 1; vui_init(); vui_curs_vis(1); vui_redraw_fn(draw); vui_redraw_ctx(&e); while (e.bufn > 0) { ASSERT(e.bufi < e.bufn); ASSERT(e.buf[e.bufi].cur.t == e.buf[e.bufi].txt); draw(NULL); arena_reset(&e.scratch); /* must happen after draw, so e.msg can persist */ vui_blit(); edit_key(&e, vui_key()); if (e.mode == MODE_NORMAL) e.count = 0; } vui_fini(); ed_fini(&e); return 0; }