1 #include "terminfo.h"
2 #include "../debug.h"
3 #include "../util/macros.h"
4 
5 #ifndef TERMINFO_DISABLE
6 
7 #include <stdint.h>
8 #include <string.h>
9 #include "key.h"
10 #include "output.h"
11 #include "terminal.h"
12 #include "xterm.h"
13 #include "../util/ascii.h"
14 #include "../util/str-util.h"
15 #include "../util/string-view.h"
16 
17 #define KEY(c, k) { \
18     .code = (c), \
19     .code_length = (sizeof(c) - 1), \
20     .key = (k) \
21 }
22 
23 #define XKEYS(p, key) \
24     KEY(p,     key | MOD_SHIFT), \
25     KEY(p "3", key | MOD_META), \
26     KEY(p "4", key | MOD_SHIFT | MOD_META), \
27     KEY(p "5", key | MOD_CTRL), \
28     KEY(p "6", key | MOD_SHIFT | MOD_CTRL), \
29     KEY(p "7", key | MOD_META | MOD_CTRL), \
30     KEY(p "8", key | MOD_SHIFT | MOD_META | MOD_CTRL)
31 
32 static struct {
33     const char *clear;
34     const char *cup;
35     const char *el;
36     const char *setab;
37     const char *setaf;
38     const char *sgr;
39 } terminfo;
40 
41 static struct TermKeyMap {
42     const char *code;
43     uint32_t code_length;
44     KeyCode key;
45 } keymap[] = {
46     KEY("kcuu1", KEY_UP),
47     KEY("kcud1", KEY_DOWN),
48     KEY("kcub1", KEY_LEFT),
49     KEY("kcuf1", KEY_RIGHT),
50     KEY("kdch1", KEY_DELETE),
51     KEY("kpp", KEY_PAGE_UP),
52     KEY("knp", KEY_PAGE_DOWN),
53     KEY("khome", KEY_HOME),
54     KEY("kend", KEY_END),
55     KEY("kich1", KEY_INSERT),
56     KEY("kcbt", MOD_SHIFT | '\t'),
57     KEY("kf1", KEY_F1),
58     KEY("kf2", KEY_F2),
59     KEY("kf3", KEY_F3),
60     KEY("kf4", KEY_F4),
61     KEY("kf5", KEY_F5),
62     KEY("kf6", KEY_F6),
63     KEY("kf7", KEY_F7),
64     KEY("kf8", KEY_F8),
65     KEY("kf9", KEY_F9),
66     KEY("kf10", KEY_F10),
67     KEY("kf11", KEY_F11),
68     KEY("kf12", KEY_F12),
69     XKEYS("kUP", KEY_UP),
70     XKEYS("kDN", KEY_DOWN),
71     XKEYS("kLFT", KEY_LEFT),
72     XKEYS("kRIT", KEY_RIGHT),
73     XKEYS("kDC", KEY_DELETE),
74     XKEYS("kPRV", KEY_PAGE_UP),
75     XKEYS("kNXT", KEY_PAGE_DOWN),
76     XKEYS("kHOM", KEY_HOME),
77     XKEYS("kEND", KEY_END),
78 };
79 
80 static_assert(ARRAY_COUNT(keymap) == 23 + (9 * 7));
81 
82 static size_t keymap_length = 0;
83 
parse_key_from_keymap(const char * buf,size_t fill,KeyCode * key)84 static ssize_t parse_key_from_keymap(const char *buf, size_t fill, KeyCode *key)
85 {
86     bool possibly_truncated = false;
87     for (size_t i = 0; i < keymap_length; i++) {
88         const struct TermKeyMap *const km = &keymap[i];
89         const char *const keycode = km->code;
90         const size_t len = km->code_length;
91         BUG_ON(keycode == NULL);
92         BUG_ON(len == 0);
93         if (len > fill) {
94             // This might be a truncated escape sequence
95             if (
96                 possibly_truncated == false
97                 && memcmp(keycode, buf, fill) == 0
98             ) {
99                 possibly_truncated = true;
100             }
101             continue;
102         }
103         if (memcmp(keycode, buf, len) != 0) {
104             continue;
105         }
106         *key = km->key;
107         return len;
108     }
109     return possibly_truncated ? -1 : 0;
110 }
111 
112 // These are normally declared in the <curses.h> and <term.h> headers.
113 // They are not included here because of the insane number of unprefixed
114 // symbols they declare and because of previous bugs caused by using them.
115 int setupterm(const char *term, int filedes, int *errret);
116 int tigetflag(const char *capname);
117 int tigetnum(const char *capname);
118 char *tigetstr(const char *capname);
119 int tputs(const char *str, int affcnt, int (*putc_fn)(int));
120 char *tparm(const char*, long, long, long, long, long, long, long, long, long);
121 #define tparm_1(str, p1) tparm(str, p1, 0, 0, 0, 0, 0, 0, 0, 0)
122 #define tparm_2(str, p1, p2) tparm(str, p1, p2, 0, 0, 0, 0, 0, 0, 0)
123 
get_terminfo_string(const char * capname)124 static char *get_terminfo_string(const char *capname)
125 {
126     char *str = tigetstr(capname);
127     if (str == (char *)-1) {
128         // Not a string cap (bug?)
129         return NULL;
130     }
131     // NULL = canceled or absent
132     return str;
133 }
134 
get_terminfo_string_view(const char * capname)135 static StringView get_terminfo_string_view(const char *capname)
136 {
137     return string_view_from_cstring(get_terminfo_string(capname));
138 }
139 
get_terminfo_flag(const char * capname)140 static bool get_terminfo_flag(const char *capname)
141 {
142     switch (tigetflag(capname)) {
143     case -1: // Not a boolean capability
144     case 0: // Canceled or absent
145         return false;
146     }
147     return true;
148 }
149 
tputs_putc(int ch)150 static int tputs_putc(int ch)
151 {
152     term_add_byte(ch);
153     return ch;
154 }
155 
tputs_control_code(StringView code)156 static void tputs_control_code(StringView code)
157 {
158     if (code.length) {
159         tputs(code.data, 1, tputs_putc);
160     }
161 }
162 
tputs_clear_screen(void)163 static void tputs_clear_screen(void)
164 {
165     if (terminfo.clear) {
166         tputs(terminfo.clear, terminal.height, tputs_putc);
167     }
168 }
169 
tputs_clear_to_eol(void)170 static void tputs_clear_to_eol(void)
171 {
172     if (terminfo.el) {
173         tputs(terminfo.el, 1, tputs_putc);
174     }
175 }
176 
tputs_move_cursor(int x,int y)177 static void tputs_move_cursor(int x, int y)
178 {
179     if (terminfo.cup) {
180         const char *seq = tparm_2(terminfo.cup, y, x);
181         if (seq) {
182             tputs(seq, 1, tputs_putc);
183         }
184     }
185 }
186 
attr_is_set(const TermColor * color,unsigned int attr)187 static bool attr_is_set(const TermColor *color, unsigned int attr)
188 {
189     if (!(color->attr & attr)) {
190         return false;
191     } else if (terminal.ncv_attributes & attr) {
192         // Terminal only allows attr when not using colors
193         return color->fg == COLOR_DEFAULT && color->bg == COLOR_DEFAULT;
194     }
195     return true;
196 }
197 
tputs_set_color(const TermColor * color)198 static void tputs_set_color(const TermColor *color)
199 {
200     if (same_color(color, &obuf.color)) {
201         return;
202     }
203 
204     if (terminfo.sgr) {
205         const char *attrs = tparm (
206             terminfo.sgr,
207             0, // p1 = "standout" (unused)
208             attr_is_set(color, ATTR_UNDERLINE),
209             attr_is_set(color, ATTR_REVERSE),
210             attr_is_set(color, ATTR_BLINK),
211             attr_is_set(color, ATTR_DIM),
212             attr_is_set(color, ATTR_BOLD),
213             attr_is_set(color, ATTR_INVIS),
214             0, // p8 = "protect" (unused)
215             0  // p9 = "altcharset" (unused)
216         );
217         tputs(attrs, 1, tputs_putc);
218     }
219 
220     TermColor c = *color;
221     if (terminfo.setaf && c.fg >= 0) {
222         const char *seq = tparm_1(terminfo.setaf, c.fg);
223         if (seq) {
224             tputs(seq, 1, tputs_putc);
225         }
226     }
227     if (terminfo.setab && c.bg >= 0) {
228         const char *seq = tparm_1(terminfo.setab, c.bg);
229         if (seq) {
230             tputs(seq, 1, tputs_putc);
231         }
232     }
233 
234     obuf.color = *color;
235 }
236 
convert_ncv_flags_to_attrs(unsigned int ncv)237 static unsigned int convert_ncv_flags_to_attrs(unsigned int ncv)
238 {
239     // These flags should have values equal to their terminfo
240     // counterparts:
241     static_assert(ATTR_UNDERLINE == 2);
242     static_assert(ATTR_REVERSE == 4);
243     static_assert(ATTR_BLINK == 8);
244     static_assert(ATTR_DIM == 16);
245     static_assert(ATTR_BOLD == 32);
246     static_assert(ATTR_INVIS == 64);
247 
248     // Mask flags to supported, common subset
249     unsigned int attrs = ncv & (
250         ATTR_UNDERLINE | ATTR_REVERSE | ATTR_BLINK
251         | ATTR_DIM | ATTR_BOLD | ATTR_INVIS
252     );
253 
254     // Italic is a special case; it occupies bit 16 in terminfo
255     // but bit 7 here
256     if (ncv & 0x8000) {
257         attrs |= ATTR_ITALIC;
258     }
259 
260     return attrs;
261 }
262 
term_init_terminfo(const char * term)263 bool term_init_terminfo(const char *term)
264 {
265     // Initialize terminfo database (or call exit(3) on failure)
266     setupterm(term, 1, (int*)0);
267 
268     terminal.put_control_code = &tputs_control_code;
269     terminal.clear_screen = &tputs_clear_screen;
270     terminal.clear_to_eol = &tputs_clear_to_eol;
271     terminal.set_color = &tputs_set_color;
272     terminal.move_cursor = &tputs_move_cursor;
273 
274     if (get_terminfo_flag("nxon")) {
275         term_init_fail (
276             "TERM type '%s' not supported: 'nxon' flag is set",
277             term
278         );
279     }
280 
281     terminfo.cup = get_terminfo_string("cup");
282     if (terminfo.cup == NULL) {
283         term_init_fail (
284             "TERM type '%s' not supported: no 'cup' capability",
285             term
286         );
287     }
288 
289     terminfo.clear = get_terminfo_string("clear");
290     terminfo.el = get_terminfo_string("el");
291     terminfo.setab = get_terminfo_string("setab");
292     terminfo.setaf = get_terminfo_string("setaf");
293     terminfo.sgr = get_terminfo_string("sgr");
294 
295     terminal.back_color_erase = get_terminfo_flag("bce");
296     terminal.width = tigetnum("cols");
297     terminal.height = tigetnum("lines");
298 
299     switch (tigetnum("colors")) {
300     case 16777216:
301         // Just use the built-in xterm_set_color() function if true color
302         // support is indicated. This bypasses tputs(3), but no true color
303         // terminal in existence actually depends on archaic tputs(3)
304         // features (like e.g. baudrate-dependant padding).
305         terminal.color_type = TERM_TRUE_COLOR;
306         terminal.set_color = &xterm_set_color;
307         break;
308     case 256:
309         terminal.color_type = TERM_256_COLOR;
310         break;
311     case 16:
312         terminal.color_type = TERM_16_COLOR;
313         break;
314     case 88:
315     case 8:
316         terminal.color_type = TERM_8_COLOR;
317         break;
318     default:
319         terminal.color_type = TERM_0_COLOR;
320         break;
321     }
322 
323     const int ncv = tigetnum("ncv");
324     if (ncv <= 0) {
325         terminal.ncv_attributes = 0;
326     } else {
327         terminal.ncv_attributes = convert_ncv_flags_to_attrs(ncv);
328     }
329 
330     terminal.control_codes = (TermControlCodes) {
331         .reset_colors = get_terminfo_string_view("op"),
332         .reset_attrs = get_terminfo_string_view("sgr0"),
333         .keypad_off = get_terminfo_string_view("rmkx"),
334         .keypad_on = get_terminfo_string_view("smkx"),
335         .cup_mode_off = get_terminfo_string_view("rmcup"),
336         .cup_mode_on = get_terminfo_string_view("smcup"),
337         .show_cursor = get_terminfo_string_view("cnorm"),
338         .hide_cursor = get_terminfo_string_view("civis")
339     };
340 
341     bool xterm_compatible_key_codes = true;
342 
343     for (size_t i = 0; i < ARRAY_COUNT(keymap); i++) {
344         const char *const code = get_terminfo_string(keymap[i].code);
345         if (code && code[0] != '\0') {
346             const size_t code_len = strlen(code);
347             const KeyCode key = keymap[i].key;
348             keymap[keymap_length++] = (struct TermKeyMap) {
349                 .code = code,
350                 .code_length = code_len,
351                 .key = key
352             };
353             KeyCode parsed_key;
354             const ssize_t parsed_len = xterm_parse_key(code, code_len, &parsed_key);
355             if (parsed_len <= 0 || parsed_key != key) {
356                 xterm_compatible_key_codes = false;
357             }
358         }
359     }
360 
361     if (!xterm_compatible_key_codes) {
362         terminal.parse_key_sequence = &parse_key_from_keymap;
363     }
364 
365     return true; // Initialization succeeded
366 }
367 
368 #else
369 
term_init_terminfo(const char * UNUSED_ARG (term))370 bool term_init_terminfo(const char* UNUSED_ARG(term))
371 {
372     return false; // terminfo not available
373 }
374 
375 #endif // ifndef TERMINFO_DISABLE
376