/* vi.c - You can't spell "evil" without "vi". * * Copyright 2015 Rob Landley * Copyright 2019 Jarno Mäkipää * * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/vi.html USE_VI(NEWTOY(vi, "<1>1", TOYFLAG_USR|TOYFLAG_BIN)) config VI bool "vi" default n help usage: vi FILE Visual text editor. Predates the existence of standardized cursor keys, so the controls are weird and historical. */ #define FOR_vi #include "toys.h" GLOBALS( int cur_col; int cur_row; unsigned screen_height; unsigned screen_width; int vi_mode; int count0; int count1; int vi_mov_flag; int modified; char vi_reg; ) /* * * TODO: * BUGS: screen pos adjust does not cover "widelines" * * * REFACTOR: use dllist functions where possible. * draw_page dont draw full page at time if nothing changed... * ex callbacks * * FEATURE: ex: / ? % //atleast easy cases * ex: r * ex: !external programs * ex: w filename //only writes to same file now * big file support? */ struct linestack_show { struct linestack_show *next; long top, left; int x, width, y, height; }; static void draw_page(); static int draw_str_until(int *drawn, char *str, int width, int bytes); static void draw_char(char c, int x, int y, int highlight); //utf8 support static int utf8_lnw(int* width, char* str, int bytes); static int utf8_dec(char key, char *utf8_scratch, int *sta_p); static int utf8_len(char *str); static int utf8_width(char *str, int bytes); static int draw_rune(char *c, int x, int y, int highlight); static char* utf8_last(char* str, int size); static int cur_left(int count0, int count1, char* unused); static int cur_right(int count0, int count1, char* unused); static int cur_up(int count0, int count1, char* unused); static int cur_down(int count0, int count1, char* unused); static void check_cursor_bounds(); static void adjust_screen_buffer(); struct str_line { int alloc_len; int str_len; char *str_data; }; //lib dllist uses next and prev kinda opposite what im used to so I just //renamed both ends to up and down struct linelist { struct linelist *up;//next struct linelist *down;//prev struct str_line *line; }; //inserted line not yet pushed to buffer struct str_line *il; struct linelist *text; //file loaded into buffer struct linelist *scr_r;//current screen coord 0 row struct linelist *c_r;//cursor position row void dlist_insert_nomalloc(struct double_list **list, struct double_list *new) { if (*list) { new->next = *list; new->prev = (*list)->prev; if ((*list)->prev) (*list)->prev->next = new; (*list)->prev = new; } else *list = new->next = new->prev = new; } // Add an entry to the end of a doubly linked list struct double_list *dlist_insert(struct double_list **list, char *data) { struct double_list *new = xmalloc(sizeof(struct double_list)); new->data = data; dlist_insert_nomalloc(list, new); return new; } //TODO implement void linelist_unload() { } void write_file(char *filename) { struct linelist *lst = text; FILE *fp = 0; if (!filename) filename = (char*)*toys.optargs; fp = fopen(filename, "w"); if (!fp) return; while (lst) { fprintf(fp, "%s\n", lst->line->str_data); lst = lst->down; } fclose(fp); } int linelist_load(char *filename) { struct linelist *lst = c_r;//cursor position or 0 FILE *fp = 0; if (!filename) filename = (char*)*toys.optargs; fp = fopen(filename, "r"); if (!fp) { char *line = xzalloc(80); ssize_t alc = 80; lst = (struct linelist*)dlist_add((struct double_list**)&lst, xzalloc(sizeof(struct str_line))); lst->line->alloc_len = alc; lst->line->str_len = 0; lst->line->str_data = line; text = lst; dlist_terminate(text->up); return 1; } for (;;) { char *line = xzalloc(80); ssize_t alc = 80; ssize_t len; if ((len = getline(&line, (void *)&alc, fp)) == -1) { if (errno == EINVAL || errno == ENOMEM) { printf("error %d\n", errno); } free(line); break; } lst = (struct linelist*)dlist_add((struct double_list**)&lst, xzalloc(sizeof(struct str_line))); lst->line->alloc_len = alc; lst->line->str_len = len; lst->line->str_data = line; if (lst->line->str_data[len-1] == '\n') { lst->line->str_data[len-1] = 0; lst->line->str_len--; } if (text == 0) { text = lst; } } if (text) { dlist_terminate(text->up); } fclose(fp); return 1; } int vi_yy(char reg, int count0, int count1) { return 1; } //TODO this is overly complicated refactor with lib dllist int vi_dd(char reg, int count0, int count1) { int count = count0*count1; struct linelist *lst = c_r; if (c_r == text && text == scr_r) { if (!text->down && !text->up && text->line) { text->line->str_len = 1; sprintf(text->line->str_data, " "); goto success_exit; } if (text->down) { text = text->down; text->up = 0; c_r = text; scr_r = text; free(lst->line->str_data); free(lst->line); free(lst); } goto recursion_exit; } //TODO use lib dllist stuff if (lst) { if (lst->down) { lst->down->up = lst->up; } if (lst->up) { lst->up->down = lst->down; } if (scr_r == c_r) { scr_r = c_r->down ? c_r->down : c_r->up; } if (c_r->down) c_r = c_r->down; else { c_r = c_r->up; count = 1; } free(lst->line->str_data); free(lst->line); free(lst); } recursion_exit: count--; //make this recursive if (count>0) return vi_dd(reg, count, 1); success_exit: check_cursor_bounds(); adjust_screen_buffer(); return 1; } //TODO i think this thing has bug when removing >40 chars from 80 wide line static int vi_x(char reg, int count0, int count1) { int count = count0; char *s; char *last; int *l; int length = 0; int width = 0; int remaining = 0; char *end; char *start; if (!c_r) return 0; s = c_r->line->str_data; l = &c_r->line->str_len; last = utf8_last(s,*l); if (last == s+TT.cur_col) { memset(last, 0, (*l)-TT.cur_col); *l = TT.cur_col; if (!TT.cur_col) return 1; last = utf8_last(s, TT.cur_col); TT.cur_col = last-s; return 1; } start = s+TT.cur_col; end = start; remaining = (*l)-TT.cur_col; for (;remaining;) { int next = utf8_lnw(&width, end, remaining); if (next && width) { if (!count) break; count--; } if (!next) break; length += next; end += next; remaining -= next; } if (remaining) { memmove(start, end, remaining); memset(start+remaining,0,end-start); } else { memset(start,0,(*l)-TT.cur_col); } *l -= end-start; if (!TT.cur_col) return 1; if (TT.cur_col == (*l)) { last = utf8_last(s, TT.cur_col); TT.cur_col = last-s; } return 1; } //move commands does not behave correct way yet. int vi_movw(int count0, int count1, char* unused) { int count = count0*count1; const char *empties = " \t\n\r"; const char *specials = ",.=-+*/(){}<>[]"; // char *current = 0; if (!c_r) return 0; if (TT.cur_col == c_r->line->str_len-1 || !c_r->line->str_len) goto next_line; if (strchr(empties, c_r->line->str_data[TT.cur_col])) goto find_non_empty; if (strchr(specials, c_r->line->str_data[TT.cur_col])) { for (;strchr(specials, c_r->line->str_data[TT.cur_col]); ) { TT.cur_col++; if (TT.cur_col == c_r->line->str_len-1) goto next_line; } } else for (;!strchr(specials, c_r->line->str_data[TT.cur_col]) && !strchr(empties, c_r->line->str_data[TT.cur_col]);) { TT.cur_col++; if (TT.cur_col == c_r->line->str_len-1) goto next_line; } for (;strchr(empties, c_r->line->str_data[TT.cur_col]); ) { TT.cur_col++; find_non_empty: if (TT.cur_col == c_r->line->str_len-1) { next_line: //we could call j and g0 if (!c_r->down) return 0; c_r = c_r->down; TT.cur_col = 0; if (!c_r->line->str_len) break; } } count--; if (count>0) return vi_movw(count, 1, 0); check_cursor_bounds(); adjust_screen_buffer(); return 1; } static int vi_movb(int count0, int count1, char* unused) { int count = count0*count1; if (!c_r) return 0; if (!TT.cur_col) { if (!c_r->up) return 0; c_r = c_r->up; TT.cur_col = (c_r->line->str_len) ? c_r->line->str_len-1 : 0; goto exit_function; } if (TT.cur_col) TT.cur_col--; while (c_r->line->str_data[TT.cur_col] <= ' ') { if (TT.cur_col) TT.cur_col--; else goto exit_function; } while (c_r->line->str_data[TT.cur_col] > ' ') { if (TT.cur_col)TT.cur_col--; else goto exit_function; } TT.cur_col++; exit_function: count--; if (count>1) return vi_movb(count, 1, 0); check_cursor_bounds(); adjust_screen_buffer(); return 1; } static int vi_move(int count0, int count1, char *unused) { int count = count0*count1; if (!c_r) return 0; if (TT.cur_col < c_r->line->str_len) TT.cur_col++; if (c_r->line->str_data[TT.cur_col] <= ' ' || count > 1) vi_movw(count, 1, 0); //find next word; while (c_r->line->str_data[TT.cur_col] > ' ') TT.cur_col++; if (TT.cur_col) TT.cur_col--; TT.vi_mov_flag |= 2; check_cursor_bounds(); adjust_screen_buffer(); return 1; } void i_insert() { char *t = xzalloc(c_r->line->alloc_len); char *s = c_r->line->str_data; int sel = c_r->line->str_len-TT.cur_col; strncpy(t, &s[TT.cur_col], sel); t[sel+1] = 0; if (c_r->line->alloc_len < c_r->line->str_len+il->str_len+5) { c_r->line->str_data = xrealloc(c_r->line->str_data, c_r->line->alloc_len*2+il->alloc_len*2); c_r->line->alloc_len = c_r->line->alloc_len*2+2*il->alloc_len; memset(&c_r->line->str_data[c_r->line->str_len], 0, c_r->line->alloc_len-c_r->line->str_len); s = c_r->line->str_data; } strcpy(&s[TT.cur_col], il->str_data); strcpy(&s[TT.cur_col+il->str_len], t); TT.cur_col += il->str_len; if (TT.cur_col) TT.cur_col--; c_r->line->str_len += il->str_len; free(t); } //new line at split pos; void i_split() { struct str_line *l = xmalloc(sizeof(struct str_line)); int l_a = c_r->line->alloc_len; int l_len = c_r->line->str_len-TT.cur_col; l->str_data = xzalloc(l_a); l->alloc_len = l_a; l->str_len = l_len; strncpy(l->str_data, &c_r->line->str_data[TT.cur_col], l_len); l->str_data[l_len] = 0; c_r->line->str_len -= l_len; c_r->line->str_data[c_r->line->str_len] = 0; c_r = (struct linelist*)dlist_insert((struct double_list**)&c_r, (char*)l); c_r->line = l; TT.cur_col = 0; check_cursor_bounds(); adjust_screen_buffer(); } static int vi_zero(int count0, int count1, char *unused) { TT.cur_col = 0; return 1; } static int vi_eol(int count0, int count1, char *unused) { int count = count0*count1; for (;count > 1 && c_r->down; count--) c_r = c_r->down; if (c_r && c_r->line->str_len) TT.cur_col = c_r->line->str_len-1; TT.vi_mov_flag |= 2; check_cursor_bounds(); return 1; } static int vi_find_c(int count0, int count1, char *symbol) { int count = count0*count1; if (c_r && c_r->line->str_len) { while (count--) { char* pos = strstr(&c_r->line->str_data[TT.cur_col], symbol); if (pos) { TT.cur_col = pos-c_r->line->str_data; return 1; } } } return 0; } static int vi_find_cb(int count0, int count1, char *symbol) { //do backward search return 1; } //if count is not spesified should go to last line static int vi_go(int count0, int count1, char *symbol) { c_r = text; while(--count0) { if (c_r && c_r->down) c_r = c_r->down; } TT.cur_col = 0; check_cursor_bounds(); adjust_screen_buffer(); return 1; } //need to refactor when implementing yank buffers static int vi_delete(char reg, struct linelist *row, int col, int flags) { if (row == c_r) { if (col < TT.cur_col) { int distance = TT.cur_col - col; TT.cur_col = col; vi_x(reg, distance, 1); } else { int distance = col - TT.cur_col; if (distance > 0) vi_x(reg, distance, 1); } if (TT.vi_mov_flag&2) vi_x(reg, 1, 1); } return 1; } static int vi_D(char reg, int count0, int count1) { int prev_col = TT.cur_col; struct linelist *pos = c_r; if (!count0) return 1; vi_eol(1, 1, 0); vi_delete(reg, pos, prev_col, 0); count0--; if (count0 && c_r->down) { c_r = c_r->down; vi_dd(reg, count0, 1); } return 1; } static int vi_join(char reg, int count0, int count1) { while (count0--) { if (c_r && c_r->down) { int size = c_r->line->str_len+c_r->down->line->str_len; if (size > c_r->line->alloc_len) { if (size > c_r->down->line->alloc_len) { c_r->line->str_data = xrealloc(c_r->line->str_data, c_r->line->alloc_len*2+il->alloc_len*2); memmove(&c_r->line->str_data[c_r->line->str_len], c_r->down->line->str_data,c_r->down->line->str_len); c_r->line->str_len = size; c_r = c_r->down; c_r->line->alloc_len = c_r->line->alloc_len*2+2*il->alloc_len; vi_dd(0,1,1); } else { memmove(&c_r->down->line->str_data[c_r->line->str_len], c_r->down->line->str_data,c_r->down->line->str_len); memmove(c_r->down->line->str_data,c_r->line->str_data, c_r->line->str_len); c_r->down->line->str_len = size; vi_dd(0,1,1); } } else { memmove(&c_r->line->str_data[c_r->line->str_len], c_r->down->line->str_data,c_r->down->line->str_len); c_r->line->str_len = size; c_r = c_r->down; vi_dd(0,1,1); } c_r = c_r->up; } } return 1; } static int vi_change(char reg, struct linelist *row, int col, int flags) { vi_delete(reg, row, col, flags); TT.vi_mode = 2; return 1; } static int vi_yank(char reg, struct linelist *row, int col, int flags) { return 1; } //NOTES //vi-mode cmd syntax is //("[REG])[COUNT0]CMD[COUNT1](MOV) //where: //------------------------------------------------------------- //"[REG] is optional buffer where deleted/yanked text goes REG can be // atleast 0-9, a-z or default " //[COUNT] is optional multiplier for cmd execution if there is 2 COUNT // operations they are multiplied together //CMD is operation to be executed //(MOV) is movement operation, some CMD does not require MOV and some // have special cases such as dd, yy, also movements can work without // CMD //ex commands can be even more complicated than this.... // struct vi_cmd_param { const char* cmd; unsigned flags; int (*vi_cmd)(char, struct linelist*, int, int);//REG,row,col,FLAGS }; struct vi_mov_param { const char* mov; unsigned flags; int (*vi_mov)(int, int, char*);//COUNT0,COUNT1,params }; //spesial cases without MOV and such struct vi_spesial_param { const char *cmd; int (*vi_spesial)(char, int, int);//REG,COUNT0,COUNT1 }; struct vi_spesial_param vi_spesial[5] = { {"dd", &vi_dd}, {"yy", &vi_yy}, {"D", &vi_D}, {"J", &vi_join}, {"x", &vi_x}, }; //there is around ~47 vi moves //some of them need extra params //such as f and ' struct vi_mov_param vi_movs[12] = { {"0", 0, &vi_zero}, {"b", 0, &vi_movb}, {"e", 0, &vi_move}, {"G", 0, &vi_go}, {"h", 0, &cur_left}, {"j", 0, &cur_down}, {"k", 0, &cur_up}, {"l", 0, &cur_right}, {"w", 0, &vi_movw}, {"$", 0, &vi_eol}, {"f", 1, &vi_find_c}, {"F", 1, &vi_find_cb}, }; //change and delete unfortunately behave different depending on move command, //such as ce cw are same, but dw and de are not... //also dw stops at w position and cw seem to stop at e pos+1... //so after movement we need to possibly set up some flags before executing //command, and command needs to adjust... struct vi_cmd_param vi_cmds[3] = { {"c", 1, &vi_change}, {"d", 1, &vi_delete}, {"y", 1, &vi_yank}, }; int run_vi_cmd(char *cmd) { int i = 0; int val = 0; char *cmd_e; int (*vi_cmd)(char, struct linelist*, int, int) = 0; int (*vi_mov)(int, int, char*) = 0; TT.count0 = 0; TT.count1 = 0; TT.vi_reg = '"'; TT.vi_mov_flag = 0; if (*cmd == '"') { cmd++; TT.vi_reg = *cmd; //TODO check validity cmd++; } val = strtol(cmd, &cmd_e, 10); if (errno || val == 0) val = 1; else cmd = cmd_e; TT.count0 = val; for (i = 0; i < 5; i++) { if (strstr(cmd, vi_spesial[i].cmd)) { return vi_spesial[i].vi_spesial(TT.vi_reg, TT.count0, TT.count1); } } for (i = 0; i < 3; i++) { if (!strncmp(cmd, vi_cmds[i].cmd, strlen(vi_cmds[i].cmd))) { vi_cmd = vi_cmds[i].vi_cmd; cmd += strlen(vi_cmds[i].cmd); break; } } val = strtol(cmd, &cmd_e, 10); if (errno || val == 0) val = 1; else cmd = cmd_e; TT.count1 = val; for (i = 0; i < 12; i++) { if (!strncmp(cmd, vi_movs[i].mov, strlen(vi_movs[i].mov))) { vi_mov = vi_movs[i].vi_mov; TT.vi_mov_flag = vi_movs[i].flags; cmd++; if (TT.vi_mov_flag&1 && !(*cmd)) return 0; break; } } if (vi_mov) { int prev_col = TT.cur_col; struct linelist *pos = c_r; if (vi_mov(TT.count0, TT.count1, cmd)) { if (vi_cmd) return (vi_cmd(TT.vi_reg, pos, prev_col, TT.vi_mov_flag)); else return 1; } else return 0; //return some error } return 0; } int search_str(char *s) { struct linelist *lst = c_r; char *c = strstr(&c_r->line->str_data[TT.cur_col], s); if (c) { TT.cur_col = c_r->line->str_data-c; //TODO ?? TT.cur_col = c-c_r->line->str_data; } else for (; !c;) { lst = lst->down; if (!lst) return 1; c = strstr(&lst->line->str_data[TT.cur_col], s); } c_r = lst; TT.cur_col = c-c_r->line->str_data; return 0; } int run_ex_cmd(char *cmd) { if (cmd[0] == '/') { //search pattern if (!search_str(&cmd[1]) ) { check_cursor_bounds(); adjust_screen_buffer(); } } else if (cmd[0] == '?') { } else if (cmd[0] == ':') { if (strstr(&cmd[1], "q!")) { //exit_application; return -1; } else if (strstr(&cmd[1], "wq")) { write_file(0); return -1; } else if (strstr(&cmd[1], "w")) { write_file(0); return 1; } } return 0; } void vi_main(void) { char keybuf[16]; char utf8_code[8]; int utf8_dec_p = 0; int key = 0; char vi_buf[16]; int vi_buf_pos = 0; il = xzalloc(sizeof(struct str_line)); il->str_data = xzalloc(80); il->alloc_len = 80; keybuf[0] = 0; memset(vi_buf, 0, 16); memset(utf8_code, 0, 8); linelist_load(0); scr_r = text; c_r = text; TT.cur_row = 0; TT.cur_col = 0; TT.screen_width = 80; TT.screen_height = 24; TT.vi_mode = 1; terminal_size(&TT.screen_width, &TT.screen_height); TT.screen_height -= 2; //TODO this is hack fix visual alignment set_terminal(0, 1, 0, 0); //writes stdout into different xterm buffer so when we exit //we dont get scroll log full of junk tty_esc("?1049h"); tty_esc("H"); xflush(1); draw_page(); while(1) { key = scan_key(keybuf, -1); printf("key %d\n", key); switch (key) { case -1: case 3: case 4: goto cleanup_vi; } if (TT.vi_mode == 1) { //NORMAL switch (key) { case '/': case '?': case ':': TT.vi_mode = 0; il->str_data[0]=key; il->str_len++; break; case 'a': if (c_r && c_r->line->str_len) TT.cur_col++; case 'i': TT.vi_mode = 2; break; case 27: vi_buf[0] = 0; vi_buf_pos = 0; break; default: if (key > 0x20 && key < 0x7B) { vi_buf[vi_buf_pos] = key;//TODO handle input better vi_buf_pos++; if (run_vi_cmd(vi_buf)) { memset(vi_buf, 0, 16); vi_buf_pos = 0; } else if (vi_buf_pos == 16) { vi_buf_pos = 0; memset(vi_buf, 0, 16); } } break; } } else if (TT.vi_mode == 0) { //EX MODE switch (key) { case 27: TT.vi_mode = 1; il->str_len = 0; memset(il->str_data, 0, il->alloc_len); break; case 0x7F: case 0x08: if (il->str_len) { il->str_data[il->str_len] = 0; if (il->str_len > 1) il->str_len--; } break; case 0x0D: if (run_ex_cmd(il->str_data) == -1) goto cleanup_vi; TT.vi_mode = 1; il->str_len = 0; memset(il->str_data, 0, il->alloc_len); break; default: //add chars to ex command until ENTER if (key >= 0x20 && key < 0x7F) { //might be utf? if (il->str_len == il->alloc_len) { il->str_data = realloc(il->str_data, il->alloc_len*2); il->alloc_len *= 2; } il->str_data[il->str_len] = key; il->str_len++; } break; } } else if (TT.vi_mode == 2) {//INSERT MODE switch (key) { case 27: i_insert(); TT.vi_mode = 1; il->str_len = 0; memset(il->str_data, 0, il->alloc_len); break; case 0x7F: case 0x08: if (il->str_len) il->str_data[il->str_len--] = 0; break; case 0x09: //TODO implement real tabs il->str_data[il->str_len++] = ' '; il->str_data[il->str_len++] = ' '; break; case 0x0D: //insert newline // i_insert(); il->str_len = 0; memset(il->str_data, 0, il->alloc_len); i_split(); break; default: if (key >= 0x20 && utf8_dec(key, utf8_code, &utf8_dec_p)) { if (il->str_len+utf8_dec_p+1 >= il->alloc_len) { il->str_data = realloc(il->str_data, il->alloc_len*2); il->alloc_len *= 2; } strcpy(il->str_data+il->str_len, utf8_code); il->str_len += utf8_dec_p; utf8_dec_p = 0; *utf8_code = 0; } break; } } draw_page(); } cleanup_vi: linelist_unload(); tty_reset(); tty_esc("?1049l"); } static void draw_page() { unsigned y = 0; int cy_scr = 0; int cx_scr = 0; int utf_l = 0; char* line = 0; int bytes = 0; int drawn = 0; int x = 0; struct linelist *scr_buf= scr_r; //clear screen tty_esc("2J"); tty_esc("H"); tty_jump(0, 0); //draw lines until cursor row for (; y < TT.screen_height; ) { if (line && bytes) { draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; y++; tty_jump(0, y); } else if (scr_buf && scr_buf->line->str_data && scr_buf->line->str_len) { if (scr_buf == c_r) break; line = scr_buf->line->str_data; bytes = scr_buf->line->str_len; scr_buf = scr_buf->down; } else { if (scr_buf == c_r) break; y++; tty_jump(0, y); //printf(" \n"); if (scr_buf) scr_buf = scr_buf->down; } } //draw cursor row until cursor //this is to calculate cursor position on screen and possible insert line = scr_buf->line->str_data; bytes = TT.cur_col; for (; y < TT.screen_height; ) { if (bytes) { x = draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; } if (!bytes) break; y++; tty_jump(0, y); } if (TT.vi_mode == 2 && il->str_len) { line = il->str_data; bytes = il->str_len; cx_scr = x; cy_scr = y; x = draw_str_until(&drawn, line, TT.screen_width-x, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; cx_scr += x; for (; y < TT.screen_height; ) { if (bytes) { x = draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; cx_scr = x; } if (!bytes) break; y++; cy_scr = y; tty_jump(0, y); } } else { cy_scr = y; cx_scr = x; } line = scr_buf->line->str_data+TT.cur_col; bytes = scr_buf->line->str_len-TT.cur_col; scr_buf = scr_buf->down; x = draw_str_until(&drawn,line, TT.screen_width-x, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; y++; tty_jump(0, y); //draw until end for (; y < TT.screen_height; ) { if (line && bytes) { draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; y++; tty_jump(0, y); } else if (scr_buf && scr_buf->line->str_data && scr_buf->line->str_len) { line = scr_buf->line->str_data; bytes = scr_buf->line->str_len; scr_buf = scr_buf->down; } else { y++; tty_jump(0, y); if (scr_buf) scr_buf = scr_buf->down; } } tty_jump(0, TT.screen_height); switch (TT.vi_mode) { case 0: tty_esc("30;44m"); printf("COMMAND|"); break; case 1: tty_esc("30;42m"); printf("NORMAL|"); break; case 2: tty_esc("30;41m"); printf("INSERT|"); break; } //DEBUG tty_esc("47m"); tty_esc("30m"); utf_l = utf8_len(&c_r->line->str_data[TT.cur_col]); if (utf_l) { char t[5] = {0, 0, 0, 0, 0}; strncpy(t, &c_r->line->str_data[TT.cur_col], utf_l); printf("utf: %d %s", utf_l, t); } printf("| %d, %d\n", cx_scr, cy_scr); //screen coord tty_jump(TT.screen_width-12, TT.screen_height); printf("| %d, %d\n", TT.cur_row, TT.cur_col); tty_esc("37m"); tty_esc("40m"); if (!TT.vi_mode) { tty_esc("1m"); tty_jump(0, TT.screen_height+1); printf("%s", il->str_data); tty_esc("0m"); } else tty_jump(cx_scr, cy_scr); xflush(1); } static void draw_char(char c, int x, int y, int highlight) { tty_jump(x, y); if (highlight) { tty_esc("30m"); //foreground black tty_esc("47m"); //background white } printf("%c", c); } //utf rune draw //printf and useless copy could be replaced by direct write() to stdout static int draw_rune(char *c, int x, int y, int highlight) { int l = utf8_len(c); char t[5] = {0, 0, 0, 0, 0}; if (!l) return 0; tty_jump(x, y); tty_esc("0m"); if (highlight) { tty_esc("30m"); //foreground black tty_esc("47m"); //background white } strncpy(t, c, 5); printf("%s", t); tty_esc("0m"); return l; } static void check_cursor_bounds() { if (c_r->line->str_len == 0) TT.cur_col = 0; else if (c_r->line->str_len-1 < TT.cur_col) TT.cur_col = c_r->line->str_len-1; if (utf8_width(&c_r->line->str_data[TT.cur_col], c_r->line->str_len-TT.cur_col) <= 0) cur_left(1, 1, 0); } static void adjust_screen_buffer() { //search cursor and screen TODO move this perhaps struct linelist *t = text; int c = -1; int s = -1; int i = 0; for (;;) { i++; if (t == c_r) c = i; if (t == scr_r) s = i; t = t->down; if ( ((c != -1) && (s != -1)) || t == 0) break; } if (c <= s) { scr_r = c_r; } else if ( c > s ) { //should count multiline long strings! int distance = c - s + 1; //TODO instead iterate scr_r up and check strlen%screen_width //for each iteration if (distance >= (int)TT.screen_height) { int adj = distance - TT.screen_height; while (adj--) { scr_r = scr_r->down; } } } TT.cur_row = c; } //return 0 if not ASCII nor UTF-8 //this is not fully tested //naive implementation with branches //there is better branchless lookup table versions out there //1 0xxxxxxx //2 110xxxxx 10xxxxxx //3 1110xxxx 10xxxxxx 10xxxxxx //4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx static int utf8_len(char *str) { int len = 0; int i = 0; uint8_t *c = (uint8_t*)str; if (!c || !(*c)) return 0; if (*c < 0x7F) return 1; if ((*c & 0xE0) == 0xc0) len = 2; else if ((*c & 0xF0) == 0xE0 ) len = 3; else if ((*c & 0xF8) == 0xF0 ) len = 4; else return 0; c++; for (i = len-1; i > 0; i--) { if ((*c++ & 0xc0) != 0x80) return 0; } return len; } //get utf8 length and width at same time static int utf8_lnw(int* width, char* str, int bytes) { wchar_t wc; int length = 1; *width = 1; // if (str < 0x7F) return length; length = mbtowc(&wc, str, bytes); switch (length) { case -1: mbtowc(0,0,4); case 0: *width = 0; length = 0; break; default: *width = wcwidth(wc); } return length; } //try to estimate width of next "glyph" in terminal buffer //combining chars 0x300-0x36F shall be zero width static int utf8_width(char *str, int bytes) { wchar_t wc; switch (mbtowc(&wc, str, bytes)) { case -1: mbtowc(0,0,4); case 0: return -1; default: return wcwidth(wc); } return 0; } static int utf8_dec(char key, char *utf8_scratch, int *sta_p) { int len = 0; char *c = utf8_scratch; c[*sta_p] = key; if (!(*sta_p)) *c = key; if (*c < 0x7F) { *sta_p = 1; return 1; } if ((*c & 0xE0) == 0xc0) len = 2; else if ((*c & 0xF0) == 0xE0 ) len = 3; else if ((*c & 0xF8) == 0xF0 ) len = 4; else {*sta_p = 0; return 0; } (*sta_p)++; if (*sta_p == 1) return 0; if ((c[*sta_p-1] & 0xc0) != 0x80) {*sta_p = 0; return 0; } if (*sta_p == len) { c[(*sta_p)] = 0; return 1; } return 0; } static char* utf8_last(char* str, int size) { char* end = str+size; int pos = size; int len = 0; int width = 0; while (pos >= 0) { len = utf8_lnw(&width, end, size-pos); if (len && width) return end; end--; pos--; } return 0; } static int draw_str_until(int *drawn, char *str, int width, int bytes) { int rune_width = 0; int rune_bytes = 0; int max_bytes = bytes; int max_width = width; char* end = str; for (;width && bytes;) { rune_bytes = utf8_lnw(&rune_width, end, 4); if (!rune_bytes) break; if (width - rune_width < 0) goto write_bytes; width -= rune_width; bytes -= rune_bytes; end += rune_bytes; } for (;bytes;) { rune_bytes = utf8_lnw(&rune_width, end, 4); if (!rune_bytes) break; if (rune_width) break; bytes -= rune_bytes; end += rune_bytes; } write_bytes: fwrite(str, max_bytes-bytes, 1, stdout); *drawn = max_bytes-bytes; return max_width-width; } static int cur_left(int count0, int count1, char* unused) { int count = count0*count1; for (;count--;) { if (!TT.cur_col) return 1; TT.cur_col--; check_cursor_bounds();//has bit ugly recursion hidden here } return 1; } static int cur_right(int count0, int count1, char* unused) { int count = count0*count1; for (;count--;) { if (c_r->line->str_len <= 1) return 1; if (TT.cur_col >= c_r->line->str_len-1) { TT.cur_col = utf8_last(c_r->line->str_data, c_r->line->str_len) - c_r->line->str_data; return 1; } TT.cur_col++; if (utf8_width(&c_r->line->str_data[TT.cur_col], c_r->line->str_len-TT.cur_col) <= 0) cur_right(1, 1, 0); } return 1; } static int cur_up(int count0, int count1, char* unused) { int count = count0*count1; for (;count-- && c_r->up;) c_r = c_r->up; check_cursor_bounds(); adjust_screen_buffer(); return 1; } static int cur_down(int count0, int count1, char* unused) { int count = count0*count1; for (;count-- && c_r->down;) c_r = c_r->down; check_cursor_bounds(); adjust_screen_buffer(); return 1; }