blob: f7edc074954c5063f33e045518a00bd881ff7dde [file] [log] [blame]
/* vi.c - You can't spell "evil" without "vi".
*
* Copyright 2015 Rob Landley <rob@landley.net>
* Copyright 2019 Jarno Mäkipää <jmakip87@gmail.com>
*
* 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;
}