#include "csi.h" #include #include #include #if defined(_DEBUG) #include #endif #include #define LOG_MODULE "csi" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "config.h" #include "debug.h" #include "grid.h" #include "selection.h" #include "sixel.h" #include "util.h" #include "version.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" #define UNHANDLED() LOG_DBG("unhandled: %s", csi_as_string(term, final, -1)) #define UNHANDLED_SGR(idx) LOG_DBG("unhandled: %s", csi_as_string(term, 'm', idx)) static void sgr_reset(struct terminal *term) { term->vt.attrs = (struct attributes){0}; term->vt.underline = (struct underline_range_data){0}; term->bits_affecting_ascii_printer.underline_style = false; term->bits_affecting_ascii_printer.underline_color = false; term_update_ascii_printer(term); } static const char * csi_as_string(struct terminal *term, uint8_t final, int idx) { static char msg[1024]; int c = snprintf(msg, sizeof(msg), "CSI: "); for (size_t i = idx >= 0 ? idx : 0; i < (idx >= 0 ? idx + 1 : term->vt.params.idx); i++) { c += snprintf(&msg[c], sizeof(msg) - c, "%u", term->vt.params.v[i].value); for (size_t j = 0; j < term->vt.params.v[i].sub.idx; j++) { c += snprintf(&msg[c], sizeof(msg) - c, ":%u", term->vt.params.v[i].sub.value[j]); } c += snprintf(&msg[c], sizeof(msg) - c, "%s", i == term->vt.params.idx - 1 ? "" : ";"); } for (size_t i = 0; i < sizeof(term->vt.private); i++) { char value = (term->vt.private >> (i * 8)) & 0xff; if (value == 0) break; c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); } snprintf(&msg[c], sizeof(msg) - c, "%c (%u parameters)", final, idx >= 0 ? 1 : term->vt.params.idx); return msg; } static void csi_sgr(struct terminal *term) { if (term->vt.params.idx == 0) { sgr_reset(term); return; } for (size_t i = 0; i < term->vt.params.idx; i++) { const int param = term->vt.params.v[i].value; switch (param) { case 0: sgr_reset(term); break; case 1: term->vt.attrs.bold = true; break; case 2: term->vt.attrs.dim = true; break; case 3: term->vt.attrs.italic = true; break; case 4: { term->vt.attrs.underline = true; term->vt.underline.style = UNDERLINE_SINGLE; if (unlikely(term->vt.params.v[i].sub.idx == 1)) { enum underline_style style = term->vt.params.v[i].sub.value[0]; switch (style) { default: case UNDERLINE_NONE: term->vt.attrs.underline = false; term->vt.underline.style = UNDERLINE_NONE; term->bits_affecting_ascii_printer.underline_style = false; break; case UNDERLINE_SINGLE: case UNDERLINE_DOUBLE: case UNDERLINE_CURLY: case UNDERLINE_DOTTED: case UNDERLINE_DASHED: term->vt.underline.style = style; term->bits_affecting_ascii_printer.underline_style = style > UNDERLINE_SINGLE; break; } term_update_ascii_printer(term); } break; } case 5: term->vt.attrs.blink = true; break; case 6: LOG_WARN("ignored: rapid blink"); break; case 7: term->vt.attrs.reverse = true; break; case 8: term->vt.attrs.conceal = true; break; case 9: term->vt.attrs.strikethrough = true; break; case 21: term->vt.attrs.underline = true; term->vt.underline.style = UNDERLINE_DOUBLE; term->bits_affecting_ascii_printer.underline_style = true; term_update_ascii_printer(term); break; case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; case 23: term->vt.attrs.italic = false; break; case 24: { term->vt.attrs.underline = false; term->vt.underline.style = UNDERLINE_NONE; term->bits_affecting_ascii_printer.underline_style = false; term_update_ascii_printer(term); break; } case 25: term->vt.attrs.blink = false; break; case 26: break; /* rapid blink, ignored */ case 27: term->vt.attrs.reverse = false; break; case 28: term->vt.attrs.conceal = false; break; case 29: term->vt.attrs.strikethrough = false; break; /* Regular foreground colors */ case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: term->vt.attrs.fg_src = COLOR_BASE16; term->vt.attrs.fg = param - 30; break; case 38: case 48: case 58: { uint32_t color; enum color_source src; /* Indexed: 38;5; */ if (term->vt.params.idx - i - 1 >= 2 && term->vt.params.v[i + 1].value == 5) { src = COLOR_BASE256; color = min(term->vt.params.v[i + 2].value, ALEN(term->colors.table) - 1); i += 2; } /* RGB: 38;2;;; */ else if (term->vt.params.idx - i - 1 >= 4 && term->vt.params.v[i + 1].value == 2) { uint8_t r = term->vt.params.v[i + 2].value; uint8_t g = term->vt.params.v[i + 3].value; uint8_t b = term->vt.params.v[i + 4].value; src = COLOR_RGB; color = r << 16 | g << 8 | b; i += 4; } /* Indexed: 38:5: */ else if (term->vt.params.v[i].sub.idx >= 2 && term->vt.params.v[i].sub.value[0] == 5) { src = COLOR_BASE256; color = min(term->vt.params.v[i].sub.value[1], ALEN(term->colors.table) - 1); } /* * RGB: 38:2::r:g:b[:ignored:tolerance:tolerance-color-space] * RGB: 38:2:r:g:b * * The second version is a "bastard" version - many * programs "forget" the color space ID * parameter... *sigh* */ else if (term->vt.params.v[i].sub.idx >= 4 && term->vt.params.v[i].sub.value[0] == 2) { const struct vt_param *param = &term->vt.params.v[i]; bool have_color_space_id = param->sub.idx >= 5; /* 0 - color space (ignored) */ int r_idx = 2 - !have_color_space_id; int g_idx = 3 - !have_color_space_id; int b_idx = 4 - !have_color_space_id; /* 5 - unused */ /* 6 - CS tolerance */ /* 7 - color space associated with tolerance */ uint8_t r = param->sub.value[r_idx]; uint8_t g = param->sub.value[g_idx]; uint8_t b = param->sub.value[b_idx]; src = COLOR_RGB; color = r << 16 | g << 8 | b; } /* Transparent: 38:1 */ /* CMY: 38:3::c:m:y[:tolerance:tolerance-color-space] */ /* CMYK: 38:4::c:m:y:k[:tolerance:tolerance-color-space] */ /* Unrecognized */ else { UNHANDLED_SGR(i); break; } if (unlikely(param == 58)) { term->vt.underline.color_src = src; term->vt.underline.color = color; term->bits_affecting_ascii_printer.underline_color = true; term_update_ascii_printer(term); } else if (param == 38) { term->vt.attrs.fg_src = src; term->vt.attrs.fg = color; } else { xassert(param == 48); term->vt.attrs.bg_src = src; term->vt.attrs.bg = color; } break; } case 39: term->vt.attrs.fg_src = COLOR_DEFAULT; break; /* Regular background colors */ case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: term->vt.attrs.bg_src = COLOR_BASE16; term->vt.attrs.bg = param - 40; break; case 49: term->vt.attrs.bg_src = COLOR_DEFAULT; break; case 59: term->vt.underline.color_src = COLOR_DEFAULT; term->vt.underline.color = 0; term->bits_affecting_ascii_printer.underline_color = false; term_update_ascii_printer(term); break; /* Bright foreground colors */ case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97: term->vt.attrs.fg_src = COLOR_BASE16; term->vt.attrs.fg = param - 90 + 8; break; /* Bright background colors */ case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: term->vt.attrs.bg_src = COLOR_BASE16; term->vt.attrs.bg = param - 100 + 8; break; default: UNHANDLED_SGR(i); break; } } } static void decset_decrst(struct terminal *term, unsigned param, bool enable) { #if defined(_DEBUG) /* For UNHANDLED() */ int UNUSED final = enable ? 'h' : 'l'; #endif /* Note: update XTSAVE/XTRESTORE if adding/removing things here */ switch (param) { case 1: /* DECCKM */ term->cursor_keys_mode = enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; break; case 5: /* DECSCNM */ term->reverse = enable; term_damage_all(term); term_damage_margins(term); break; case 6: { /* DECOM */ term->origin = enable ? ORIGIN_RELATIVE : ORIGIN_ABSOLUTE; term_cursor_home(term); break; } case 7: /* DECAWM */ term->auto_margin = enable; term->grid->cursor.lcf = false; break; case 9: if (enable) LOG_WARN("unimplemented: X10 mouse tracking mode"); #if 0 else if (term->mouse_tracking == MOUSE_X10) term->mouse_tracking = MOUSE_NONE; #endif break; case 12: term->cursor_blink.decset = enable; term_cursor_blink_update(term); break; case 25: /* DECTCEM */ term->hide_cursor = !enable; break; case 45: term->reverse_wrap = enable; break; case 66: /* DECNKM */ term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; break; case 67: if (enable) LOG_WARN("unimplemented: DECBKM"); break; case 80: term->sixel.scrolling = !enable; break; case 1000: if (enable) term->mouse_tracking = MOUSE_CLICK; else if (term->mouse_tracking == MOUSE_CLICK) term->mouse_tracking = MOUSE_NONE; term_xcursor_update(term); break; case 1001: if (enable) LOG_WARN("unimplemented: highlight mouse tracking"); break; case 1002: if (enable) term->mouse_tracking = MOUSE_DRAG; else if (term->mouse_tracking == MOUSE_DRAG) term->mouse_tracking = MOUSE_NONE; term_xcursor_update(term); break; case 1003: if (enable) term->mouse_tracking = MOUSE_MOTION; else if (term->mouse_tracking == MOUSE_MOTION) term->mouse_tracking = MOUSE_NONE; term_xcursor_update(term); break; case 1004: term->focus_events = enable; break; case 1005: if (enable) LOG_WARN("unimplemented: mouse reporting mode: UTF-8"); #if 0 else if (term->mouse_reporting == MOUSE_UTF8) term->mouse_reporting = MOUSE_NONE; #endif break; case 1006: if (enable) term->mouse_reporting = MOUSE_SGR; else if (term->mouse_reporting == MOUSE_SGR) term->mouse_reporting = MOUSE_NORMAL; break; case 1007: term->alt_scrolling = enable; break; case 1015: if (enable) term->mouse_reporting = MOUSE_URXVT; else if (term->mouse_reporting == MOUSE_URXVT) term->mouse_reporting = MOUSE_NORMAL; break; case 1016: if (enable) term->mouse_reporting = MOUSE_SGR_PIXELS; else if (term->mouse_reporting == MOUSE_SGR_PIXELS) term->mouse_reporting = MOUSE_NORMAL; break; case 1034: /* smm */ LOG_DBG("%s 8-bit meta mode", enable ? "enabling" : "disabling"); term->meta.eight_bit = enable; break; case 1035: /* numLock */ LOG_DBG("%s Num Lock modifier", enable ? "enabling" : "disabling"); term->num_lock_modifier = enable; break; case 1036: /* metaSendsEscape */ LOG_DBG("%s meta-sends-escape", enable ? "enabling" : "disabling"); term->meta.esc_prefix = enable; break; case 1042: term->bell_action_enabled = enable; break; #if 0 case 1043: LOG_WARN("unimplemented: raise window on ctrl-g"); break; #endif case 1048: if (enable) term_save_cursor(term); else term_restore_cursor(term, &term->grid->saved_cursor); break; case 47: case 1047: case 1049: if (enable && term->grid != &term->alt) { selection_cancel(term); if (param == 1049) term_save_cursor(term); term->grid = &term->alt; /* Cursor retains its position from the normal grid */ term_cursor_to( term, min(term->normal.cursor.point.row, term->rows - 1), min(term->normal.cursor.point.col, term->cols - 1)); tll_free(term->normal.scroll_damage); term_erase(term, 0, 0, term->rows - 1, term->cols - 1); } else if (!enable && term->grid == &term->alt) { selection_cancel(term); term->grid = &term->normal; /* Cursor retains its position from the alt grid */ term_cursor_to( term, min(term->alt.cursor.point.row, term->rows - 1), min(term->alt.cursor.point.col, term->cols - 1)); if (param == 1049) term_restore_cursor(term, &term->grid->saved_cursor); /* Delete all sixel images on the alt screen */ tll_foreach(term->alt.sixel_images, it) { sixel_destroy(&it->item); tll_remove(term->alt.sixel_images, it); } tll_free(term->alt.scroll_damage); term_damage_view(term); } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); term_remove_all_multi_cursors(term); break; case 1070: term->sixel.use_private_palette = enable; break; case 2004: term->bracketed_paste = enable; break; case 2026: if (enable) term_enable_app_sync_updates(term); else term_disable_app_sync_updates(term); break; case 2027: #if defined(FOOT_GRAPHEME_CLUSTERING) term->grapheme_shaping = enable; #endif break; case 2031: term->report_theme_changes = enable; break; case 2048: if (enable) term_enable_size_notifications(term); else term_disable_size_notifications(term); break; case 8452: term->sixel.cursor_right_of_graphics = enable; break; case 737769: if (enable) term_ime_enable(term); else { term_ime_disable(term); term->ime_reenable_after_url_mode = false; } break; default: UNHANDLED(); break; } } static void decset(struct terminal *term, unsigned param) { decset_decrst(term, param, true); } static void decrst(struct terminal *term, unsigned param) { decset_decrst(term, param, false); } /* * These values represent the current state of a DEC private mode, * as returned in the DECRPM reply to a DECRQM query. */ enum decrpm_status { DECRPM_NOT_RECOGNIZED = 0, DECRPM_SET = 1, DECRPM_RESET = 2, DECRPM_PERMANENTLY_SET = 3, DECRPM_PERMANENTLY_RESET = 4, }; static enum decrpm_status decrpm(bool enabled) { return enabled ? DECRPM_SET : DECRPM_RESET; } static enum decrpm_status decrqm(const struct terminal *term, unsigned param) { switch (param) { case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); case 5: return decrpm(term->reverse); case 6: return decrpm(term->origin); case 7: return decrpm(term->auto_margin); case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ case 12: return decrpm(term->cursor_blink.decset); case 25: return decrpm(!term->hide_cursor); case 45: return decrpm(term->reverse_wrap); case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); case 67: return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM */ case 80: return decrpm(!term->sixel.scrolling); case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); case 1001: return DECRPM_PERMANENTLY_RESET; case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); case 1004: return decrpm(term->focus_events); case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); case 1007: return decrpm(term->alt_scrolling); case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); case 1034: return decrpm(term->meta.eight_bit); case 1035: return decrpm(term->num_lock_modifier); case 1036: return decrpm(term->meta.esc_prefix); case 1042: return decrpm(term->bell_action_enabled); case 47: /* FALLTHROUGH */ case 1047: /* FALLTHROUGH */ case 1049: return decrpm(term->grid == &term->alt); case 1070: return decrpm(term->sixel.use_private_palette); case 2004: return decrpm(term->bracketed_paste); case 2026: return decrpm(term->render.app_sync_updates.enabled); case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE ? DECRPM_PERMANENTLY_RESET : decrpm(term->grapheme_shaping); case 2031: return decrpm(term->report_theme_changes); case 2048: return decrpm(term->size_notifications); case 8452: return decrpm(term->sixel.cursor_right_of_graphics); case 737769: return decrpm(term_ime_is_enabled(term)); } return DECRPM_NOT_RECOGNIZED; } static void xtsave(struct terminal *term, unsigned param) { switch (param) { case 1: term->xtsave.application_cursor_keys = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; break; case 5: term->xtsave.reverse = term->reverse; break; case 6: term->xtsave.origin = term->origin; break; case 7: term->xtsave.auto_margin = term->auto_margin; break; case 9: /* term->xtsave.mouse_x10 = term->mouse_tracking == MOUSE_X10; */ break; case 12: term->xtsave.cursor_blink = term->cursor_blink.decset; break; case 25: term->xtsave.show_cursor = !term->hide_cursor; break; case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; case 66: term->xtsave.application_keypad_keys = term->keypad_keys_mode == KEYPAD_APPLICATION; break; case 67: break; case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; case 1001: break; case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; case 1003: term->xtsave.mouse_motion = term->mouse_tracking == MOUSE_MOTION; break; case 1004: term->xtsave.focus_events = term->focus_events; break; case 1005: /* term->xtsave.mouse_utf8 = term->mouse_reporting == MOUSE_UTF8; */ break; case 1006: term->xtsave.mouse_sgr = term->mouse_reporting == MOUSE_SGR; break; case 1007: term->xtsave.alt_scrolling = term->alt_scrolling; break; case 1015: term->xtsave.mouse_urxvt = term->mouse_reporting == MOUSE_URXVT; break; case 1016: term->xtsave.mouse_sgr_pixels = term->mouse_reporting == MOUSE_SGR_PIXELS; break; case 1034: term->xtsave.meta_eight_bit = term->meta.eight_bit; break; case 1035: term->xtsave.num_lock_modifier = term->num_lock_modifier; break; case 1036: term->xtsave.meta_esc_prefix = term->meta.esc_prefix; break; case 1042: term->xtsave.bell_action_enabled = term->bell_action_enabled; break; case 1047: term->xtsave.alt_screen = term->grid == &term->alt; break; case 1048: term_save_cursor(term); break; case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; case 1070: term->xtsave.sixel_private_palette = term->sixel.use_private_palette; break; case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; case 2031: term->xtsave.report_theme_changes = term->report_theme_changes; break; case 2048: term->xtsave.size_notifications = term->size_notifications; break; case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; } } static void xtrestore(struct terminal *term, unsigned param) { bool enable; switch (param) { case 1: enable = term->xtsave.application_cursor_keys; break; case 5: enable = term->xtsave.reverse; break; case 6: enable = term->xtsave.origin; break; case 7: enable = term->xtsave.auto_margin; break; case 9: /* enable = term->xtsave.mouse_x10; break; */ return; case 12: enable = term->xtsave.cursor_blink; break; case 25: enable = term->xtsave.show_cursor; break; case 45: enable = term->xtsave.reverse_wrap; break; case 47: enable = term->xtsave.alt_screen; break; case 66: enable = term->xtsave.application_keypad_keys; break; case 67: return; case 80: enable = term->xtsave.sixel_display_mode; break; case 1000: enable = term->xtsave.mouse_click; break; case 1001: return; case 1002: enable = term->xtsave.mouse_drag; break; case 1003: enable = term->xtsave.mouse_motion; break; case 1004: enable = term->xtsave.focus_events; break; case 1005: /* enable = term->xtsave.mouse_utf8; break; */ return; case 1006: enable = term->xtsave.mouse_sgr; break; case 1007: enable = term->xtsave.alt_scrolling; break; case 1015: enable = term->xtsave.mouse_urxvt; break; case 1016: enable = term->xtsave.mouse_sgr_pixels; break; case 1034: enable = term->xtsave.meta_eight_bit; break; case 1035: enable = term->xtsave.num_lock_modifier; break; case 1036: enable = term->xtsave.meta_esc_prefix; break; case 1042: enable = term->xtsave.bell_action_enabled; break; case 1047: enable = term->xtsave.alt_screen; break; case 1048: enable = true; break; case 1049: enable = term->xtsave.alt_screen; break; case 1070: enable = term->xtsave.sixel_private_palette; break; case 2004: enable = term->xtsave.bracketed_paste; break; case 2026: enable = term->xtsave.app_sync_updates; break; case 2027: enable = term->xtsave.grapheme_shaping; break; case 2031: enable = term->xtsave.report_theme_changes; break; case 2048: enable = term->xtsave.size_notifications; break; case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; case 737769: enable = term->xtsave.ime; break; default: return; } decset_decrst(term, param, enable); } static bool params_to_rectangular_area(const struct terminal *term, int first_idx, int *top, int *left, int *bottom, int *right) { int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); if (rel_top > rel_bottom || *left > *right) return false; *top = term_row_rel_to_abs(term, rel_top); *bottom = term_row_rel_to_abs(term, rel_bottom); return true; } void csi_dispatch(struct terminal *term, uint8_t final) { LOG_DBG("%s (%08x)", csi_as_string(term, final, -1), term->vt.private); switch (term->vt.private) { case 0: { switch (final) { case 'b': if (term->vt.last_printed != 0) { /* * Note: we never reset 'last-printed'. According to * ECMA-48, the behaviour is undefined if REP was * _not_ preceded by a graphical character. */ int count = vt_param_get(term, 0, 1); LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); int width; if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { const struct composed *comp = composed_lookup( term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); xassert(comp != NULL); width = comp->forced_width > 0 ? comp->forced_width : comp->width; } else width = c32width(term->vt.last_printed); if (width > 0) { for (int i = 0; i < count; i++) term_print(term, term->vt.last_printed, width, false); } } break; case 'c': { if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } /* Send Device Attributes (Primary DA) */ /* * Responses: * - CSI?1;2c vt100 with advanced video option * - CSI?1;0c vt101 with no options * - CSI?6c vt102 * - CSI?62;c vt220 * - CSI?63;c vt320 * - CSI?64;c vt420 * * Ps (response may contain multiple): * - 1 132 columns * - 2 Printer. * - 3 ReGIS graphics. * - 4 Sixel graphics. * - 6 Selective erase. * - 8 User-defined keys. * - 9 National Replacement Character sets. * - 15 Technical characters. * - 16 Locator port. * - 17 Terminal state interrogation. * - 18 User windows. * - 21 Horizontal scrolling. * - 22 ANSI color, e.g., VT525. * - 28 Rectangular editing. * - 29 ANSI text locator (i.e., DEC Locator mode). * - 52 Clipboard access * * Note: we report ourselves as a VT220, mainly to be able * to pass parameters, to indicate we support sixel, and * ANSI colors. * * The VT level must be synchronized with the secondary DA * response. * * Note: tertiary DA responds with "FOOT". */ char reply[32]; int len = snprintf( reply, sizeof(reply), "\033[?62%s;22;28%sc", term->conf->tweak.sixel ? ";4" : "", (term->conf->security.osc52 == OSC52_ENABLED || term->conf->security.osc52 == OSC52_COPY_ENABLED ? ";52" : "")); term_to_slave(term, reply, len); break; } case 'd': { /* VPA - vertical line position absolute */ int rel_row = vt_param_get(term, 0, 1) - 1; int row = term_row_rel_to_abs(term, rel_row); term_cursor_to(term, row, term->grid->cursor.point.col); break; } case 'm': csi_sgr(term); break; case 'A': term_cursor_up(term, vt_param_get(term, 0, 1)); break; case 'e': case 'B': term_cursor_down(term, vt_param_get(term, 0, 1)); break; case 'a': case 'C': term_cursor_right(term, vt_param_get(term, 0, 1)); break; case 'D': term_cursor_left(term, vt_param_get(term, 0, 1)); break; case 'E': /* CNL - Cursor Next Line */ term_cursor_down(term, vt_param_get(term, 0, 1)); term_cursor_left(term, term->grid->cursor.point.col); break; case 'F': /* CPL - Cursor Previous Line */ term_cursor_up(term, vt_param_get(term, 0, 1)); term_cursor_left(term, term->grid->cursor.point.col); break; case 'g': { int param = vt_param_get(term, 0, 0); switch (param) { case 0: /* Clear tab stop at *current* column */ tll_foreach(term->tab_stops, it) { if (it->item == term->grid->cursor.point.col) tll_remove(term->tab_stops, it); else if (it->item > term->grid->cursor.point.col) break; } break; case 3: /* Clear *all* tabs */ tll_free(term->tab_stops); break; default: UNHANDLED(); break; } break; } case '`': case 'G': { /* Cursor horizontal absolute */ int col = min(vt_param_get(term, 0, 1), term->cols) - 1; term_cursor_col(term, col); break; } case 'f': case 'H': { /* Move cursor */ int rel_row = vt_param_get(term, 0, 1) - 1; int row = term_row_rel_to_abs(term, rel_row); int col = min(vt_param_get(term, 1, 1), term->cols) - 1; term_cursor_to(term, row, col); break; } case 'J': { /* Erase screen */ int param = vt_param_get(term, 0, 0); switch (param) { case 0: { /* From cursor to end of screen */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, term->rows - 1, term->cols - 1); term->grid->cursor.lcf = false; break; } case 1: { /* From start of screen to cursor */ const struct coord *cursor = &term->grid->cursor.point; term_erase(term, 0, 0, cursor->row, cursor->col); term->grid->cursor.lcf = false; break; } case 2: /* Erase entire screen */ term_erase(term, 0, 0, term->rows - 1, term->cols - 1); term->grid->cursor.lcf = false; term_remove_all_multi_cursors(term); break; case 3: { /* Erase scrollback */ term_erase_scrollback(term); term_remove_all_multi_cursors(term); break; } default: UNHANDLED(); break; } break; } case 'K': { /* Erase line */ int param = vt_param_get(term, 0, 0); switch (param) { case 0: { /* From cursor to end of line */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, cursor->row, term->cols - 1); term->grid->cursor.lcf = false; break; } case 1: { /* From start of line to cursor */ const struct coord *cursor = &term->grid->cursor.point; term_erase(term, cursor->row, 0, cursor->row, cursor->col); term->grid->cursor.lcf = false; break; } case 2: { /* Entire line */ const struct coord *cursor = &term->grid->cursor.point; term_erase(term, cursor->row, 0, cursor->row, term->cols - 1); term->grid->cursor.lcf = false; break; } default: UNHANDLED(); break; } break; } case 'L': { /* IL */ if (term->grid->cursor.point.row < term->scroll_region.start || term->grid->cursor.point.row >= term->scroll_region.end) break; int count = min( vt_param_get(term, 0, 1), term->scroll_region.end - term->grid->cursor.point.row); term_scroll_reverse_partial( term, (struct scroll_region){ .start = term->grid->cursor.point.row, .end = term->scroll_region.end}, count); term->grid->cursor.lcf = false; term->grid->cursor.point.col = 0; break; } case 'M': { /* DL */ if (term->grid->cursor.point.row < term->scroll_region.start || term->grid->cursor.point.row >= term->scroll_region.end) break; int count = min( vt_param_get(term, 0, 1), term->scroll_region.end - term->grid->cursor.point.row); term_scroll_partial( term, (struct scroll_region){ .start = term->grid->cursor.point.row, .end = term->scroll_region.end}, count); term->grid->cursor.lcf = false; term->grid->cursor.point.col = 0; break; } case 'P': { /* DCH: Delete character(s) */ /* Number of characters to delete */ int count = min( vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); /* Number of characters left after deletion (on current line) */ int remaining = term->cols - (term->grid->cursor.point.col + count); /* 'Delete' characters by moving the remaining ones */ memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col], &term->grid->cur_row->cells[term->grid->cursor.point.col + count], remaining * sizeof(term->grid->cur_row->cells[0])); for (size_t c = 0; c < remaining; c++) term->grid->cur_row->cells[term->grid->cursor.point.col + c].attrs.clean = 0; term->grid->cur_row->dirty = true; /* Erase the remainder of the line */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col + remaining, cursor->row, term->cols - 1); term->grid->cursor.lcf = false; break; } case '@': { /* ICH: insert character(s) */ /* Number of characters to insert */ int count = min( vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); /* Characters to move */ int remaining = term->cols - (term->grid->cursor.point.col + count); /* Push existing characters */ memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col + count], &term->grid->cur_row->cells[term->grid->cursor.point.col], remaining * sizeof(term->grid->cur_row->cells[0])); for (size_t c = 0; c < remaining; c++) term->grid->cur_row->cells[term->grid->cursor.point.col + count + c].attrs.clean = 0; term->grid->cur_row->dirty = true; /* Erase (insert space characters) */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, cursor->row, cursor->col + count - 1); term->grid->cursor.lcf = false; break; } case 'S': { const struct scroll_region *r = &term->scroll_region; int amount = min(vt_param_get(term, 0, 1), r->end - r->start); term_scroll(term, amount); break; } case 'T': { const struct scroll_region *r = &term->scroll_region; int amount = min(vt_param_get(term, 0, 1), r->end - r->start); term_scroll_reverse(term, amount); break; } case 'X': { /* Erase chars */ int count = min( vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, cursor->row, cursor->col + count - 1); term->grid->cursor.lcf = false; break; } case 'I': { /* CHT - Tab Forward (param is number of tab stops to move through) */ for (int i = 0; i < vt_param_get(term, 0, 1); i++) { int new_col = term->cols - 1; tll_foreach(term->tab_stops, it) { if (it->item > term->grid->cursor.point.col) { new_col = it->item; break; } } xassert(new_col >= term->grid->cursor.point.col); bool lcf = term->grid->cursor.lcf; term_cursor_right(term, new_col - term->grid->cursor.point.col); term->grid->cursor.lcf = lcf; } break; } case 'Z': /* CBT - Back tab (param is number of tab stops to move back through) */ for (int i = 0; i < vt_param_get(term, 0, 1); i++) { int new_col = 0; tll_rforeach(term->tab_stops, it) { if (it->item < term->grid->cursor.point.col) { new_col = it->item; break; } } xassert(term->grid->cursor.point.col >= new_col); term_cursor_left(term, term->grid->cursor.point.col - new_col); } break; case 'h': case 'l': { /* Set/Reset Mode (SM/RM) */ int param = vt_param_get(term, 0, 0); bool sm = final == 'h'; if (param == 4) { /* Insertion Replacement Mode (IRM) */ term->insert_mode = sm; term->bits_affecting_ascii_printer.insert_mode = sm; term_update_ascii_printer(term); break; } /* * ECMA-48 defines modes 1-22, all of which were optional * (§7.1; "may have one state only") and are considered * deprecated (§7.1) in the latest (5th) edition. xterm only * documents modes 2, 4, 12 and 20, the last of which was * outright removed (§8.3.106) in 5th edition ECMA-48. */ if (sm) { LOG_WARN("SM with unimplemented mode: %d", param); } break; } case 'r': { int start = vt_param_get(term, 0, 1); int end = min(vt_param_get(term, 1, term->rows), term->rows); if (end > start) { /* 1-based */ term->scroll_region.start = start - 1; term->scroll_region.end = end; term_cursor_home(term); LOG_DBG("scroll region: %d-%d", term->scroll_region.start, term->scroll_region.end); } break; } case 's': term_save_cursor(term); break; case 'u': term_restore_cursor(term, &term->grid->saved_cursor); break; case 't': { /* * Window operations */ const unsigned param = vt_param_get(term, 0, 0); switch (param) { case 1: LOG_WARN("unimplemented: de-iconify"); break; case 2: LOG_WARN("unimplemented: iconify"); break; case 3: LOG_WARN("unimplemented: move window to pixel position"); break; case 4: LOG_WARN("unimplemented: resize window in pixels"); break; case 5: LOG_WARN("unimplemented: raise window to front of stack"); break; case 6: LOG_WARN("unimplemented: raise window to back of stack"); break; case 7: LOG_WARN("unimplemented: refresh window"); break; case 8: LOG_WARN("unimplemented: resize window in chars"); break; case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; case 10: LOG_WARN("unimplemented: to/from full screen"); break; case 20: LOG_WARN("unimplemented: report icon label"); break; case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; case 11: /* report if window is iconified */ /* We don't know - always report *not* iconified */ /* 1=not iconified, 2=iconified */ term_to_slave(term, "\033[1t", 4); break; case 13: { /* report window position */ /* We don't know our position - always report (0,0) */ static const char reply[] = "\033[3;0;0t"; switch (vt_param_get(term, 1, 0)) { case 0: /* window position */ case 2: /* text area position */ term_to_slave(term, reply, sizeof(reply) - 1); break; default: UNHANDLED(); break; } break; } case 14: { /* report window size in pixels */ int width = -1; int height = -1; switch (vt_param_get(term, 1, 0)) { case 0: /* text area size */ width = term->width - term->margins.left - term->margins.right; height = term->height - term->margins.top - term->margins.bottom; break; case 2: /* window size */ width = term->width; height = term->height; break; default: UNHANDLED(); break; } if (width >= 0 && height >= 0) { char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[4;%d;%dt", height, width); term_to_slave(term, reply, n); } break; } case 15: /* report screen size in pixels */ tll_foreach(term->window->on_outputs, it) { char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", it->item->dim.px_real.height, it->item->dim.px_real.width); term_to_slave(term, reply, n); break; } if (tll_length(term->window->on_outputs) == 0) term_to_slave(term, "\033[5;0;0t", 8); break; case 16: { /* report cell size in pixels */ char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[6;%d;%dt", term->cell_height, term->cell_width); term_to_slave(term, reply, n); break; } case 18: { /* text area size in chars */ char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[8;%d;%dt", term->rows, term->cols); term_to_slave(term, reply, n); break; } case 19: { /* report screen size in chars */ tll_foreach(term->window->on_outputs, it) { char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[9;%d;%dt", it->item->dim.px_real.height / term->cell_height, it->item->dim.px_real.width / term->cell_width); term_to_slave(term, reply, n); break; } if (tll_length(term->window->on_outputs) == 0) term_to_slave(term, "\033[9;0;0t", 8); break; } case 21: { #if 0 /* Disabled for now, see #1894 */ char reply[3 + strlen(term->window_title) + 2 + 1]; int chars = xsnprintf( reply, sizeof(reply), "\033]l%s\033\\", term->window_title); term_to_slave(term, reply, chars); #else LOG_WARN("CSI 21 t (report window title) ignored"); #endif break; } case 22: { /* push window title */ /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); if (what == 0 || what == 2) { tll_push_back( term->window_title_stack, xstrdup(term->window_title)); } break; } case 23: { /* pop window title */ /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); if (what == 0 || what == 2) { if (tll_length(term->window_title_stack) > 0) { char *title = tll_pop_back(term->window_title_stack); term_set_window_title(term, title); free(title); } } break; } case 1001: { } default: LOG_DBG("ignoring %s", csi_as_string(term, final, -1)); break; } break; } case 'n': { if (term->vt.params.idx > 0) { int param = vt_param_get(term, 0, 0); switch (param) { case 5: /* Query device status */ term_to_slave(term, "\x1b[0n", 4); /* "Device OK" */ break; case 6: { /* u7 - cursor position query */ int row = term->origin == ORIGIN_ABSOLUTE ? term->grid->cursor.point.row : term->grid->cursor.point.row - term->scroll_region.start; /* TODO: we use 0-based position, while the xterm * terminfo says the receiver of the reply should * decrement, hence we must add 1 */ char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\x1b[%d;%dR", row + 1, term->grid->cursor.point.col + 1); term_to_slave(term, reply, n); break; } default: UNHANDLED(); break; } } else UNHANDLED(); break; } default: UNHANDLED(); break; } break; /* private[0] == 0 */ } case '?': { switch (final) { case 'h': /* DECSET - DEC private mode set */ for (size_t i = 0; i < term->vt.params.idx; i++) decset(term, term->vt.params.v[i].value); break; case 'l': /* DECRST - DEC private mode reset */ for (size_t i = 0; i < term->vt.params.idx; i++) decrst(term, term->vt.params.v[i].value); break; case 's': for (size_t i = 0; i < term->vt.params.idx; i++) xtsave(term, term->vt.params.v[i].value); break; case 'r': for (size_t i = 0; i < term->vt.params.idx; i++) xtrestore(term, term->vt.params.v[i].value); break; case 'S': { if (!term->conf->tweak.sixel) { UNHANDLED(); break; } unsigned target = vt_param_get(term, 0, 0); unsigned operation = vt_param_get(term, 1, 0); switch (target) { case 1: switch (operation) { case 1: sixel_colors_report_current(term); break; case 2: sixel_colors_reset(term); break; case 3: sixel_colors_set(term, vt_param_get(term, 2, 0)); break; case 4: sixel_colors_report_max(term); break; default: UNHANDLED(); break; } break; case 2: switch (operation) { case 1: sixel_geometry_report_current(term); break; case 2: sixel_geometry_reset(term); break; case 3: sixel_geometry_set(term, vt_param_get(term, 2, 0), vt_param_get(term, 3, 0)); break; case 4: sixel_geometry_report_max(term); break; default: UNHANDLED(); break; } break; default: UNHANDLED(); break; } break; } case 'm': { int resource = vt_param_get(term, 0, 0); int value = -1; switch (resource) { case 0: /* modifyKeyboard */ value = 0; break; case 1: /* modifyCursorKeys */ case 2: /* modifyFunctionKeys */ value = 1; break; case 4: /* modifyOtherKeys */ value = term->modify_other_keys_2 ? 2 : 1; break; default: LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", resource, csi_as_string(term, final, -1)); break; } if (value >= 0) { char reply[16] = {0}; int chars = snprintf(reply, sizeof(reply), "\033[>%d;%dm", resource, value); term_to_slave(term, reply, chars); } break; } case 'n': { const int param = vt_param_get(term, 0, 0); switch (param) { case 996: { /* Query current theme mode (see private mode 2031) */ /* * 1 - dark mode * 2 - light mode * * In foot, the themes aren't necessarily light/dark, * but by convention, the primary theme is dark, and * the alternative theme is light. */ char reply[16] = {0}; int chars = snprintf( reply, sizeof(reply), "\033[?997;%dn", term->colors.active_theme == COLOR_THEME1 ? 1 : 2); term_to_slave(term, reply, chars); break; } } break; } case 'p': { /* * Request status of ECMA-48/"ANSI" private mode (DECRQM * for SM/RM modes; see private="?$" case further below for * DECSET/DECRST modes) */ unsigned param = vt_param_get(term, 0, 0); unsigned status = DECRPM_NOT_RECOGNIZED; if (param == 4) { status = decrpm(term->insert_mode); } char reply[32]; size_t n = xsnprintf(reply, sizeof(reply), "\033[%u;%u$y", param, status); term_to_slave(term, reply, n); break; } case 'u': { enum kitty_kbd_flags flags = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; char reply[8]; int chars = snprintf(reply, sizeof(reply), "\033[?%uu", flags); term_to_slave(term, reply, chars); break; } default: UNHANDLED(); break; } break; /* private[0] == '?' */ } case '>': { switch (final) { case 'c': /* Send Device Attributes (Secondary DA) */ if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } /* * Param 1 - terminal type: * 0 - vt100 * 1 - vt220 * 2 - vt240 * 18 - vt330 * 19 - vt340 * 24 - vt320 * 41 - vt420 * 61 - vt510 * 64 - vt520 * 65 - vt525 * * Param 2 - firmware version * xterm uses its version number. We use an xterm * version number too, since e.g. Emacs uses this to * determine level of support. * * We report ourselves as a VT220. This must be * synchronized with the primary DA response. * * Note: tertiary DA replies with "FOOT". */ static_assert(FOOT_MAJOR < 100, "Major version must not exceed 99"); static_assert(FOOT_MINOR < 100, "Minor version must not exceed 99"); static_assert(FOOT_PATCH < 100, "Patch version must not exceed 99"); char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[>1;%02u%02u%02u;0c", FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH); term_to_slave(term, reply, n); break; case 'm': if (term->vt.params.idx == 0) { /* Reset all */ } else { int resource = vt_param_get(term, 0, 0); int value = vt_param_get(term, 1, -1); switch (resource) { case 0: /* modifyKeyboard */ break; case 1: /* modifyCursorKeys */ case 2: /* modifyFunctionKeys */ /* Ignored, we always report modifiers */ if (value != 2 && value != -1) { LOG_WARN( "unimplemented: %s = %d", resource == 1 ? "modifyCursorKeys" : resource == 2 ? "modifyFunctionKeys" : "", value); } break; case 4: /* modifyOtherKeys */ term->modify_other_keys_2 = value == 2; LOG_DBG("modifyOtherKeys=%d", value); break; default: LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", resource, csi_as_string(term, final, -1)); break; } } break; /* final == 'm' */ case 'n': { int resource = vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ switch (resource) { case 0: /* modifyKeyboard */ case 1: /* modifyCursorKeys */ case 2: /* modifyFunctionKeys */ break; case 4: /* modifyOtherKeys */ /* We don't support fully disabling modifyOtherKeys, * but simply revert back to mode '1' */ term->modify_other_keys_2 = false; LOG_DBG("modifyOtherKeys=1"); break; } break; } case 'u': { int flags = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; struct grid *grid = term->grid; uint8_t idx = grid->kitty_kbd.idx; if (idx + 1 >= ALEN(grid->kitty_kbd.flags)) { /* Stack full, evict oldest by wrapping around */ idx = 0; } else idx++; grid->kitty_kbd.flags[idx] = flags; grid->kitty_kbd.idx = idx; LOG_DBG("kitty kbd: pushed new flags: 0x%03x", flags); break; } case 'q': { /* XTVERSION */ if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033P>|foot(%u.%u.%u%s%s)\033\\", FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH, FOOT_EXTRA[0] != '\0' ? "-" : "", FOOT_EXTRA); term_to_slave(term, reply, n); break; } default: UNHANDLED(); break; } break; /* private[0] == '>' */ } case '<': { switch (final) { case 'u': { int count = vt_param_get(term, 0, 1); LOG_DBG("kitty kbd: popping %d levels of flags", count); struct grid *grid = term->grid; uint8_t idx = grid->kitty_kbd.idx; for (int i = 0; i < count; i++) { /* Reset flags. This ensures we get flags=0 when * over-popping */ grid->kitty_kbd.flags[idx] = 0; if (idx == 0) idx = ALEN(grid->kitty_kbd.flags) - 1; else idx--; } grid->kitty_kbd.idx = idx; LOG_DBG("kitty kbd: flags after pop: 0x%03x", term->grid->kitty_kbd.flags[idx]); break; } } break; /* private[0] == '<' */ } case ' ': { switch (final) { case 'q': { int param = vt_param_get(term, 0, 0); switch (param) { case 0: /* blinking block, but we use it to reset to configured default */ term->cursor_style = term->conf->cursor.style; term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); break; case 1: /* blinking block */ case 2: /* steady block */ term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW ? CURSOR_HOLLOW : CURSOR_BLOCK; break; case 3: /* blinking underline */ case 4: /* steady underline */ term->cursor_style = CURSOR_UNDERLINE; break; case 5: /* blinking bar */ case 6: /* steady bar */ term->cursor_style = CURSOR_BEAM; break; default: UNHANDLED(); break; } if (param > 0 && param <= 6) { term->cursor_blink.deccsusr = param & 1; term_cursor_blink_update(term); } break; } default: UNHANDLED(); break; } break; /* private[0] == ' ' */ } case '!': { if (final == 'p') { term_reset(term, false); break; } UNHANDLED(); break; /* private[0] == '!' */ } case '=': { switch (final) { case 'c': if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } /* * Send Device Attributes (Tertiary DA) * * Reply format is "DCS ! | DDDDDDDD ST" * * D..D is the unit ID of the terminal, consisting of four * hexadecimal pairs. The first pair represents the * manufacturing site code. This code can be any * hexadecimal value from 00 through FF. */ term_to_slave(term, "\033P!|464f4f54\033\\", 14); /* FOOT */ break; case 'u': { int flag_set = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; int mode = vt_param_get(term, 1, 1); struct grid *grid = term->grid; uint8_t idx = grid->kitty_kbd.idx; switch (mode) { case 1: /* set bits are set, unset bits are reset */ grid->kitty_kbd.flags[idx] = flag_set; break; case 2: /* set bits are set, unset bits are left unchanged */ grid->kitty_kbd.flags[idx] |= flag_set; break; case 3: /* set bits are reset, unset bits are left unchanged */ grid->kitty_kbd.flags[idx] &= ~flag_set; break; default: UNHANDLED(); break; } LOG_DBG("kitty kbd: flags after update: 0x%03x", grid->kitty_kbd.flags[idx]); break; } default: UNHANDLED(); break; } break; /* private[0] == '=' */ } case '$': { switch (final) { case 'r': { /* DECCARA */ int top, left, bottom, right; if (!params_to_rectangular_area( term, 0, &top, &left, &bottom, &right)) { break; } for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); row->dirty = true; for (int c = left; c <= right; c++) { struct attributes *a = &row->cells[c].attrs; a->clean = 0; for (size_t i = 4; i < term->vt.params.idx; i++) { const int param = term->vt.params.v[i].value; /* DECCARA only supports a sub-set of SGR parameters */ switch (param) { case 0: a->bold = false; a->underline = false; a->blink = false; a->reverse = false; break; case 1: a->bold = true; break; case 4: a->underline = true; break; case 5: a->blink = true; break; case 7: a->reverse = true; break; case 22: a->bold = false; break; case 24: a->underline = false; break; case 25: a->blink = false; break; case 27: a->reverse = false; break; } } } } break; } case 't': { /* DECRARA */ int top, left, bottom, right; if (!params_to_rectangular_area( term, 0, &top, &left, &bottom, &right)) { break; } for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); row->dirty = true; for (int c = left; c <= right; c++) { struct attributes *a = &row->cells[c].attrs; a->clean = 0; for (size_t i = 4; i < term->vt.params.idx; i++) { const int param = term->vt.params.v[i].value; /* DECRARA only supports a sub-set of SGR parameters */ switch (param) { case 0: a->bold = !a->bold; a->underline = !a->underline; a->blink = !a->blink; a->reverse = !a->reverse; break; case 1: a->bold = !a->bold; break; case 4: a->underline = !a->underline; break; case 5: a->blink = !a->blink; break; case 7: a->reverse = !a->reverse; break; } } } } break; } case 'v': { /* DECCRA */ int src_top, src_left, src_bottom, src_right; if (!params_to_rectangular_area( term, 0, &src_top, &src_left, &src_bottom, &src_right)) { break; } int src_page = vt_param_get(term, 4, 1); int dst_rel_top = vt_param_get(term, 5, 1) - 1; int dst_left = vt_param_get(term, 6, 1) - 1; int dst_page = vt_param_get(term, 7, 1); if (unlikely(src_page != 1 || dst_page != 1)) { /* We don’t support “pages” */ break; } int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); int dst_top = term_row_rel_to_abs(term, dst_rel_top); int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); /* Target area outside the screen is clipped */ const size_t row_count = min(src_bottom - src_top, dst_bottom - dst_top) + 1; const size_t cell_count = min(src_right - src_left, dst_right - dst_left) + 1; sixel_overwrite_by_rectangle( term, dst_top, dst_left, row_count, cell_count); /* * Copy source area * * Note: since source and destination may overlap, we need * to copy out the entire source region first, and _then_ * write the destination. I.e. this is similar to how * memmove() behaves, but adapted to our row/cell * structure. */ struct cell **copy = xmalloc(row_count * sizeof(copy[0])); for (int r = 0; r < row_count; r++) { copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); const struct row *row = grid_row(term->grid, src_top + r); const struct cell *cell = &row->cells[src_left]; memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); } /* Paste into destination area */ for (int r = 0; r < row_count; r++) { struct row *row = grid_row(term->grid, dst_top + r); row->dirty = true; struct cell *cell = &row->cells[dst_left]; memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); free(copy[r]); for (;cell < &row->cells[dst_left + cell_count]; cell++) cell->attrs.clean = 0; if (unlikely(row->extra != NULL)) { /* TODO: technically, we should copy the source URIs... */ grid_row_uri_range_erase(row, dst_left, dst_right); } } free(copy); break; } case 'x': { /* DECFRA */ const uint8_t c = vt_param_get(term, 0, 0); if (unlikely(!((c >= 32 && c < 126) || c >= 160))) break; int top, left, bottom, right; if (!params_to_rectangular_area( term, 1, &top, &left, &bottom, &right)) { break; } /* Erase the entire region at once (MUCH cheaper than * doing it row by row, or even character by * character). */ sixel_overwrite_by_rectangle( term, top, left, bottom - top + 1, right - left + 1); for (int r = top; r <= bottom; r++) term_fill(term, r, left, c, right - left + 1, true); break; } case 'z': { /* DECERA */ int top, left, bottom, right; if (!params_to_rectangular_area( term, 0, &top, &left, &bottom, &right)) { break; } /* * Note: term_erase() _also_ erases sixels, but since * we’re forced to erase one row at a time, erasing the * entire sixel here is more efficient. */ sixel_overwrite_by_rectangle( term, top, left, bottom - top + 1, right - left + 1); for (int r = top; r <= bottom; r++) term_erase(term, r, left, r, right); break; } } break; /* private[0] == ‘$’ */ } case '#': { switch (final) { case 'P': { /* XTPUSHCOLORS */ int slot = vt_param_get(term, 0, 0); /* Pm == 0, "push" (what xterm does is take take the *current* slot + 1, even if that's in the middle of the stack, and overwrites whatever is already in that slot) */ if (slot == 0) slot = term->color_stack.idx + 1; if (term->color_stack.size < slot) { const size_t new_size = slot; term->color_stack.stack = xrealloc( term->color_stack.stack, new_size * sizeof(term->color_stack.stack[0])); /* Initialize new slots (except the selected slot, which is done below) */ xassert(new_size > 0); for (size_t i = term->color_stack.size; i < new_size - 1; i++) { memcpy(&term->color_stack.stack[i], &term->colors, sizeof(term->colors)); } term->color_stack.size = new_size; } xassert(slot > 0); xassert(slot <= term->color_stack.size); term->color_stack.idx = slot; memcpy(&term->color_stack.stack[slot - 1], &term->colors, sizeof(term->colors)); break; } case 'Q': { /* XTPOPCOLORS */ int slot = vt_param_get(term, 0, 0); /* Pm == 0, "pop" (what xterm does is copy colors from the *current* slot, *and* decrease the current slot index, even if that's in the middle of the stack) */ if (slot == 0) slot = term->color_stack.idx; if (slot > 0 && slot <= term->color_stack.size) { memcpy(&term->colors, &term->color_stack.stack[slot - 1], sizeof(term->colors)); term->color_stack.idx = slot - 1; /* Assume a full palette switch *will* affect almost all cells. The alternative is to call term_damage_color() for all 256 palette entries *and* the default fg/bg (256 + 2 calls in total) */ term_damage_view(term); term_damage_margins(term); } else if (slot == 0) { LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); } else { LOG_ERR( "XTPOPCOLORS: invalid color slot: %d " "(stack has %zu slots, current slot is %zu)", vt_param_get(term, 0, 0), term->color_stack.size, term->color_stack.idx); } break; } case 'R': { /* XTREPORTCOLORS */ char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", term->color_stack.idx, term->color_stack.size); term_to_slave(term, reply, n); break; } } break; /* private[0] == '#' */ } case 0x243f: /* ?$ */ switch (final) { case 'p': { unsigned param = vt_param_get(term, 0, 0); /* * Request DEC private mode (DECRQM) * Reply: * 0 - not recognized * 1 - set * 2 - reset * 3 - permanently set * 4 - permantently reset */ unsigned status = decrqm(term, param); char reply[32]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); term_to_slave(term, reply, n); break; } default: UNHANDLED(); break; } break; /* private[0] == '?' && private[1] == '$' */ case 0x203e: /* '> ' */ switch (final) { case 'q': { if (term->vt.params.idx == 0) { term_to_slave(term, "\033[>1;2;3;29;30;40;100;101 q", 27); break; } const unsigned shape = vt_param_get(term, 0, 0); switch (shape) { case MULTI_CURSOR_SHAPE_NONE: case MULTI_CURSOR_SHAPE_BLOCK: case MULTI_CURSOR_SHAPE_BEAM: case MULTI_CURSOR_SHAPE_UNDERLINE: case MULTI_CURSOR_SHAPE_PRIMARY: LOG_DBG("multi-cursor: %s", shape == MULTI_CURSOR_SHAPE_NONE ? "no cursor" : shape == MULTI_CURSOR_SHAPE_BLOCK ? "block cursor" : shape == MULTI_CURSOR_SHAPE_BEAM ? "beam cursor" : shape == MULTI_CURSOR_SHAPE_UNDERLINE ? "underline cursor" : "main cursor"); pixman_region32_t modified; pixman_region32_init(&modified); for (size_t i = 1; i < term->vt.params.idx; i++) { const unsigned coord_type = vt_param_get(term, i, 0); const struct vt_param *param = &term->vt.params.v[i]; switch (coord_type) { case 0: LOG_DBG("multi-cursor: unimplemented: " "coordinate relative to main cursor"); break; case 2:{ const size_t pair_count = param->sub.idx / 2; for (size_t j = 0; j < pair_count; j++) { const unsigned row = min(param->sub.value[j * 2 + 0], term->rows) - 1; const unsigned col = min(param->sub.value[j * 2 + 1], term->cols) - 1; LOG_DBG("pair: row=%u, col=%u", row, col); pixman_region32_union_rect(&modified, &modified, col, row, 1, 1); } break; } case 4: { if (param->sub.idx == 0) { /* Special case, rectangle is the entire screen */ LOG_DBG("rectangle: all screen"); pixman_region32_union_rect(&modified, &modified, 0, 0, term->cols, term->rows); } else { const size_t rect_count = param->sub.idx / 4; for (size_t j = 0; j < rect_count; j++) { const unsigned top = min(param->sub.value[j * 4 + 0], term->rows) - 1; const unsigned left = min(param->sub.value[j * 4 + 1], term->cols) - 1; const unsigned bottom = max(min(param->sub.value[j * 4 + 2], term->rows) - 1, top); const unsigned right = max(min(param->sub.value[j * 4 + 3], term->cols) - 1, left); LOG_DBG( "rectangle top=%u, left=%u, bottom=%u, right=%u (width=%u, height=%u", top, left, bottom, right, right - left + 1, bottom - top + 1); pixman_region32_union_rect(&modified, &modified, left, top, right - left + 1, bottom - top + 1); } } break; } default: LOG_WARN("multi-cursor: invalid coordinate type: %u", coord_type); return; } } if (shape == MULTI_CURSOR_SHAPE_NONE) { /* Cursors disabled, remove from active set */ pixman_region32_subtract( &term->multi_cursor.active, &term->multi_cursor.active, &modified); } else { /* Add new/updated cursors to active set */ pixman_region32_union( &term->multi_cursor.active, &term->multi_cursor.active, &modified); } if (pixman_region32_empty(&term->multi_cursor.active)) { free(term->multi_cursor.shapes); term->multi_cursor.shapes = NULL; } else { if (term->multi_cursor.shapes == NULL) { term->multi_cursor.shapes = xcalloc( term->grid->num_cols * term->grid->num_rows, sizeof(enum multi_cursor_shape)); } } int rect_count = 0; const pixman_box32_t *boxes = pixman_region32_rectangles(&modified, &rect_count); /* Set shape, and dirty affected cells */ for (const pixman_box32_t *box = boxes; box < &boxes[rect_count]; box++) { for (int r = box->y1; r < box->y2; r++) { struct row *row = grid_row(term->grid, r); xassert(row != NULL); row->dirty = true; for (int c = box->x1; c < box->x2; c++) { if (term->multi_cursor.shapes != NULL) term->multi_cursor.shapes[r * term->cols + c] = shape; row->cells[c].attrs.clean = false; } } } pixman_region32_fini(&modified); break; case MULTI_CURSOR_SHAPE_TEXT_COLOR: case MULTI_CURSOR_SHAPE_CURSOR_COLOR: { const unsigned color_space = vt_param_get(term, 1, 0); enum multi_cursor_color_source color_source = MULTI_CURSOR_COLOR_PRIMARY; uint32_t color = shape == MULTI_CURSOR_SHAPE_TEXT_COLOR ? term->multi_cursor.text_color : term->multi_cursor.cursor_color; switch (color_space) { case MULTI_CURSOR_COLOR_PRIMARY: color_source = MULTI_CURSOR_COLOR_PRIMARY; break; case MULTI_CURSOR_COLOR_SPECIAL: color_source = MULTI_CURSOR_COLOR_SPECIAL; break; case MULTI_CURSOR_COLOR_RGB: { const struct vt_param *param = &term->vt.params.v[1]; if (param->sub.idx == 3) { const uint8_t red = param->sub.value[0]; const uint8_t green = param->sub.value[1]; const uint8_t blue = param->sub.value[2]; color_source = MULTI_CURSOR_COLOR_RGB; color = 0xffu << 24 | red << 16 | green << 8 | blue; } break; } case MULTI_CURSOR_COLOR_256: { const struct vt_param *param = &term->vt.params.v[1]; if (param->sub.idx == 1) { const unsigned color_index = min( param->sub.value[0], ALEN(term->colors.table) - 1); color_source = MULTI_CURSOR_COLOR_256; color = color_index; } break; } default: LOG_WARN("multi-cursor: invalid color space: %u", color_space); return; } if (shape == MULTI_CURSOR_SHAPE_TEXT_COLOR) { term->multi_cursor.text_color_source = color_source; term->multi_cursor.text_color = color; } else { term->multi_cursor.cursor_color_source = color_source; term->multi_cursor.cursor_color = color; } break; } case MULTI_CURSOR_SHAPE_QUERY_CURSORS: { term_to_slave(term, "\033[>100", 6); int rect_count = 0; const pixman_box32_t *boxes = pixman_region32_rectangles( &term->multi_cursor.active, &rect_count); xassert(rect_count > 0 || term->multi_cursor.shapes == NULL); for (int j = 0; j < rect_count; j++) { const pixman_box32_t *box = &boxes[j]; for (int r = box->y1; r < box->y2; r++) { for (int c = box->x1; c < box->x2; c++) { const enum multi_cursor_shape shape = term->multi_cursor.shapes[r * term->cols + c]; xassert(shape != MULTI_CURSOR_SHAPE_NONE); char reply[64]; int len = snprintf( reply, sizeof(reply), ";%u:2:%d:%d", shape, r + 1, c + 1); term_to_slave(term, reply, len); } } } term_to_slave(term, " q", 2); break; } case MULTI_CURSOR_SHAPE_QUERY_COLORS: term_to_slave(term, "\033[>101", 7); char reply[64]; int len = 0; switch (term->multi_cursor.text_color_source) { case MULTI_CURSOR_COLOR_PRIMARY: len = snprintf(reply, sizeof(reply), ";30:%u", MULTI_CURSOR_COLOR_PRIMARY); break; case MULTI_CURSOR_COLOR_SPECIAL: len = snprintf(reply, sizeof(reply), ";30:%u", MULTI_CURSOR_COLOR_SPECIAL); break; case MULTI_CURSOR_COLOR_RGB: { const uint32_t color = term->multi_cursor.text_color; const uint8_t red = (color >> 16) & 0xff; const uint8_t green = (color >> 8) & 0xff; const uint8_t blue = (color >> 0) & 0xff; len = snprintf(reply, sizeof(reply), ";30:%u:%hhu:%hhu:%hhu", MULTI_CURSOR_COLOR_RGB, red, green, blue); break; } case MULTI_CURSOR_COLOR_256: len = snprintf(reply, sizeof(reply), ";30:%u:%u", MULTI_CURSOR_COLOR_256, term->multi_cursor.text_color); break; } term_to_slave(term, reply, len); len = 0; switch (term->multi_cursor.cursor_color_source) { case MULTI_CURSOR_COLOR_PRIMARY: len = snprintf(reply, sizeof(reply), ";40:%u", MULTI_CURSOR_COLOR_PRIMARY); break; case MULTI_CURSOR_COLOR_SPECIAL: len = snprintf(reply, sizeof(reply), ";40:%u", MULTI_CURSOR_COLOR_SPECIAL); break; case MULTI_CURSOR_COLOR_RGB: { const uint32_t color = term->multi_cursor.cursor_color; const uint8_t red = (color >> 16) & 0xff; const uint8_t green = (color >> 8) & 0xff; const uint8_t blue = (color >> 0) & 0xff; len = snprintf(reply, sizeof(reply), ";40:%u:%hhu:%hhu:%hhu", MULTI_CURSOR_COLOR_RGB, red, green, blue); break; } case MULTI_CURSOR_COLOR_256: len = snprintf(reply, sizeof(reply), ";40:%u:%u", MULTI_CURSOR_COLOR_256, term->multi_cursor.cursor_color); break; } term_to_slave(term, reply, len); term_to_slave(term, " q", 2); break; default: LOG_WARN("multi-cursor: invalid shape/color command: %u", shape); return; } break; } default: UNHANDLED(); break; } break; /* private[0] == '>' && private[1] == ' ' */ default: UNHANDLED(); break; } }