1 #include "termdriver.h"
2 
3 // This entire driver requires unibilium
4 #ifdef HAVE_UNIBILIUM
5 #include "unibilium.h"
6 
7 #include <string.h>
8 #include <stdarg.h>
9 
10 /* I would love if terminfo gave us these extra strings, but it does not. At a
11  * minor risk of non-portability, define them here.
12  */
13 struct TermInfoExtraStrings {
14   const char *enter_altscreen_mode;
15   const char *exit_altscreen_mode;
16 
17   const char *enter_mouse_mode;
18   const char *exit_mouse_mode;
19 };
20 
21 static const struct TermInfoExtraStrings extra_strings_default = {
22   // Alternate screen + cursor save mode
23   .enter_altscreen_mode = "\e[?1049h",
24   .exit_altscreen_mode  = "\e[?1049l",
25 };
26 
27 static const struct TermInfoExtraStrings extra_strings_vt200_mouse = {
28   // Alternate screen + cursor save mode
29   .enter_altscreen_mode = "\e[?1049h",
30   .exit_altscreen_mode  = "\e[?1049l",
31 
32   // Mouse click/drag reporting
33   // Also speculatively enable SGR protocol
34   .enter_mouse_mode = "\e[?1002h\e[?1006h",
35   .exit_mouse_mode  = "\e[?1002l\e[?1006l",
36 };
37 
38 /* Also, some terminfo databases are incomplete. Lets provide some fallbacks
39  */
40 struct TermInfoFallback {
41   enum unibi_string cap;
42   const char *value;
43 };
44 
45 const static struct TermInfoFallback terminfo_fallback[] = {
46   { unibi_erase_chars, "\e[%dX" },
47   { unibi_parm_dch,    "\e[%dP" },
48   { 0 },
49 };
50 
51 struct TIDriver {
52   TickitTermDriver driver;
53 
54   unibi_term *ut;
55 
56   struct {
57     unsigned int altscreen:1;
58     unsigned int cursorvis:1;
59     unsigned int mouse:1;
60   } mode;
61 
62   struct {
63     unsigned int bce:1;
64     int colours;
65   } cap;
66 
67   struct {
68     // Positioning
69     const char *cup;  // cursor_address
70     const char *vpa;  // row_address == vertical position absolute
71     const char *hpa;  // column_address = horizontal position absolute
72 
73     // Moving
74     const char *cuu; const char *cuu1; // Cursor Up
75     const char *cud; const char *cud1; // Cursor Down
76     const char *cuf; const char *cuf1; // Cursor Forward == Right
77     const char *cub; const char *cub1; // Cursor Backward == Left
78 
79     // Editing
80     const char *ich; const char *ich1; // Insert Character
81     const char *dch; const char *dch1; // Delete Character
82     const char *il;  const char *il1;  // Insert Line
83     const char *dl;  const char *dl1;  // Delete Line
84     const char *ech;                   // Erase Character
85     const char *ed2;                   // Erase Data 2 == Clear screen
86     const char *stbm;                  // Set Top/Bottom Margins
87 
88     // Formatting
89     const char *sgr;    // Select Graphic Rendition
90     const char *sgr0;   // Exit Attribute Mode
91     const char *sgr_i0, *sgr_i1; // SGR italic off/on
92     const char *sgr_fg; // SGR foreground colour
93     const char *sgr_bg; // SGR background colour
94 
95     // Mode setting/clearing
96     const char *sm_csr; const char *rm_csr; // Set/reset mode: Cursor visible
97   } str;
98 
99   const struct TermInfoExtraStrings *extra;
100 };
101 
lookup_ti_string(struct TIDriver * td,const TickitTermProbeArgs * args,enum unibi_string s)102 static const char *lookup_ti_string(struct TIDriver *td, const TickitTermProbeArgs *args, enum unibi_string s)
103 {
104   const char *value = unibi_get_str(td->ut, s);
105 
106   for(const struct TermInfoFallback *fb = terminfo_fallback; !value && fb->cap; fb++)
107     if(fb->cap == s)
108       value = fb->value;
109 
110   if(args->ti_hook && args->ti_hook->getstr)
111     value = (*args->ti_hook->getstr)(unibi_name_str(s), value, args->ti_hook->data);
112 
113   return value;
114 }
115 
require_ti_string(struct TIDriver * td,const TickitTermProbeArgs * args,enum unibi_string s)116 static const char *require_ti_string(struct TIDriver *td, const TickitTermProbeArgs *args, enum unibi_string s)
117 {
118   const char *ret = lookup_ti_string(td, args, s);
119   if(ret)
120     return ret;
121 
122   fprintf(stderr, "Required TI string '%s' is missing\n", unibi_name_str(s));
123   abort();
124 }
125 
print(TickitTermDriver * ttd,const char * str,size_t len)126 static bool print(TickitTermDriver *ttd, const char *str, size_t len)
127 {
128   tickit_termdrv_write_str(ttd, str, len);
129   return true;
130 }
131 
run_ti(TickitTermDriver * ttd,const char * str,int n_params,...)132 static void run_ti(TickitTermDriver *ttd, const char *str, int n_params, ...)
133 {
134   unibi_var_t params[9];
135   va_list args;
136 
137   if(!str) {
138     fprintf(stderr, "Abort on attempt to use NULL TI string\n");
139     abort();
140   }
141 
142   va_start(args, n_params);
143   for(int i = 0; i < 10 && i < n_params; i++)
144     params[i] = unibi_var_from_num(va_arg(args, int));
145 
146   char tmp[64];
147   char *buf = tmp;
148   size_t len = unibi_run(str, params, buf, sizeof(tmp));
149 
150   if(len > sizeof(tmp)) {
151     buf = tickit_termdrv_get_tmpbuffer(ttd, len);
152     unibi_run(str, params, buf, len);
153   }
154 
155   tickit_termdrv_write_str(ttd, buf, len);
156 }
157 
goto_abs(TickitTermDriver * ttd,int line,int col)158 static bool goto_abs(TickitTermDriver *ttd, int line, int col)
159 {
160   struct TIDriver *td = (struct TIDriver*)ttd;
161 
162   if(line != -1 && col != -1)
163     run_ti(ttd, td->str.cup, 2, line, col);
164   else if(line != -1) {
165     if(!td->str.vpa)
166       return false;
167 
168     run_ti(ttd, td->str.vpa, 1, line);
169   }
170   else if(col != -1) {
171     if(col == 0) {
172       tickit_termdrv_write_str(ttd, "\r", 1);
173       return true;
174     }
175 
176     if(td->str.hpa)
177       run_ti(ttd, td->str.hpa, 1, col);
178     else if(td->str.cuf) {
179       // Emulate HPA by CR + CUF
180       tickit_termdrv_write_str(ttd, "\r", 1);
181       run_ti(ttd, td->str.cuf, 1, col);
182     }
183     else
184       return false;
185   }
186 
187   return true;
188 }
189 
move_rel(TickitTermDriver * ttd,int downward,int rightward)190 static bool move_rel(TickitTermDriver *ttd, int downward, int rightward)
191 {
192   struct TIDriver *td = (struct TIDriver*)ttd;
193 
194   if(downward == 1 && td->str.cud1)
195     run_ti(ttd, td->str.cud1, 0);
196   else if(downward == -1 && td->str.cuu1)
197     run_ti(ttd, td->str.cuu1, 0);
198   else if(downward > 0)
199     run_ti(ttd, td->str.cud, 1, downward);
200   else if(downward < 0)
201     run_ti(ttd, td->str.cuu, 1, -downward);
202 
203   if(rightward == 1 && td->str.cuf1)
204     run_ti(ttd, td->str.cuf1, 0);
205   else if(rightward == -1 && td->str.cub1)
206     run_ti(ttd, td->str.cub1, 0);
207   else if(rightward > 0)
208     run_ti(ttd, td->str.cuf, 1, rightward);
209   else if(rightward < 0)
210     run_ti(ttd, td->str.cub, 1, -rightward);
211 
212   return true;
213 }
214 
scrollrect(TickitTermDriver * ttd,const TickitRect * rect,int downward,int rightward)215 static bool scrollrect(TickitTermDriver *ttd, const TickitRect *rect, int downward, int rightward)
216 {
217   struct TIDriver *td = (struct TIDriver*)ttd;
218 
219   if(!downward && !rightward)
220     return true;
221 
222   int term_lines, term_cols;
223   tickit_term_get_size(ttd->tt, &term_lines, &term_cols);
224 
225   if((tickit_rect_right(rect) == term_cols) && downward == 0) {
226     for(int line = rect->top; line < tickit_rect_bottom(rect); line++) {
227       goto_abs(ttd, line, rect->left);
228 
229       if(rightward == 1 && td->str.dch1)
230         run_ti(ttd, td->str.dch1, 0);
231       else if(rightward == -1 && td->str.ich1)
232         run_ti(ttd, td->str.ich1, 0);
233       else if(rightward > 0)
234         run_ti(ttd, td->str.dch, 1, rightward);
235       else if(rightward < 0)
236         run_ti(ttd, td->str.ich, 1, -rightward);
237     }
238 
239     return true;
240   }
241 
242   if(rect->left == 0 && rect->cols == term_cols && rightward == 0) {
243     run_ti(ttd, td->str.stbm, 2, rect->top, tickit_rect_bottom(rect) - 1);
244 
245     goto_abs(ttd, rect->top, 0);
246 
247     if(downward == 1 && td->str.dl1)
248       run_ti(ttd, td->str.dl1, 0);
249     else if(downward == -1 && td->str.il1)
250       run_ti(ttd, td->str.il1, 0);
251     else if(downward > 0)
252       run_ti(ttd, td->str.dl, 1, downward);
253     else if(downward < 0)
254       run_ti(ttd, td->str.il, 1, -downward);
255 
256     run_ti(ttd, td->str.stbm, 2, 0, term_lines - 1);
257     return true;
258   }
259 
260   return false;
261 }
262 
erasech(TickitTermDriver * ttd,int count,TickitMaybeBool moveend)263 static bool erasech(TickitTermDriver *ttd, int count, TickitMaybeBool moveend)
264 {
265   struct TIDriver *td = (struct TIDriver *)ttd;
266 
267   if(count < 1)
268     return true;
269 
270   /* Even if the terminal can do bce, only use ECH if we're not in
271    * reverse-video mode. Most terminals don't do rv+ECH properly
272    */
273   if(td->cap.bce && !tickit_pen_get_bool_attr(tickit_termdrv_current_pen(ttd), TICKIT_PEN_REVERSE)) {
274     run_ti(ttd, td->str.ech, 1, count);
275 
276     if(moveend == TICKIT_YES)
277       move_rel(ttd, 0, count);
278   }
279   else {
280      /* TODO: consider tickit_termdrv_write_chrfill(ttd, c, n)
281      */
282     char *spaces = tickit_termdrv_get_tmpbuffer(ttd, 64);
283     memset(spaces, ' ', 64);
284     while(count > 64) {
285       tickit_termdrv_write_str(ttd, spaces, 64);
286       count -= 64;
287     }
288     tickit_termdrv_write_str(ttd, spaces, count);
289 
290     if(moveend == TICKIT_NO)
291       move_rel(ttd, 0, -count);
292   }
293 
294   return true;
295 }
296 
clear(TickitTermDriver * ttd)297 static bool clear(TickitTermDriver *ttd)
298 {
299   struct TIDriver *td = (struct TIDriver *)ttd;
300 
301   run_ti(ttd, td->str.ed2, 0);
302 
303   return true;
304 }
305 
chpen(TickitTermDriver * ttd,const TickitPen * delta,const TickitPen * final)306 static bool chpen(TickitTermDriver *ttd, const TickitPen *delta, const TickitPen *final)
307 {
308   struct TIDriver *td = (struct TIDriver *)ttd;
309 
310   /* TODO: This is all a bit of a mess
311    * Would be nicer to detect if fg/bg colour are changed, and if not, use
312    * the individual enter/exit modes from the delta pen
313    */
314 
315   run_ti(ttd, td->str.sgr, 9,
316       0, // standout
317       tickit_pen_get_bool_attr(final, TICKIT_PEN_UNDER),
318       tickit_pen_get_bool_attr(final, TICKIT_PEN_REVERSE),
319       tickit_pen_get_bool_attr(final, TICKIT_PEN_BLINK),
320       0, // dim
321       tickit_pen_get_bool_attr(final, TICKIT_PEN_BOLD),
322       0, // invisible
323       0, // protect
324       0); // alt charset
325 
326   if(tickit_pen_has_attr(delta, TICKIT_PEN_ITALIC)) {
327     if(td->str.sgr_i1 && tickit_pen_get_bool_attr(delta, TICKIT_PEN_ITALIC))
328       run_ti(ttd, td->str.sgr_i1, 0);
329     else if(td->str.sgr_i0)
330       run_ti(ttd, td->str.sgr_i0, 0);
331   }
332 
333   int c;
334   if((c = tickit_pen_get_colour_attr(final, TICKIT_PEN_FG)) > -1 &&
335       c < td->cap.colours)
336     run_ti(ttd, td->str.sgr_fg, 1, c);
337 
338   if((c = tickit_pen_get_colour_attr(final, TICKIT_PEN_BG)) > -1 &&
339       c < td->cap.colours)
340     run_ti(ttd, td->str.sgr_bg, 1, c);
341 
342   return true;
343 }
344 
getctl_int(TickitTermDriver * ttd,TickitTermCtl ctl,int * value)345 static bool getctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int *value)
346 {
347   struct TIDriver *td = (struct TIDriver *)ttd;
348 
349   switch(ctl) {
350     case TICKIT_TERMCTL_ALTSCREEN:
351       *value = td->mode.altscreen;
352       return true;
353 
354     case TICKIT_TERMCTL_CURSORVIS:
355       *value = td->mode.cursorvis;
356       return true;
357 
358     case TICKIT_TERMCTL_MOUSE:
359       *value = td->mode.mouse;
360       return true;
361 
362     case TICKIT_TERMCTL_COLORS:
363       *value = td->cap.colours;
364       return true;
365 
366     default:
367       return false;
368   }
369 }
370 
setctl_int(TickitTermDriver * ttd,TickitTermCtl ctl,int value)371 static bool setctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int value)
372 {
373   struct TIDriver *td = (struct TIDriver *)ttd;
374 
375   switch(ctl) {
376     case TICKIT_TERMCTL_ALTSCREEN:
377       if(!td->extra->enter_altscreen_mode)
378         return false;
379 
380       if(!td->mode.altscreen == !value)
381         return true;
382 
383       tickit_termdrv_write_str(ttd, value ? td->extra->enter_altscreen_mode : td->extra->exit_altscreen_mode, 0);
384       td->mode.altscreen = !!value;
385       return true;
386 
387     case TICKIT_TERMCTL_CURSORVIS:
388       if(!td->mode.cursorvis == !value)
389         return true;
390 
391       run_ti(ttd, value ? td->str.sm_csr : td->str.rm_csr, 0);
392       td->mode.cursorvis = !!value;
393       return true;
394 
395     case TICKIT_TERMCTL_MOUSE:
396       if(!td->extra->enter_mouse_mode)
397         return false;
398 
399       if(!td->mode.mouse == !value)
400         return true;
401 
402       tickit_termdrv_write_str(ttd, value ? td->extra->enter_mouse_mode : td->extra->exit_mouse_mode, 0);
403       td->mode.mouse = !!value;
404       return true;
405 
406     default:
407       return false;
408   }
409 }
410 
setctl_str(TickitTermDriver * ttd,TickitTermCtl ctl,const char * value)411 static bool setctl_str(TickitTermDriver *ttd, TickitTermCtl ctl, const char *value)
412 {
413   return false;
414 }
415 
attach(TickitTermDriver * ttd,TickitTerm * tt)416 static void attach(TickitTermDriver *ttd, TickitTerm *tt)
417 {
418   struct TIDriver *td = (struct TIDriver *)ttd;
419   unibi_term *ut = td->ut;
420 
421   tickit_term_set_size(tt, unibi_get_num(ut, unibi_lines), unibi_get_num(ut, unibi_columns));
422 }
423 
start(TickitTermDriver * ttd)424 static void start(TickitTermDriver *ttd)
425 {
426   // Nothing needed
427 }
428 
shutdown(TickitTermDriver * ttd)429 static void shutdown(TickitTermDriver *ttd)
430 {
431   struct TIDriver *td = (struct TIDriver *)ttd;
432 
433   if(td->mode.mouse)
434     tickit_termdrv_write_str(ttd, td->extra->exit_mouse_mode, 0);
435   if(!td->mode.cursorvis)
436     run_ti(ttd, td->str.sm_csr, 0);
437   if(td->mode.altscreen)
438     tickit_termdrv_write_str(ttd, td->extra->exit_altscreen_mode, 0);
439 
440   run_ti(ttd, td->str.sgr0, 0);
441 }
442 
resume(TickitTermDriver * ttd)443 static void resume(TickitTermDriver *ttd)
444 {
445   struct TIDriver *td = (struct TIDriver *)ttd;
446 
447   if(td->mode.altscreen)
448     tickit_termdrv_write_str(ttd, td->extra->enter_altscreen_mode, 0);
449   if(!td->mode.cursorvis)
450     run_ti(ttd, td->str.rm_csr, 0);
451   if(td->mode.mouse)
452     tickit_termdrv_write_str(ttd, td->extra->enter_mouse_mode, 0);
453 }
454 
destroy(TickitTermDriver * ttd)455 static void destroy(TickitTermDriver *ttd)
456 {
457   struct TIDriver *td = (struct TIDriver *)ttd;
458 
459   unibi_destroy(td->ut);
460 
461   free(td);
462 }
463 
464 static TickitTermDriverVTable ti_vtable = {
465   .attach     = attach,
466   .destroy    = destroy,
467   .start      = start,
468   .stop       = shutdown,
469   .pause      = shutdown,
470   .resume     = resume,
471   .print      = print,
472   .goto_abs   = goto_abs,
473   .move_rel   = move_rel,
474   .scrollrect = scrollrect,
475   .erasech    = erasech,
476   .clear      = clear,
477   .chpen      = chpen,
478   .getctl_int = getctl_int,
479   .setctl_int = setctl_int,
480   .setctl_str = setctl_str,
481 };
482 
new(const TickitTermProbeArgs * args)483 static TickitTermDriver *new(const TickitTermProbeArgs *args)
484 {
485   unibi_term *ut = unibi_from_term(args->termtype);
486   if(!ut)
487     return NULL;
488 
489   struct TIDriver *td = malloc(sizeof(struct TIDriver));
490   td->driver.vtable = &ti_vtable;
491 
492   td->ut = ut;
493 
494   td->mode.mouse = 0;
495   td->mode.cursorvis = 1;
496   td->mode.altscreen = 0;
497 
498   td->cap.bce = unibi_get_bool(ut, unibi_back_color_erase);
499   td->cap.colours = unibi_get_num(ut, unibi_max_colors);
500 
501   td->str.cup    = require_ti_string(td, args, unibi_cursor_address);
502   td->str.vpa    = lookup_ti_string (td, args, unibi_row_address);
503   td->str.hpa    = lookup_ti_string (td, args, unibi_column_address);
504   td->str.cuu    = require_ti_string(td, args, unibi_parm_up_cursor);
505   td->str.cuu1   = lookup_ti_string (td, args, unibi_cursor_up);
506   td->str.cud    = require_ti_string(td, args, unibi_parm_down_cursor);
507   td->str.cud1   = lookup_ti_string (td, args, unibi_cursor_down);
508   td->str.cuf    = require_ti_string(td, args, unibi_parm_right_cursor);
509   td->str.cuf1   = lookup_ti_string (td, args, unibi_cursor_right);
510   td->str.cub    = require_ti_string(td, args, unibi_parm_left_cursor);
511   td->str.cub1   = lookup_ti_string (td, args, unibi_cursor_left);
512   td->str.ich    = require_ti_string(td, args, unibi_parm_ich);
513   td->str.ich1   = lookup_ti_string (td, args, unibi_insert_character);
514   td->str.dch    = require_ti_string(td, args, unibi_parm_dch);
515   td->str.dch1   = lookup_ti_string (td, args, unibi_delete_character);
516   td->str.il     = require_ti_string(td, args, unibi_parm_insert_line);
517   td->str.il1    = lookup_ti_string (td, args, unibi_insert_line);
518   td->str.dl     = require_ti_string(td, args, unibi_parm_delete_line);
519   td->str.dl1    = lookup_ti_string (td, args, unibi_delete_line);
520   td->str.ech    = require_ti_string(td, args, unibi_erase_chars);
521   td->str.ed2    = require_ti_string(td, args, unibi_clear_screen);
522   td->str.stbm   = require_ti_string(td, args, unibi_change_scroll_region);
523   td->str.sgr    = require_ti_string(td, args, unibi_set_attributes);
524   td->str.sgr0   = require_ti_string(td, args, unibi_exit_attribute_mode);
525   td->str.sgr_i0 = lookup_ti_string (td, args, unibi_exit_italics_mode);
526   td->str.sgr_i1 = lookup_ti_string (td, args, unibi_enter_italics_mode);
527   td->str.sgr_fg = require_ti_string(td, args, unibi_set_a_foreground);
528   td->str.sgr_bg = require_ti_string(td, args, unibi_set_a_background);
529 
530   td->str.sm_csr = require_ti_string(td, args, unibi_cursor_normal);
531   td->str.rm_csr = require_ti_string(td, args, unibi_cursor_invisible);
532 
533   const char *key_mouse = lookup_ti_string(td, args, unibi_key_mouse);
534   if(key_mouse && strcmp(key_mouse, "\e[M") == 0)
535     td->extra = &extra_strings_vt200_mouse;
536   else
537     td->extra = &extra_strings_default;
538 
539   return (TickitTermDriver*)td;
540 }
541 
542 #else /* not HAVE_UNIBILIUM */
543 
new(const TickitTermProbeArgs * args)544 static TickitTermDriver *new(const TickitTermProbeArgs *args)
545 {
546   return NULL;
547 }
548 
549 #endif
550 
551 TickitTermDriverProbe tickit_termdrv_probe_ti = {
552   .new = new,
553 };
554