#include "dcs.h" #include #define LOG_MODULE "dcs" #define LOG_ENABLE_DBG 0 #include "log.h" #include "foot-terminfo.h" #include "sixel.h" #include "util.h" #include "vt.h" #include "xmalloc.h" static bool ensure_size(struct terminal *term, size_t required_size) { if (required_size <= term->vt.dcs.size) return true; uint8_t *new_data = realloc(term->vt.dcs.data, required_size); if (new_data == NULL) { LOG_ERRNO("failed to increase size of DCS buffer"); return false; } term->vt.dcs.data = new_data; term->vt.dcs.size = required_size; return true; } /* Decode hex-encoded string *inline*. NULL terminates */ static char * hex_decode(const char *s, size_t len) { if (len % 2) return NULL; char *hex = xmalloc(len / 2 + 1); char *o = hex; /* TODO: error checking */ for (size_t i = 0; i < len; i += 2) { uint8_t nib1 = hex2nibble(*s); s++; uint8_t nib2 = hex2nibble(*s); s++; if (nib1 == HEX_DIGIT_INVALID || nib2 == HEX_DIGIT_INVALID) goto err; *o = nib1 << 4 | nib2; o++; } *o = '\0'; return hex; err: free(hex); return NULL; } UNITTEST { /* Verify table is sorted */ const char *p = terminfo_capabilities; size_t left = sizeof(terminfo_capabilities); const char *last_cap = NULL; while (left > 0) { const char *cap = p; const char *val = cap + strlen(cap) + 1; size_t size = strlen(cap) + 1 + strlen(val) + 1;; xassert(size <= left); p += size; left -= size; if (last_cap != NULL) xassert(strcmp(last_cap, cap) < 0); last_cap = cap; } } static bool lookup_capability(const char *name, const char **value) { const char *p = terminfo_capabilities; size_t left = sizeof(terminfo_capabilities); while (left > 0) { const char *cap = p; const char *val = cap + strlen(cap) + 1; size_t size = strlen(cap) + 1 + strlen(val) + 1;; xassert(size <= left); p += size; left -= size; int r = strcmp(cap, name); if (r == 0) { *value = val; return true; } else if (r > 0) break; } *value = NULL; return false; } static void xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) { char *name = hex_decode(hex_cap_name, len); if (name == NULL) goto err; #if 0 const struct foot_terminfo_entry *entry = bsearch(name, terminfo_capabilities, ALEN(terminfo_capabilities), sizeof(*entry), &terminfo_entry_compar); #endif const char *value; bool valid_capability = lookup_capability(name, &value); xassert(!valid_capability || value != NULL); LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s", name, (int)len, hex_cap_name, valid_capability ? value : ""); if (!valid_capability) goto err; if (value[0] == '\0') { /* Boolean */ term_to_slave(term, "\033P1+r", 5); term_to_slave(term, hex_cap_name, len); term_to_slave(term, "\033\\", 2); goto out; } /* * Reply format: * \EP 1 + r cap=value \E\\ * Where ‘cap’ and ‘value are hex encoded ascii strings */ char *reply = xmalloc( 5 + /* DCS 1 + r (\EP1+r) */ len + /* capability name, hex encoded */ 1 + /* ‘=’ */ strlen(value) * 2 + /* capability value, hex encoded */ 2 + /* ST (\E\\) */ 1); int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name); for (const char *c = value; *c != '\0'; c++) { uint8_t nib1 = (uint8_t)*c >> 4; uint8_t nib2 = (uint8_t)*c & 0xf; reply[idx] = nib1 >= 0xa ? 'A' + nib1 - 0xa : '0' + nib1; idx++; reply[idx] = nib2 >= 0xa ? 'A' + nib2 - 0xa : '0' + nib2; idx++; } reply[idx] = '\033'; idx++; reply[idx] = '\\'; idx++; term_to_slave(term, reply, idx); free(reply); goto out; err: term_to_slave(term, "\033P0+r", 5); term_to_slave(term, hex_cap_name, len); term_to_slave(term, "\033\\", 2); out: free(name); } static void xtgettcap_put(struct terminal *term, uint8_t c) { struct vt *vt = &term->vt; /* Grow buffer expontentially */ if (vt->dcs.idx >= vt->dcs.size) { size_t new_size = vt->dcs.size * 2; if (new_size == 0) new_size = 128; if (!ensure_size(term, new_size)) return; } vt->dcs.data[vt->dcs.idx++] = c; } static void xtgettcap_unhook(struct terminal *term) { size_t left = term->vt.dcs.idx; const char *const end = (const char *)&term->vt.dcs.data[left]; const char *p = (const char *)term->vt.dcs.data; while (true) { const char *sep = memchr(p, ';', left); size_t cap_len; if (sep == NULL) { /* Last capability */ cap_len = end - p; } else { cap_len = sep - p; } xtgettcap_reply(term, p, cap_len); left -= cap_len + 1; p += cap_len + 1; if (sep == NULL) break; } } static void NOINLINE append_sgr_attr_n(char **reply, size_t *len, const char *attr, size_t n) { size_t new_len = *len + n + 1; *reply = xrealloc(*reply, new_len); memcpy(&(*reply)[*len], attr, n); (*reply)[new_len - 1] = ';'; *len = new_len; } static void decrqss_put(struct terminal *term, uint8_t c) { /* Largest request we support is two bytes */ if (!ensure_size(term, 2)) return; struct vt *vt = &term->vt; if (vt->dcs.idx > 2) return; vt->dcs.data[vt->dcs.idx++] = c; } static void decrqss_unhook(struct terminal *term) { const uint8_t *query = term->vt.dcs.data; const size_t n = term->vt.dcs.idx; /* * A note on the Ps parameter in the reply: many DEC manual * instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim * that 0 means “request is valid”, and 1 means “request is * invalid”. * * However, this appears to be a typo; actual hardware inverts the * response (as does XTerm and mlterm): * https://github.com/hackerb9/vt340test/issues/13 */ if (n == 1 && query[0] == 'r') { /* DECSTBM - Set Top and Bottom Margins */ char reply[64]; int len = snprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", term->scroll_region.start + 1, term->scroll_region.end); term_to_slave(term, reply, len); } else if (n == 1 && query[0] == 'm') { /* SGR - Set Graphic Rendition */ char *reply = NULL; size_t len = 0; #define append_sgr_attr(num_as_str) \ append_sgr_attr_n(&reply, &len, num_as_str, sizeof(num_as_str) - 1) /* Always present, both in the example from the VT510 manual * (https://vt100.net/docs/vt510-rm/DECRPSS), and in XTerm and * mlterm */ append_sgr_attr("0"); struct attributes *a = &term->vt.attrs; if (a->bold) append_sgr_attr("1"); if (a->dim) append_sgr_attr("2"); if (a->italic) append_sgr_attr("3"); if (a->underline) append_sgr_attr("4"); if (a->blink) append_sgr_attr("5"); if (a->reverse) append_sgr_attr("7"); if (a->conceal) append_sgr_attr("8"); if (a->strikethrough) append_sgr_attr("9"); switch (a->fg_src) { case COLOR_DEFAULT: break; case COLOR_BASE16: { char value[4]; int val_len = snprintf( value, sizeof(value), "%u", a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_BASE256: { char value[16]; int val_len = snprintf(value, sizeof(value), "38:5:%u", a->fg); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_RGB: { uint8_t r = a->fg >> 16; uint8_t g = a->fg >> 8; uint8_t b = a->fg >> 0; char value[32]; int val_len = snprintf( value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; } } switch (a->bg_src) { case COLOR_DEFAULT: break; case COLOR_BASE16: { char value[4]; int val_len = snprintf( value, sizeof(value), "%u", a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_BASE256: { char value[16]; int val_len = snprintf(value, sizeof(value), "48:5:%u", a->bg); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_RGB: { uint8_t r = a->bg >> 16; uint8_t g = a->bg >> 8; uint8_t b = a->bg >> 0; char value[32]; int val_len = snprintf( value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; } } #undef append_sgr_attr_n reply[len - 1] = 'm'; term_to_slave(term, "\033P1$r", 5); term_to_slave(term, reply, len); term_to_slave(term, "\033\\", 2); free(reply); } else if (n == 2 && memcmp(query, " q", 2) == 0) { /* DECSCUSR - Set Cursor Style */ int mode; switch (term->cursor_style) { case CURSOR_BLOCK: mode = 2; break; case CURSOR_UNDERLINE: mode = 4; break; case CURSOR_BEAM: mode = 6; break; default: BUG("invalid cursor style"); break; } if (term->cursor_blink.deccsusr) mode--; char reply[16]; int len = snprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); term_to_slave(term, reply, len); } else { static const char err[] = "\033P0$r\033\\"; term_to_slave(term, err, sizeof(err) - 1); } } void dcs_hook(struct terminal *term, uint8_t final) { LOG_DBG("hook: %c (intermediate(s): %.2s, param=%d)", final, (const char *)&term->vt.private, vt_param_get(term, 0, 0)); xassert(term->vt.dcs.data == NULL); xassert(term->vt.dcs.size == 0); xassert(term->vt.dcs.put_handler == NULL); xassert(term->vt.dcs.unhook_handler == NULL); switch (term->vt.private) { case 0: switch (final) { case 'q': { int p1 = vt_param_get(term, 0, 0); int p2 = vt_param_get(term, 1,0); int p3 = vt_param_get(term, 2, 0); sixel_init(term, p1, p2, p3); term->vt.dcs.put_handler = &sixel_put; term->vt.dcs.unhook_handler = &sixel_unhook; break; } } break; case '$': switch (final) { case 'q': term->vt.dcs.put_handler = &decrqss_put; term->vt.dcs.unhook_handler = &decrqss_unhook; break; } break; case '=': switch (final) { case 's': /* BSU/ESU: https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */ switch (vt_param_get(term, 0, 0)) { case 1: term->vt.dcs.unhook_handler = &term_enable_app_sync_updates; return; case 2: term->vt.dcs.unhook_handler = &term_disable_app_sync_updates; return; } break; } break; case '+': switch (final) { case 'q': /* XTGETTCAP */ term->vt.dcs.put_handler = &xtgettcap_put; term->vt.dcs.unhook_handler = &xtgettcap_unhook; break; } break; } } void dcs_put(struct terminal *term, uint8_t c) { /* LOG_DBG("PUT: %c", c); */ if (term->vt.dcs.put_handler != NULL) term->vt.dcs.put_handler(term, c); else { if (!ensure_size(term, term->vt.dcs.idx + 1)) return; term->vt.dcs.data[term->vt.dcs.idx++] = c; } } void dcs_unhook(struct terminal *term) { if (term->vt.dcs.unhook_handler != NULL) term->vt.dcs.unhook_handler(term); term->vt.dcs.unhook_handler = NULL; term->vt.dcs.put_handler = NULL; free(term->vt.dcs.data); term->vt.dcs.data = NULL; term->vt.dcs.size = 0; term->vt.dcs.idx = 0; }