#include "csi.h"

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#if defined(_DEBUG)
 #include <stdio.h>
#endif

#include <sys/timerfd.h>

#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)
{
    memset(&term->vt.attrs, 0, sizeof(term->vt.attrs));
}

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; 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: break; /* double-underline, not implemented */
        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; 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: {
            uint32_t color;
            enum color_source src;

            /* Indexed: 38;5;<idx> */
            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;<r>;<g>;<b> */
            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:<idx> */
            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:<color-space>: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:<color-space>:c:m:y[:tolerance:tolerance-color-space] */
            /* CMYK:        38:4:<color-space>:c:m:y:k[:tolerance:tolerance-color-space] */

            /* Unrecognized */
            else {
                UNHANDLED_SGR(i);
                break;
            }

            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;

        /* 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 3:
        /* DECCOLM */
        if (enable)
            LOG_WARN("unimplemented: 132 column mode (DECCOLM)");

        term_erase(term, 0, 0, term->rows - 1, term->cols - 1);
        term_cursor_home(term);
        break;

    case 4:
        /* DECSCLM - Smooth scroll */
        if (enable)
            LOG_WARN("unimplemented: Smooth (Slow) Scroll (DECSCLM)");
        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 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_all(term);
        }
        term_update_ascii_printer(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 8452:
        term->sixel.cursor_right_of_graphics = enable;
        break;

    case 737769:
        if (enable)
            term_ime_enable(term);
        else
            term_ime_disable(term);
        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);
}

static bool
decrqm(const struct terminal *term, unsigned param, bool *enabled)
{
    switch (param) {
    case 1: *enabled = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; return true;
    case 3: *enabled = false; return true;
    case 4: *enabled = false; return true;
    case 5: *enabled = term->reverse; return true;
    case 6: *enabled = term->origin; return true;
    case 7: *enabled = term->auto_margin; return true;
    case 9: *enabled = false; /*  term->mouse_tracking == MOUSE_X10; */ return true;
    case 12: *enabled = term->cursor_blink.decset; return true;
    case 25: *enabled = !term->hide_cursor; return true;
    case 45: *enabled = term->reverse_wrap; return true;
    case 66: *enabled = term->keypad_keys_mode == KEYPAD_APPLICATION; return true;
    case 80: *enabled = !term->sixel.scrolling; return true;
    case 1000: *enabled = term->mouse_tracking == MOUSE_CLICK; return true;
    case 1001: *enabled = false; return true;
    case 1002: *enabled = term->mouse_tracking == MOUSE_DRAG; return true;
    case 1003: *enabled = term->mouse_tracking == MOUSE_MOTION; return true;
    case 1004: *enabled = term->focus_events; return true;
    case 1005: *enabled = false; /* term->mouse_reporting == MOUSE_UTF8; */ return true;
    case 1006: *enabled = term->mouse_reporting == MOUSE_SGR; return true;
    case 1007: *enabled = term->alt_scrolling; return true;
    case 1015: *enabled = term->mouse_reporting == MOUSE_URXVT; return true;
    case 1016: *enabled = term->mouse_reporting == MOUSE_SGR_PIXELS; return true;
    case 1034: *enabled = term->meta.eight_bit; return true;
    case 1035: *enabled = term->num_lock_modifier; return true;
    case 1036: *enabled = term->meta.esc_prefix; return true;
    case 1042: *enabled = term->bell_action_enabled; return true;
    case 47:   /* FALLTHROUGH */
    case 1047: /* FALLTHROUGH */
    case 1049: *enabled = term->grid == &term->alt; return true;
    case 1070: *enabled = term->sixel.use_private_palette; return true;
    case 2004: *enabled = term->bracketed_paste; return true;
    case 2026: *enabled = term->render.app_sync_updates.enabled; return true;
    case 8452: *enabled = term->sixel.cursor_right_of_graphics; return true;
    case 737769: *enabled = term_ime_is_enabled(term); return true;
    }

    return false;
}

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 3: break;
    case 4: 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 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 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 3: return;
    case 4: return;
    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 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 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break;
    case 737769: enable = term->xtsave.ime; break;

    default: return;
    }

    decset_decrst(term, param, enable);
}

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);

                const int width = c32width(term->vt.last_printed);
                if (width > 0) {
                    for (int i = 0; i < count; i++)
                        term_print(term, term->vt.last_printed, width);
                }
            }
            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;<Ps>c  vt220
             *  - CSI?63;<Ps>c  vt320
             *  - CSI?64;<Ps>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).
             *
             * 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".
             */
            if (term->conf->tweak.sixel) {
                static const char reply[] = "\033[?62;4;22c";
                term_to_slave(term, reply, sizeof(reply) - 1);
            } else {
                static const char reply[] = "\033[?62;22c";
                term_to_slave(term, reply, sizeof(reply) - 1);
            }
            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_to(term, term->grid->cursor.point.row, 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;
                break;

            case 3: {
                /* Erase scrollback */
                term_erase_scrollback(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': {
            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': {
            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':
            /* Set mode */
            switch (vt_param_get(term, 0, 0)) {
            case 2:   /* Keyboard Action Mode - AM */
                LOG_WARN("unimplemented: keyboard action mode (AM)");
                break;

            case 4:   /* Insert Mode - IRM */
                term->insert_mode = true;
                term_update_ascii_printer(term);
                break;

            case 12:  /* Send/receive Mode - SRM */
                LOG_WARN("unimplemented: send/receive mode (SRM)");
                break;

            case 20:  /* Automatic Newline Mode - LNM */
                /* TODO: would be easy to implemented; when active
                 * term_linefeed() would _also_ do a
                 * term_carriage_return() */
                LOG_WARN("unimplemented: automatic newline mode (LNM)");
                break;
            }
            break;

        case 'l':
            /* Reset mode */
            switch (vt_param_get(term, 0, 0)) {
            case 4:   /* Insert Mode - IRM */
                term->insert_mode = false;
                term_update_ascii_printer(term);
                break;

            case 2:   /* Keyboard Action Mode - AM */
            case 12:  /* Send/receive Mode - SRM */
            case 20:  /* Automatic Newline Mode - LNM */
                break;
            }
            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 21: LOG_WARN("unimplemented: report window title"); 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 / term->scale, width / term->scale);
                    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_scaled.height,
                             it->item->dim.px_scaled.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->scale,
                         term->cell_width / term->scale);
                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 / term->scale,
                             it->item->dim.px_real.width / term->cell_width / term->scale);
                    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 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 '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" : "<invalid>",
                            value);
                    }
                    break;

                case 4: /* modifyOtherKeys */
                    term->modify_other_keys_2 = value == 2;
                    LOG_DBG("modifyOtherKeys=%d", value);
                    break;

                default:
                    LOG_WARN("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 modifyFuncionKeys */
            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;
                term_cursor_blink_update(term);
                break;

            case 1:         /* blinking block */
            case 2:         /* steady block */
                term->cursor_style = 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 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
             */
            bool enabled;
            unsigned value;
            if (decrqm(term, param, &enabled))
                value = enabled ? 1 : 2;
            else
                value = 0;

            char reply[32];
            size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, value);
            term_to_slave(term, reply, n);
            break;

        }

        default:
            UNHANDLED();
            break;
        }

        break; /* private[0] == ‘?’ && private[1] == ‘$’ */

    default:
        UNHANDLED();
        break;
    }
}
