1 #include "termdriver.h"
2
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6
7 #define strneq(a,b,n) (strncmp(a,b,n)==0)
8
9 #define CSI_MORE_SUBPARAM 0x80000000
10 #define CSI_NEXT_SUB(x) ((x) & CSI_MORE_SUBPARAM)
11 #define CSI_PARAM(x) ((x) & ~CSI_MORE_SUBPARAM)
12
13 struct XTermDriver {
14 TickitTermDriver driver;
15
16 int dcs_offset;
17 char dcs_buffer[16];
18
19 struct {
20 unsigned int altscreen:1;
21 unsigned int cursorvis:1;
22 unsigned int cursorblink:1;
23 unsigned int cursorshape:2;
24 unsigned int mouse:2;
25 unsigned int keypad:1;
26 } mode;
27
28 struct {
29 unsigned int cursorshape:1;
30 unsigned int slrm:1;
31 unsigned int csi_sub_colon:1;
32 unsigned int rgb8:1;
33 } cap;
34
35 struct {
36 unsigned int cursorvis:1;
37 unsigned int cursorblink:1;
38 unsigned int cursorshape:2;
39 unsigned int slrm:1;
40 } initialised;
41 };
42
print(TickitTermDriver * ttd,const char * str,size_t len)43 static bool print(TickitTermDriver *ttd, const char *str, size_t len)
44 {
45 tickit_termdrv_write_str(ttd, str, len);
46 return true;
47 }
48
goto_abs(TickitTermDriver * ttd,int line,int col)49 static bool goto_abs(TickitTermDriver *ttd, int line, int col)
50 {
51 if(line != -1 && col > 0)
52 tickit_termdrv_write_strf(ttd, "\e[%d;%dH", line+1, col+1);
53 else if(line != -1 && col == 0)
54 tickit_termdrv_write_strf(ttd, "\e[%dH", line+1);
55 else if(line != -1)
56 tickit_termdrv_write_strf(ttd, "\e[%dd", line+1);
57 else if(col > 0)
58 tickit_termdrv_write_strf(ttd, "\e[%dG", col+1);
59 else if(col != -1)
60 tickit_termdrv_write_str(ttd, "\e[G", 3);
61
62 return true;
63 }
64
move_rel(TickitTermDriver * ttd,int downward,int rightward)65 static bool move_rel(TickitTermDriver *ttd, int downward, int rightward)
66 {
67 if(downward > 1)
68 tickit_termdrv_write_strf(ttd, "\e[%dB", downward);
69 else if(downward == 1)
70 tickit_termdrv_write_str(ttd, "\e[B", 3);
71 else if(downward == -1)
72 tickit_termdrv_write_str(ttd, "\e[A", 3);
73 else if(downward < -1)
74 tickit_termdrv_write_strf(ttd, "\e[%dA", -downward);
75
76 if(rightward > 1)
77 tickit_termdrv_write_strf(ttd, "\e[%dC", rightward);
78 else if(rightward == 1)
79 tickit_termdrv_write_str(ttd, "\e[C", 3);
80 else if(rightward == -1)
81 tickit_termdrv_write_str(ttd, "\e[D", 3);
82 else if(rightward < -1)
83 tickit_termdrv_write_strf(ttd, "\e[%dD", -rightward);
84
85 return true;
86 }
87
scrollrect(TickitTermDriver * ttd,const TickitRect * rect,int downward,int rightward)88 static bool scrollrect(TickitTermDriver *ttd, const TickitRect *rect, int downward, int rightward)
89 {
90 struct XTermDriver *xd = (struct XTermDriver *)ttd;
91
92 if(!downward && !rightward)
93 return true;
94
95 int term_cols;
96 tickit_term_get_size(ttd->tt, NULL, &term_cols);
97
98 int right = tickit_rect_right(rect);
99
100 /* Use DECSLRM only for 1 line of insert/delete, because any more and it's
101 * likely better to use the generic system below
102 */
103 if(((xd->cap.slrm && rect->lines == 1) || (right == term_cols))
104 && downward == 0) {
105 if(right < term_cols)
106 tickit_termdrv_write_strf(ttd, "\e[;%ds", right);
107
108 for(int line = rect->top; line < tickit_rect_bottom(rect); line++) {
109 goto_abs(ttd, line, rect->left);
110 if(rightward > 1)
111 tickit_termdrv_write_strf(ttd, "\e[%dP", rightward); /* DCH */
112 else if(rightward == 1)
113 tickit_termdrv_write_str(ttd, "\e[P", 3); /* DCH1 */
114 else if(rightward == -1)
115 tickit_termdrv_write_str(ttd, "\e[@", 3); /* ICH1 */
116 else if(rightward < -1)
117 tickit_termdrv_write_strf(ttd, "\e[%d@", -rightward); /* ICH */
118 }
119
120 if(right < term_cols)
121 tickit_termdrv_write_strf(ttd, "\e[s");
122
123 return true;
124 }
125
126 if(xd->cap.slrm ||
127 (rect->left == 0 && rect->cols == term_cols && rightward == 0)) {
128 tickit_termdrv_write_strf(ttd, "\e[%d;%dr", rect->top + 1, tickit_rect_bottom(rect));
129
130 if(rect->left > 0 || right < term_cols)
131 tickit_termdrv_write_strf(ttd, "\e[%d;%ds", rect->left + 1, right);
132
133 goto_abs(ttd, rect->top, rect->left);
134
135 if(downward > 1)
136 tickit_termdrv_write_strf(ttd, "\e[%dM", downward); /* DL */
137 else if(downward == 1)
138 tickit_termdrv_write_str(ttd, "\e[M", 3); /* DL1 */
139 else if(downward == -1)
140 tickit_termdrv_write_str(ttd, "\e[L", 3); /* IL1 */
141 else if(downward < -1)
142 tickit_termdrv_write_strf(ttd, "\e[%dL", -downward); /* IL */
143
144 if(rightward > 1)
145 tickit_termdrv_write_strf(ttd, "\e[%d'~", rightward); /* DECDC */
146 else if(rightward == 1)
147 tickit_termdrv_write_str(ttd, "\e['~", 4); /* DECDC1 */
148 else if(rightward == -1)
149 tickit_termdrv_write_str(ttd, "\e['}", 4); /* DECIC1 */
150 if(rightward < -1)
151 tickit_termdrv_write_strf(ttd, "\e[%d'}", -rightward); /* DECIC */
152
153 tickit_termdrv_write_str(ttd, "\e[r", 3);
154
155 if(rect->left > 0 || right < term_cols)
156 tickit_termdrv_write_str(ttd, "\e[s", 3);
157
158 return true;
159 }
160
161 return false;
162 }
163
erasech(TickitTermDriver * ttd,int count,TickitMaybeBool moveend)164 static bool erasech(TickitTermDriver *ttd, int count, TickitMaybeBool moveend)
165 {
166 if(count < 1)
167 return true;
168
169 /* Only use ECH if we're not in reverse-video mode. xterm doesn't do rv+ECH
170 * properly
171 */
172 if(!tickit_pen_get_bool_attr(tickit_termdrv_current_pen(ttd), TICKIT_PEN_REVERSE)) {
173 if(count == 1)
174 tickit_termdrv_write_str(ttd, "\e[X", 3);
175 else
176 tickit_termdrv_write_strf(ttd, "\e[%dX", count);
177
178 if(moveend == TICKIT_YES)
179 move_rel(ttd, 0, count);
180 }
181 else {
182 /* TODO: consider tickit_termdrv_write_chrfill(ttd, c, n)
183 */
184 char *spaces = tickit_termdrv_get_tmpbuffer(ttd, 64);
185 memset(spaces, ' ', 64);
186 while(count > 64) {
187 tickit_termdrv_write_str(ttd, spaces, 64);
188 count -= 64;
189 }
190 tickit_termdrv_write_str(ttd, spaces, count);
191
192 if(moveend == TICKIT_NO)
193 move_rel(ttd, 0, -count);
194 }
195
196 return true;
197 }
198
clear(TickitTermDriver * ttd)199 static bool clear(TickitTermDriver *ttd)
200 {
201 tickit_termdrv_write_strf(ttd, "\e[2J", 4);
202 return true;
203 }
204
205 static struct SgrOnOff { int on, off; } sgr_onoff[] = {
206 {}, /* none */
207 { 30, 39 }, /* fg */
208 { 40, 49 }, /* bg */
209 { 1, 22 }, /* bold */
210 { 4, 24 }, /* under */
211 { 3, 23 }, /* italic */
212 { 7, 27 }, /* reverse */
213 { 9, 29 }, /* strike */
214 { 10, 10 }, /* altfont */
215 { 5, 25 }, /* blink */
216 };
217
chpen(TickitTermDriver * ttd,const TickitPen * delta,const TickitPen * final)218 static bool chpen(TickitTermDriver *ttd, const TickitPen *delta, const TickitPen *final)
219 {
220 struct XTermDriver *xd = (struct XTermDriver *)ttd;
221
222 /* There can be at most 16 SGR parameters; 5 from each of 2 colours, and
223 * 6 single attributes
224 */
225 int params[16];
226 int pindex = 0;
227
228 for(TickitPenAttr attr = 1; attr < TICKIT_N_PEN_ATTRS; attr++) {
229 if(!tickit_pen_has_attr(delta, attr))
230 continue;
231
232 struct SgrOnOff *onoff = &sgr_onoff[attr];
233
234 int val;
235
236 switch(attr) {
237 case TICKIT_PEN_FG:
238 case TICKIT_PEN_BG:
239 val = tickit_pen_get_colour_attr(delta, attr);
240 if(val < 0)
241 params[pindex++] = onoff->off;
242 else if(xd->cap.rgb8 && tickit_pen_has_colour_attr_rgb8(delta, attr)) {
243 TickitPenRGB8 rgb = tickit_pen_get_colour_attr_rgb8(delta, attr);
244 params[pindex++] = (onoff->on+8) | CSI_MORE_SUBPARAM;
245 params[pindex++] = 2 | CSI_MORE_SUBPARAM;
246 params[pindex++] = rgb.r | CSI_MORE_SUBPARAM;
247 params[pindex++] = rgb.g | CSI_MORE_SUBPARAM;
248 params[pindex++] = rgb.b;
249 }
250 else if(val < 8)
251 params[pindex++] = onoff->on + val;
252 else if(val < 16)
253 params[pindex++] = onoff->on+60 + val-8;
254 else {
255 params[pindex++] = (onoff->on+8) | CSI_MORE_SUBPARAM;
256 params[pindex++] = 5 | CSI_MORE_SUBPARAM;
257 params[pindex++] = val;
258 }
259 break;
260
261 case TICKIT_PEN_UNDER:
262 val = tickit_pen_get_int_attr(delta, attr);
263 if(!val)
264 params[pindex++] = onoff->off;
265 else if(val == 1)
266 params[pindex++] = onoff->on;
267 else {
268 params[pindex++] = onoff->on | CSI_MORE_SUBPARAM;
269 params[pindex++] = val;
270 }
271 break;
272
273 case TICKIT_PEN_ALTFONT:
274 val = tickit_pen_get_int_attr(delta, attr);
275 if(val < 0 || val >= 10)
276 params[pindex++] = onoff->off;
277 else
278 params[pindex++] = onoff->on + val;
279 break;
280
281 case TICKIT_PEN_BOLD:
282 case TICKIT_PEN_ITALIC:
283 case TICKIT_PEN_REVERSE:
284 case TICKIT_PEN_STRIKE:
285 case TICKIT_PEN_BLINK:
286 val = tickit_pen_get_bool_attr(delta, attr);
287 params[pindex++] = val ? onoff->on : onoff->off;
288 break;
289
290 case TICKIT_N_PEN_ATTRS:
291 break;
292 }
293 }
294
295 if(pindex == 0)
296 return true;
297
298 /* If we're going to clear all the attributes then empty SGR is neater */
299 if(!tickit_pen_is_nondefault(final))
300 pindex = 0;
301
302 /* Render params[] into a CSI string */
303
304 size_t len = 3; /* ESC [ ... m */
305 for(int i = 0; i < pindex; i++)
306 len += snprintf(NULL, 0, "%d", CSI_PARAM(params[i])) + 1;
307 if(pindex > 0)
308 len--; /* Last one has no final separator */
309
310 char *buffer = tickit_termdrv_get_tmpbuffer(ttd, len + 1);
311 char *s = buffer;
312
313 s += sprintf(s, "\e[");
314 for(int i = 0; i < pindex-1; i++)
315 s += sprintf(s, "%d%c", CSI_PARAM(params[i]),
316 CSI_NEXT_SUB(params[i]) && xd->cap.csi_sub_colon ? ':' : ';');
317 if(pindex > 0)
318 s += sprintf(s, "%d", CSI_PARAM(params[pindex-1]));
319 sprintf(s, "m");
320
321 tickit_termdrv_write_str(ttd, buffer, len);
322
323 return true;
324 }
325
getctl_int(TickitTermDriver * ttd,TickitTermCtl ctl,int * value)326 static bool getctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int *value)
327 {
328 struct XTermDriver *xd = (struct XTermDriver *)ttd;
329
330 switch(ctl) {
331 case TICKIT_TERMCTL_ALTSCREEN:
332 *value = xd->mode.altscreen;
333 return true;
334
335 case TICKIT_TERMCTL_CURSORVIS:
336 *value = xd->mode.cursorvis;
337 return true;
338
339 case TICKIT_TERMCTL_CURSORBLINK:
340 *value = xd->mode.cursorblink;
341 return true;
342
343 case TICKIT_TERMCTL_MOUSE:
344 *value = xd->mode.mouse;
345 return true;
346
347 case TICKIT_TERMCTL_CURSORSHAPE:
348 *value = xd->mode.cursorshape;
349 return true;
350
351 case TICKIT_TERMCTL_KEYPAD_APP:
352 *value = xd->mode.keypad;
353 return true;
354
355 case TICKIT_TERMCTL_COLORS:
356 *value = xd->cap.rgb8 ? (1<<24) : 256;
357 return true;
358
359 default:
360 return false;
361 }
362 }
363
mode_for_mouse(TickitTermMouseMode mode)364 static int mode_for_mouse(TickitTermMouseMode mode)
365 {
366 switch(mode) {
367 case TICKIT_TERM_MOUSEMODE_CLICK: return 1000;
368 case TICKIT_TERM_MOUSEMODE_DRAG: return 1002;
369 case TICKIT_TERM_MOUSEMODE_MOVE: return 1003;
370
371 case TICKIT_TERM_MOUSEMODE_OFF:
372 break;
373 }
374 return 0;
375 }
376
setctl_int(TickitTermDriver * ttd,TickitTermCtl ctl,int value)377 static bool setctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int value)
378 {
379 struct XTermDriver *xd = (struct XTermDriver *)ttd;
380
381 switch(ctl) {
382 case TICKIT_TERMCTL_ALTSCREEN:
383 if(!xd->mode.altscreen == !value)
384 return true;
385
386 tickit_termdrv_write_str(ttd, value ? "\e[?1049h" : "\e[?1049l", 0);
387 xd->mode.altscreen = !!value;
388 return true;
389
390 case TICKIT_TERMCTL_CURSORVIS:
391 if(!xd->mode.cursorvis == !value)
392 return true;
393
394 tickit_termdrv_write_str(ttd, value ? "\e[?25h" : "\e[?25l", 0);
395 xd->mode.cursorvis = !!value;
396 return true;
397
398 case TICKIT_TERMCTL_CURSORBLINK:
399 if(xd->initialised.cursorblink && !xd->mode.cursorblink == !value)
400 return true;
401
402 tickit_termdrv_write_str(ttd, value ? "\e[?12h" : "\e[?12l", 0);
403 xd->mode.cursorblink = !!value;
404 return true;
405
406 case TICKIT_TERMCTL_MOUSE:
407 if(xd->mode.mouse == value)
408 return true;
409
410 /* Modes 1000, 1002 and 1003 are mutually exclusive; enabling any one
411 * disables the other two
412 */
413 if(!value)
414 tickit_termdrv_write_strf(ttd, "\e[?%dl\e[?1006l", mode_for_mouse(xd->mode.mouse));
415 else
416 tickit_termdrv_write_strf(ttd, "\e[?%dh\e[?1006h", mode_for_mouse(value));
417
418 xd->mode.mouse = value;
419 return true;
420
421 case TICKIT_TERMCTL_CURSORSHAPE:
422 if(xd->initialised.cursorshape && xd->mode.cursorshape == value)
423 return true;
424
425 if(xd->cap.cursorshape)
426 tickit_termdrv_write_strf(ttd, "\e[%d q", value * 2 + (xd->mode.cursorblink ? -1 : 0));
427 xd->mode.cursorshape = value;
428 return true;
429
430 case TICKIT_TERMCTL_KEYPAD_APP:
431 if(!xd->mode.keypad == !value)
432 return true;
433
434 tickit_termdrv_write_strf(ttd, value ? "\e=" : "\e>");
435 return true;
436
437 default:
438 return false;
439 }
440 }
441
setctl_str(TickitTermDriver * ttd,TickitTermCtl ctl,const char * value)442 static bool setctl_str(TickitTermDriver *ttd, TickitTermCtl ctl, const char *value)
443 {
444 switch(ctl) {
445 case TICKIT_TERMCTL_ICON_TEXT:
446 tickit_termdrv_write_strf(ttd, "\e]1;%s\e\\", value);
447 return true;
448
449 case TICKIT_TERMCTL_TITLE_TEXT:
450 tickit_termdrv_write_strf(ttd, "\e]2;%s\e\\", value);
451 return true;
452
453 case TICKIT_TERMCTL_ICONTITLE_TEXT:
454 tickit_termdrv_write_strf(ttd, "\e]0;%s\e\\", value);
455 return true;
456
457 default:
458 return false;
459 }
460 }
461
start(TickitTermDriver * ttd)462 static void start(TickitTermDriver *ttd)
463 {
464 // Enable DECSLRM
465 tickit_termdrv_write_strf(ttd, "\e[?69h");
466
467 // Find out if DECSLRM is actually supported
468 tickit_termdrv_write_strf(ttd, "\e[?69$p");
469
470 // Also query the current cursor visibility, blink status, and shape
471 tickit_termdrv_write_strf(ttd, "\e[?25$p\e[?12$p\eP$q q\e\\");
472
473 // Try to work out whether the terminal supports 24bit colours (RGB8) and
474 // whether it understands : to separate sub-params
475 tickit_termdrv_write_strf(ttd, "\e[38;5;255m\e[38:2:0:1:2m\eP$qm\e\\\e[m");
476
477 /* Some terminals (e.g. xfce4-terminal) don't understand DECRQM and print
478 * the raw bytes directly as output, while still claiming to be TERM=xterm
479 * It doens't hurt at this point to clear the current line just in case.
480 */
481 tickit_termdrv_write_strf(ttd, "\e[G\e[K");
482
483 tickit_term_flush(ttd->tt);
484 }
485
started(TickitTermDriver * ttd)486 static bool started(TickitTermDriver *ttd)
487 {
488 struct XTermDriver *xd = (struct XTermDriver *)ttd;
489
490 return xd->initialised.cursorvis &&
491 xd->initialised.cursorblink &&
492 xd->initialised.cursorshape &&
493 xd->initialised.slrm;
494 }
495
on_modereport(TickitTermDriver * ttd,int initial,int mode,int value)496 static int on_modereport(TickitTermDriver *ttd, int initial, int mode, int value)
497 {
498 struct XTermDriver *xd = (struct XTermDriver *)ttd;
499
500 if(initial == '?') // DEC mode
501 switch(mode) {
502 case 12: // Cursor blink
503 if(value == 1)
504 xd->mode.cursorblink = 1;
505 xd->initialised.cursorblink = 1;
506 break;
507 case 25: // DECTCEM == Cursor visibility
508 if(value == 1)
509 xd->mode.cursorvis = 1;
510 xd->initialised.cursorvis = 1;
511 break;
512 case 69: // DECVSSM
513 if(value == 1 || value == 2)
514 xd->cap.slrm = 1;
515 xd->initialised.slrm = 1;
516 break;
517 }
518
519 return 1;
520 }
521
on_decrqss(TickitTermDriver * ttd,const char * args,size_t arglen)522 static int on_decrqss(TickitTermDriver *ttd, const char *args, size_t arglen)
523 {
524 struct XTermDriver *xd = (struct XTermDriver *)ttd;
525
526 if(strneq(args + arglen - 2, " q", 2)) { // DECSCUSR
527 int value;
528 if(sscanf(args, "%d", &value)) {
529 // value==1 or 2 => shape == 1, 3 or 4 => 2, etc..
530 int shape = (value+1) / 2;
531 xd->mode.cursorshape = shape;
532 xd->cap.cursorshape = 1;
533 }
534 xd->initialised.cursorshape = 1;
535 }
536 else if(strneq(args + arglen - 1, "m", 1)) { // SGR
537 // skip the initial number, find the first separator
538 while(arglen && args[0] >= '0' && args[0] <= '9')
539 args++, arglen--;
540 if(!arglen)
541 return 1;
542
543 if(args[0] == ':')
544 xd->cap.csi_sub_colon = 1;
545 args++, arglen--;
546
547 // If the palette index is 2 then the terminal understands rgb8
548 int value;
549 if(sscanf(args, "%d", &value) && value == 2)
550 xd->cap.rgb8 = 1;
551 }
552
553 return 1;
554 }
555
teardown(TickitTermDriver * ttd)556 static void teardown(TickitTermDriver *ttd)
557 {
558 struct XTermDriver *xd = (struct XTermDriver *)ttd;
559
560 if(xd->mode.mouse)
561 tickit_termdrv_write_strf(ttd, "\e[?%dl\e[?1006l", mode_for_mouse(xd->mode.mouse));
562 if(!xd->mode.cursorvis)
563 tickit_termdrv_write_str(ttd, "\e[?25h", 0);
564 if(xd->mode.altscreen)
565 tickit_termdrv_write_str(ttd, "\e[?1049l", 0);
566 if(xd->mode.keypad)
567 tickit_termdrv_write_strf(ttd, "\e>");
568
569 // Reset pen
570 tickit_termdrv_write_str(ttd, "\e[m", 3);
571 }
572
resume(TickitTermDriver * ttd)573 static void resume(TickitTermDriver *ttd)
574 {
575 struct XTermDriver *xd = (struct XTermDriver *)ttd;
576
577 if(xd->mode.keypad)
578 tickit_termdrv_write_strf(ttd, "\e=");
579 if(xd->mode.altscreen)
580 tickit_termdrv_write_str(ttd, "\e[?1049h", 0);
581 if(!xd->mode.cursorvis)
582 tickit_termdrv_write_str(ttd, "\e[?25l", 0);
583 if(xd->mode.mouse)
584 tickit_termdrv_write_strf(ttd, "\e[?%dh\e[?1006h", mode_for_mouse(xd->mode.mouse));
585 }
586
destroy(TickitTermDriver * ttd)587 static void destroy(TickitTermDriver *ttd)
588 {
589 struct XTermDriver *xd = (struct XTermDriver *)ttd;
590
591 free(xd);
592 }
593
594 static TickitTermDriverVTable xterm_vtable = {
595 .destroy = destroy,
596 .start = start,
597 .started = started,
598 .stop = teardown,
599 .pause = teardown,
600 .resume = resume,
601 .print = print,
602 .goto_abs = goto_abs,
603 .move_rel = move_rel,
604 .scrollrect = scrollrect,
605 .erasech = erasech,
606 .clear = clear,
607 .chpen = chpen,
608 .getctl_int = getctl_int,
609 .setctl_int = setctl_int,
610 .setctl_str = setctl_str,
611 .on_modereport = on_modereport,
612 .on_decrqss = on_decrqss,
613 };
614
new(const TickitTermProbeArgs * args)615 static TickitTermDriver *new(const TickitTermProbeArgs *args)
616 {
617 const char *termtype = args->termtype;
618
619 if(strncmp(termtype, "xterm", 5) != 0)
620 return NULL;
621
622 switch(termtype[5]) {
623 case 0: case '-':
624 break;
625 default:
626 return NULL;
627 }
628
629 struct XTermDriver *xd = malloc(sizeof(struct XTermDriver));
630 xd->driver.vtable = &xterm_vtable;
631
632 xd->dcs_offset = -1;
633
634 memset(&xd->mode, 0, sizeof xd->mode);
635 xd->mode.cursorvis = 1;
636
637 memset(&xd->cap, 0, sizeof xd->cap);
638
639 memset(&xd->initialised, 0, sizeof xd->initialised);
640
641 return (TickitTermDriver*)xd;
642 }
643
644 TickitTermDriverProbe tickit_termdrv_probe_xterm = {
645 .new = new,
646 };
647