2 * Copyright Neil Brown ©2015-2019 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
5 * ncurses front end for edlib.
7 * There is currently only support for a single terminal window
8 * which provides a single pane.
10 * Rendering operations are:
11 * draw text with attributes at location
12 * erase with attributes in rectangle
20 #define _XOPEN_SOURCE_EXTENDED
21 #ifndef _DEFAULT_SOURCE
22 #define _DEFAULT_SOURCE
32 #include <sys/ioctl.h>
40 typedef char hash_t[MD5_DIGEST_SIZE*2+1];
44 #undef NCURSES_OK_ADDR
45 #define NCURSES_OK_ADDR(p) ((void*)0 != NCURSES_CAST(const void *, (p)))
56 struct col_hash *col_hash;
62 char last_screen[MD5_DIGEST_SIZE*2+1];
63 char next_screen[MD5_DIGEST_SIZE*2+1];
64 /* The next event to generate when idle */
65 enum { DoNil, DoMouse, DoKey, DoCheck, DoClose} next_event;
71 static SCREEN *current_screen;
72 static void ncurses_clear(struct pane *p safe, struct pane *display safe,
73 int attr, short x, short y, short w, short h);
74 static void ncurses_text(struct pane *p safe, struct pane *display safe,
75 wchar_t ch, int attr, short x, short y, short cursor);
76 DEF_CMD(input_handle);
77 DEF_CMD(handle_winch);
78 static struct map *nc_map;
79 DEF_LOOKUP_CMD(ncurses_handle, nc_map);
81 static void set_screen(struct pane *p)
83 struct display_data *dd;
84 extern void *_nc_globals[100];
86 static int index = -1, offset=0;
89 if (current_screen && index >= 0)
90 _nc_globals[index] = NULL;
91 current_screen = NULL;
97 if (dd->scr == current_screen)
102 for (i=0; i<100; i++)
103 if (_nc_globals[i] < (void*)stdscr &&
104 _nc_globals[i]+4*(sizeof(void*)) >= (void*)stdscr) {
106 offset = ((void*)stdscr) - _nc_globals[i];
111 current_screen = dd->scr;
113 _nc_globals[index] = ((void*)stdscr) - offset;
119 DEF_CMD(abort_replay);
121 static int parse_event(struct pane *p safe);
122 static int prepare_recrep(struct pane *p safe)
124 struct display_data *dd = p->data;
127 name = getenv("EDLIB_RECORD");
129 dd->log = fopen(name, "w");
130 name = getenv("EDLIB_REPLAY");
132 dd->input = fopen(name, "r");
133 if (getenv("EDLIB_PAUSE"))
134 sleep(atoi(getenv("EDLIB_PAUSE")));
142 static void close_recrep(struct pane *p safe)
144 struct display_data *dd = p->data;
147 fprintf(dd->log, "Close\n");
152 static void record_key(struct pane *p safe, char *key safe)
154 struct display_data *dd = p->data;
159 if (!strchr(key, '"'))
161 else if (!strchr(key, '\''))
163 else if (!strchr(key, '/'))
167 fprintf(dd->log, "Key %c%s%c\n", q,key,q);
170 static void record_mouse(struct pane *p safe, char *key safe, int x, int y)
172 struct display_data *dd = p->data;
176 if (!strchr(key, '"'))
178 else if (!strchr(key, '\''))
180 else if (!strchr(key, '/'))
184 fprintf(dd->log, "Mouse %c%s%c %d,%d\n", q,key,q, x, y);
187 static void record_screen(struct pane *p safe)
189 struct display_data *dd = p->data;
190 struct md5_state ctx;
191 uint16_t buf[CCHARW_MAX+4];
192 char out[MD5_DIGEST_SIZE*2+1];
195 if (!dd->log && !(dd->input && dd->next_event == DoCheck))
199 for (r = 0; r < p->h; r++)
200 for (c = 0; c < p->w; c++) {
202 wchar_t wc[CCHARW_MAX+2];
208 getcchar(&cc, wc, &a, &color, NULL);
209 buf[0] = htole16(color);
210 for (l = 0; l < CCHARW_MAX && wc[l]; l++)
211 buf[l+2] = htole16(wc[l]);
213 md5_update(&ctx, (uint8_t*)buf, (l+2) * 2);
215 md5_final_txt(&ctx, out);
217 fprintf(dd->log, "Display %d,%d %s", p->w, p->h, out);
218 strcpy(dd->last_screen, out);
219 if (dd->cursor.x >= 0)
220 fprintf(dd->log, " %d,%d", dd->cursor.x, dd->cursor.y);
221 fprintf(dd->log, "\n");
223 if (dd->input && dd->next_event == DoCheck) {
224 call_comm("event:free", p, &abort_replay);
225 // if (strcmp(dd->last_screen, dd->next_screen) != 0)
226 // dd->next_event = DoClose;
227 call_comm("editor-on-idle", p, &next_evt);
231 static inline int match(char *line safe, char *w safe)
233 return strncmp(line, w, strlen(w)) == 0;
236 static char *copy_quote(char *line safe, char *buf safe)
242 if (q != '"' && q != '\'' && q != '/')
244 while (*line != q && *line)
252 static char *get_coord(char *line safe, struct xy *co safe)
259 v = strtol(line, &ep, 10);
260 if (!ep || ep == line || *ep != ',')
264 v = strtol(line, &ep, 10);
265 if (!ep || ep == line || (*ep && *ep != ' ' && *ep != '\n'))
271 static char *get_hash(char *line safe, hash_t hash safe)
276 for (i = 0; i < MD5_DIGEST_SIZE*2 && *line; i++)
283 REDEF_CMD(abort_replay)
285 struct display_data *dd = ci->home->data;
287 dd->next_event = DoClose;
288 return next_evt_func(ci);
291 static int parse_event(struct pane *p safe)
293 struct display_data *dd = p->data;
297 dd->next_event = DoNil;
299 fgets(line, sizeof(line)-1, dd->input) == NULL)
301 else if (match(line, "Key ")) {
302 if (!copy_quote(line+4, dd->event_info))
304 dd->next_event = DoKey;
305 } else if (match(line, "Mouse ")) {
306 char *f = copy_quote(line+6, dd->event_info);
309 f = get_coord(f, &dd->event_pos);
312 dd->next_event = DoMouse;
313 } else if (match(line, "Display ")) {
314 char *f = get_coord(line+8, &dd->event_pos);
317 f = get_hash(f, dd->next_screen);
318 dd->next_event = DoCheck;
319 } else if (match(line, "Close")) {
320 dd->next_event = DoClose;
323 if (dd->next_event != DoCheck)
324 call_comm("editor-on-idle", p, &next_evt);
326 call_comm("event:timer", p, &abort_replay, 10*1000);
332 struct pane *p = ci->home;
333 struct display_data *dd = p->data;
334 int button = 0, type = 0;
337 delay = getenv("EDLIB_REPLAY_DELAY");
338 if (delay && dd->next_event != DoCheck)
339 usleep(atoi(delay)*1000);
341 switch(dd->next_event) {
343 record_key(p, dd->event_info);
344 call("Keystroke", p, 0, NULL, dd->event_info);
347 record_mouse(p, dd->event_info, dd->event_pos.x,
349 if (strstr(dd->event_info, ":Press"))
351 else if (strstr(dd->event_info, ":Release"))
353 else if (strstr(dd->event_info, ":Motion"))
355 if (type == 1 || type == 2) {
356 char *e = dd->event_info + strlen(dd->event_info) - 1;
359 call("Mouse-event", p, button, NULL, dd->event_info,
361 dd->event_pos.x, dd->event_pos.y);
364 /* No point checking, just do a diff against new trace log. */
365 /* not; (strcmp(dd->next_screen, dd->last_screen) != 0) */
368 call("event:deactivate", p);
372 call_comm("event:read", p, &input_handle, 0);
373 call_comm("event:signal", p, &handle_winch, SIGWINCH);
380 static inline int prepare_recrep(struct pane *p safe) {return 0;}
381 static inline void record_key(struct pane *p safe, char *key) {}
382 static inline void record_mouse(struct pane *p safe, char *key safe,
384 static inline void record_screen(struct pane *p safe) {}
385 static inline void close_recrep(struct pane *p safe) {}
390 struct pane *p = ci->home;
392 call("Sig:Winch", p);
395 pane_damaged(p, DAMAGED_SIZE);
401 struct call_return *cr = container_of(ci->comm, struct call_return, c);
407 DEF_CMD(nc_close_display)
409 /* If this is only display, then refuse to close this one */
410 struct call_return cr;
411 struct display_data *dd = ci->home->data;
414 call("Message", ci->focus, 0, NULL, dd->noclose);
420 call_comm("editor:notify:all-displays", ci->focus, &cr.c);
422 pane_close(ci->home);
424 call("Message", ci->focus, 0, NULL,
425 "Cannot close only window.");
429 DEF_CMD(nc_set_noclose)
431 struct display_data *dd = ci->home->data;
436 dd->noclose = strdup(ci->str);
440 static void ncurses_end(struct pane *p safe)
449 * hash table for colours and pairs
450 * key is r,g,b (0-1000) in 10bit fields,
451 * or fg,bg in 16 bit fields with bit 31 set
452 * content is colour number of colour pair number.
453 * We never delete entries, unless we delete everything.
460 #define COL_KEY(r,g,b) ((r<<20) | (g<<10) | b)
461 #define PAIR_KEY(fg, bg) ((1<<31) | (fg<<16) | bg)
462 #define hash_key(k) ((((k) * 0x61C88647) >> 20) & 0xff)
465 int next_col, next_pair;
466 struct chash *tbl[256];
469 static struct col_hash *safe hash_init(struct display_data *dd safe)
472 dd->col_hash = malloc(sizeof(*dd->col_hash));
473 memset(dd->col_hash, 0, sizeof(*dd->col_hash));
474 dd->col_hash->next_col = 16;
475 dd->col_hash->next_pair = 1;
480 static void hash_free(struct display_data *dd safe)
489 for (h = 0; h < 255; h++)
490 while ((c = ch->tbl[h]) != NULL) {
491 ch->tbl[h] = c->next;
498 static int find_col(struct display_data *dd safe, int rgb[])
500 if (0 /* dynamic colours */) {
501 struct col_hash *ch = hash_init(dd);
502 int k = COL_KEY(rgb[0], rgb[1], rgb[2]);
506 for (c = ch->tbl[h]; c; c = c->next)
509 c = malloc(sizeof(*c));
511 c->content = ch->next_col++;
512 c->next = ch->tbl[h];
514 init_color(c->content, rgb[0], rgb[1], rgb[2]);
517 /* If colour is grey, map to 1 of 26 for 0, 232 to 255, 15
518 * The 24 grey shades have bit values from 8 to 238, so the
519 * gap to white is a little bigger, but that probably doesn't
521 * Otherwise map to 6x6x6 rgb cube from 16
522 * Actual colours are biased bright, at 0,95,135,175,215,255
523 * with a 95 gap at bottom and 40 elsewhere.
524 * So we divide 5 and 2 half ranges, and merge bottom 2.
529 //printf("want %d,%d,%d\n", rgb[0], rgb[1], rgb[2]);
530 if (abs(rgb[0] - rgb[1]) < 10 &&
531 abs(rgb[1] - rgb[2]) < 10) {
532 /* grey - within 1% */
533 int v = (rgb[0] + rgb[1] + rgb[2]) / 3;
535 /* We divide the space in 24 ranges surrounding
536 * the grey values, and 2 half-ranges near black
537 * and white. So add half a range - 1000/50 -
538 * then divide by 1000/25 to get a number from 0 to 25.
540 v = (v + 1000/50) / (1000/25);
542 return 0; /* black */
544 return 15; /* white */
545 //printf(" grey %d\n", v + 231);
546 /* grey shades are from 232 to 255 inclusive */
549 for (h = 0; h < 3; h++) {
552 v = (v + 1000/12) / (1000/6);
553 /* v is from 0 to 6, we want up to 5
554 * with 0 and 1 merged
561 //printf(" color %d\n", c + 16);
566 static int to_pair(struct display_data *dd safe, int fg, int bg)
568 struct col_hash *ch = hash_init(dd);
569 int k = PAIR_KEY(fg, bg);
573 for (c = ch->tbl[h]; c; c = c->next)
576 c = malloc(sizeof(*c));
578 c->content = ch->next_pair++;
579 c->next = ch->tbl[h];
581 init_pair(c->content, fg, bg);
586 static int cvt_attrs(struct pane *home safe, const char *attrs)
588 struct display_data *dd = home->data;
593 int fg = COLOR_BLACK;
594 int bg = COLOR_WHITE+8;
609 strncpy(tmp, a, c-a);
611 if (strcmp(tmp, "inverse")==0) attr |= A_STANDOUT;
612 else if (strcmp(tmp, "bold")==0) attr |= A_BOLD;
613 else if (strcmp(tmp, "underline")==0) attr |= A_UNDERLINE;
614 else if (strncmp(tmp, "fg:", 3) == 0) {
615 struct call_return cr =
616 call_ret(all, "colour:map", home,
618 int rgb[3] = {cr.i, cr.i2, cr.x};
619 fg = find_col(dd, rgb);
620 } else if (strncmp(tmp, "bg:", 3) == 0) {
621 struct call_return cr =
622 call_ret(all, "colour:map", home,
624 int rgb[3] = {cr.i, cr.i2, cr.x};
625 bg = find_col(dd, rgb);
629 if (fg != COLOR_BLACK || bg != COLOR_WHITE+8) {
630 int p = to_pair(dd, fg, bg);
631 attr |= COLOR_PAIR(p);
636 static int make_cursor(int attr)
638 return attr ^ A_UNDERLINE;
641 DEF_CMD(nc_notify_display)
643 struct display_data *dd = ci->home->data;
644 comm_call(ci->comm2, "callback:display", ci->home, dd->last_event);
650 struct pane *p = ci->home;
651 struct display_data *dd = p->data;
654 fclose(dd->scr_file);
660 struct pane *p = ci->home;
661 int attr = cvt_attrs(p, ci->str2?:ci->str);
663 ncurses_clear(ci->focus, p, attr, 0, 0, 0, 0);
664 pane_damaged(p, DAMAGED_POSTORDER);
668 DEF_CMD(nc_text_size)
670 int max_space = ci->num;
675 const char *str = ci->str;
681 while (str[offset] != 0) {
683 int skip = mbrtowc(&wc, str+offset, len-offset, &mbs);
691 if (size <= max_space)
694 return comm_call(ci->comm2, "callback:size", ci->focus,
695 max_bytes, NULL, NULL,
696 0, NULL, NULL, size, 1);
699 DEF_CMD(nc_draw_text)
701 struct pane *p = ci->home;
702 int attr = cvt_attrs(p, ci->str2);
703 int cursor_offset = ci->num;
705 short x = ci->x, y = ci->y;
707 const char *str = ci->str;
714 while (str[offset] != 0) {
716 int skip = mbrtowc(&wc, str+offset, len-offset, &mbs);
723 if (cursor_offset >= offset &&
724 cursor_offset < offset + skip)
725 ncurses_text(ci->focus, p, wc, attr, x, y, 1);
727 ncurses_text(ci->focus, p, wc, attr, x, y, 0);
731 if (offset == cursor_offset)
732 ncurses_text(ci->focus, p, ' ', 0, x, y, 1);
733 pane_damaged(p, DAMAGED_POSTORDER);
737 DEF_CMD(nc_refresh_size)
739 struct pane *p = ci->home;
742 getmaxyx(stdscr, p->h, p->w);
746 DEF_CMD(nc_refresh_post)
748 struct pane *p = ci->home;
749 struct display_data *dd = p->data;
751 if (dd->cursor.x >= 0)
752 move(dd->cursor.y, dd->cursor.x);
758 static struct pane *ncurses_init(struct pane *ed,
759 const char *tty, const char *term)
763 struct display_data *dd;
768 f = fopen(tty, "r+");
773 scr = newterm(term, f, f);
780 dd->cursor.x = dd->cursor.y = -1;
781 dd->is_xterm = (term && strncmp(term, "xterm", 5) == 0);
783 p = pane_register(ed, 0, &ncurses_handle.c, dd);
787 use_default_colors();
793 intrflush(stdscr, FALSE);
794 keypad(stdscr, TRUE);
795 mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
798 getmaxyx(stdscr, p->h, p->w);
800 call("editor:request:all-displays", p);
801 if (!prepare_recrep(p)) {
802 call_comm("event:read", p, &input_handle, fileno(f));
804 call_comm("event:signal", p, &handle_winch, SIGWINCH);
806 pane_damaged(p, DAMAGED_SIZE);
810 REDEF_CMD(handle_winch)
812 struct pane *p = ci->home;
813 struct display_data *dd = p->data;
815 ioctl(fileno(dd->scr_file), TIOCGWINSZ, &size);
817 resize_term(size.ws_row, size.ws_col);
820 pane_damaged(p, DAMAGED_SIZE);
824 static void ncurses_clear(struct pane *p safe, struct pane *display safe,
825 int attr, short x, short y, short w, short h)
834 pane_absxy(p, &x, &y, &w, &h);
836 if (pane_masked(display, x, y, p->abs_z, &w0, &h0))
841 for (r = y; r < y+h; r++)
842 for (c = x; c < x+w; c++)
843 if ((r < y+h0 && c < x+w0) ||
844 !pane_masked(display, c, r, p->abs_z, NULL, NULL))
848 static void ncurses_text(struct pane *p safe, struct pane *display safe,
849 wchar_t ch, int attr, short x, short y, short cursor)
851 struct display_data *dd;
860 while (p2->parent != p2 && p2 != display) {
861 if (p2->parent->focus != p2 && p2->z >= 0)
867 pane_absxy(p, &x, &y, &w, &h);
871 if (pane_masked(display, x, y, p->abs_z, NULL, NULL))
877 /* Cursor is in-focus */
882 /* Cursor here, but not focus */
883 attr = make_cursor(attr);
887 mvadd_wch(y, x, &cc);
890 static struct namelist {
897 {KEY_RIGHT, ":Right"},
899 {KEY_BACKSPACE, ":Backspace\037:C-H\037:C-h"},
900 {KEY_DL, ":DelLine"},
901 {KEY_IL, ":InsLine"},
904 {KEY_ENTER, ":Enter\037:C-M\037:C-m"},
907 {KEY_NPAGE, ":Next"},
908 {KEY_PPAGE, ":Prior"},
911 {KEY_SDL, ":S:DelLine"},
912 {KEY_SEND, ":S:End"},
913 {KEY_SHOME, ":S:Home"},
914 {KEY_SLEFT, ":S:Left"},
915 {KEY_SRIGHT, ":S:Right"},
916 {KEY_BTAB, ":S:Tab"},
918 { 01057, ":M:Prior"},
923 { 01064, ":M:Right"},
935 static char *find_name (struct namelist *l safe, wint_t c)
938 for (i = 0; l[i].name; i++)
944 static void send_key(int keytype, wint_t c, int meta, struct pane *p safe)
946 struct display_data *dd = p->data;
948 char buf[100];/* FIXME */
949 char *m = meta ? ":M" : "";
951 if (keytype == KEY_CODE_YES) {
952 n = find_name(key_names, c);
954 sprintf(buf, "%sNcurs-%o", m, c);
956 strcat(strcpy(buf, m), n);
958 n = find_name(char_names, c);
960 sprintf(buf, "%s%s\037%s:C-%c\037%s:C-%c",
965 sprintf(buf, "%s:C-%c\037%s:C-%c",
968 sprintf(buf, "%s-%lc", m, c);
971 dd->last_event = time(NULL);
973 call("Keystroke", p, 0, NULL, buf);
976 static void do_send_mouse(struct pane *p safe, int x, int y, char *cmd safe,
977 int button, char *mod, int type)
980 struct display_data *dd = p->data;
982 record_mouse(p, cmd, x, y);
983 ret = call("Mouse-event", p, button, NULL, cmd, type, NULL, mod, x, y);
984 if (type == 1 && !dd->report_position) {
986 fprintf(dd->scr_file, "\033[?1002h");
987 fflush(dd->scr_file);
989 dd->report_position = 1;
990 } else if (type == 3 && !ret) {
992 fprintf(dd->scr_file, "\033[?1002l");
993 fflush(dd->scr_file);
995 dd->report_position = 0;
999 static void send_mouse(MEVENT *mev safe, struct pane *p safe)
1001 struct display_data *dd = p->data;
1007 /* MEVENT has lots of bits. We want a few numbers */
1008 for (b = 1 ; b <= (NCURSES_MOUSE_VERSION <= 1 ? 3 : 5); b++) {
1009 mmask_t s = mev->bstate;
1014 if (s & BUTTON_SHIFT) modf |= 1;
1015 if (s & BUTTON_CTRL) modf |= 2;
1016 if (s & BUTTON_ALT) modf |= 4;
1018 case 0: mod = ""; break;
1019 case 1: mod = ":S"; break;
1020 case 2: mod = ":C"; break;
1021 case 3: mod = ":C:S"; break;
1022 case 4: mod = ":M"; break;
1023 case 5: mod = ":M:S"; break;
1024 case 6: mod = ":M:C"; break;
1025 case 7: mod = ":M:C:S"; break;
1027 if (BUTTON_PRESS(s, b))
1028 action = "%s:Press-%d";
1029 else if (BUTTON_RELEASE(s, b)) {
1030 action = "%s:Release-%d";
1031 /* Modifiers only reported on button Press */
1035 snprintf(buf, sizeof(buf), action, mod, b);
1036 dd->last_event = time(NULL);
1037 do_send_mouse(p, x, y, buf, b, mod, BUTTON_PRESS(s,b) ? 1 : 2);
1039 if ((mev->bstate & REPORT_MOUSE_POSITION) &&
1040 dd->report_position)
1041 /* Motion doesn't update last_event */
1042 do_send_mouse(p, x, y, ":Motion", 0, "", 3);
1045 REDEF_CMD(input_handle)
1047 struct pane *p = ci->home;
1051 int have_escape = 0;
1053 if (!(void*)p->data)
1054 /* already closed */
1057 while ((is_keycode = get_wch(&c)) != ERR) {
1058 if (c == KEY_MOUSE) {
1060 while (getmouse(&mev) != ERR)
1061 send_mouse(&mev, p);
1062 } else if (have_escape) {
1063 send_key(is_keycode, c, 1, p);
1065 } else if (c == '\e')
1068 send_key(is_keycode, c, 0, p);
1069 /* Don't know what other code might have done,
1070 * so re-set the screen
1075 send_key(is_keycode, '\e', 0, p);
1079 DEF_CMD(display_ncurses)
1084 term = pane_attr_get(ci->focus, "TERM");
1086 term = getenv("TERM");
1088 term = "xterm-256color";
1090 p = ncurses_init(ci->focus, ci->str, term);
1092 struct pane *p2 = call_ret(pane, "attach-x11selection", p);
1095 return comm_call(ci->comm2, "callback:display", p);
1100 void edlib_init(struct pane *ed safe)
1102 call_comm("global-set-command", ed, &display_ncurses, 0, NULL,
1103 "attach-display-ncurses");
1105 nc_map = key_alloc();
1106 key_add(nc_map, "Display:refresh", &nc_refresh);
1107 key_add(nc_map, "Display:close", &nc_close_display);
1108 key_add(nc_map, "Display:set-noclose", &nc_set_noclose);
1109 key_add(nc_map, "Close", &nc_close);
1110 key_add(nc_map, "Free", &edlib_do_free);
1111 key_add(nc_map, "pane-clear", &nc_clear);
1112 key_add(nc_map, "text-size", &nc_text_size);
1113 key_add(nc_map, "Draw:text", &nc_draw_text);
1114 key_add(nc_map, "Refresh:size", &nc_refresh_size);
1115 key_add(nc_map, "Refresh:postorder", &nc_refresh_post);
1116 key_add(nc_map, "all-displays", &nc_notify_display);
1117 key_add(nc_map, "Sig:Winch", &handle_winch);