TUI: kitty keyboard protocol, word movement and deletion keys
ci / linux (clang) (push) Failing after 23s
ci / linux (gcc) (push) Failing after 22s
ci / sanitizers (push) Failing after 21s
ci / musl (push) Failing after 1m12s
ci / macos (push) Canceled after 0s

Query CSI ? u at startup and push the disambiguate flag on the alternate
screen when the terminal answers; CSI u reports are mapped back to legacy
input (Shift/Ctrl+Enter insert a newline, Ctrl+Backspace deletes a word).
Ctrl/Alt+Left/Right move by word, Ctrl/Alt+Delete delete the next word.
Our define_key codes move above the range ncurses uses for terminfo
extended keys.
This commit is contained in:
WHC
2026-10-06 01:52:48 +08:00
parent 8efd63bb31
commit b28bca2cfc
6 changed files with 251 additions and 14 deletions
+131 -12
View File
@@ -127,9 +127,90 @@ static int cst_clock_gettime_mono(struct timespec *ts) {
#define COMP_MAX 8
#define TODO_PANEL_MAX 6
#define ASK_OPTS_MAX 9
/* Bracketed paste markers, bound with define_key() above KEY_MAX. */
#define KEY_PASTE_BEGIN (KEY_MAX + 1)
#define KEY_PASTE_END (KEY_MAX + 2)
/* Our define_key() codes. ncurses numbers the terminfo extended keys (kLFT5,
* kDC3, ...) from KEY_MAX up, and key codes must fit in 16 bits, so stay well
* clear of the first and below the second. */
#define KEY_OWN_BASE (KEY_MAX + 0x1000)
/* Bracketed paste markers. */
#define KEY_PASTE_BEGIN (KEY_OWN_BASE + 1)
#define KEY_PASTE_END (KEY_OWN_BASE + 2)
/* Kitty keyboard protocol: the reply to CSI ? u, and CSI cp;1+mods u reports
* bound as KEY_KITTY_BASE + cp * 8 + mods for ASCII code points. */
#define KEY_KITTY_REPLY (KEY_OWN_BASE + 3)
#define KEY_KITTY_BASE (KEY_OWN_BASE + 16)
#define KITTY_CP_END 128
#ifdef NCURSES_VERSION
static int g_kitty_on;
/* Bind the reports and ask whether the terminal speaks the protocol; the
* flags are pushed once the reply arrives (see tui_wget_wch). */
static void kitty_init(void) {
char seq[24];
for (int f = 0; f < 32; f++) {
snprintf(seq, sizeof(seq), "\033[?%du", f);
define_key(seq, KEY_KITTY_REPLY);
}
for (int cp = 9; cp < KITTY_CP_END; cp++) {
if (cp != 9 && cp != 13 && cp != 27 && cp < ' ')
continue;
if (cp == 27)
define_key("\033[27u", KEY_KITTY_BASE + cp * 8);
for (int m = 1; m < 8; m++) {
snprintf(seq, sizeof(seq), "\033[%d;%du", cp, m + 1);
define_key(seq, KEY_KITTY_BASE + cp * 8 + m);
}
}
printf("\033[?u");
fflush(stdout);
}
/* Pop our flags before leaving; harmless if they were never pushed. */
static void kitty_restore(void) {
if (g_kitty_on) {
printf("\033[<u");
fflush(stdout);
g_kitty_on = 0;
}
}
#endif
/* wget_wch() that turns kitty key reports back into legacy input. */
static int tui_wget_wch(WINDOW *w, wint_t *wch) {
for (;;) {
int rc = wget_wch(w, wch);
#ifdef NCURSES_VERSION
if (rc != KEY_CODE_YES)
return rc;
if (*wch == KEY_KITTY_REPLY) {
if (!g_kitty_on) {
/* disambiguate: Esc, alt+x, ctrl+x, shift+enter... as CSI u */
printf("\033[>1u");
fflush(stdout);
g_kitty_on = 1;
}
continue;
}
int k = (int)*wch - KEY_KITTY_BASE;
if (k < 0 || k >= KITTY_CP_END * 8)
return rc;
int out[2];
int n = cst_key_from_kitty(k / 8, k % 8, out);
if (n == 0)
continue;
if (n == 2)
unget_wch((wchar_t)out[1]);
if (out[0] == CST_K_BTAB) {
*wch = KEY_BTAB;
return KEY_CODE_YES;
}
*wch = (wint_t)out[0];
return OK;
#else
return rc;
#endif
}
}
/* Color pair indices */
enum {
@@ -3411,7 +3492,7 @@ static int picker_run(tui_t *t, const char *title, char items[][128], int n,
wrefresh(dlg);
wint_t wch = 0;
int rc = wget_wch(dlg, &wch);
int rc = tui_wget_wch(dlg, &wch);
if (rc == ERR)
continue;
if (rc == KEY_CODE_YES) {
@@ -3542,7 +3623,7 @@ static int secret_input_run(tui_t *t, const char *title, const char *hint,
refresh();
wint_t wch = 0;
int r = get_wch(&wch);
int r = tui_wget_wch(stdscr, &wch);
if (r == ERR)
continue;
if (r == KEY_CODE_YES) {
@@ -5634,8 +5715,10 @@ static int run_keys_panel(tui_t *t, int busy, int *quit) {
"Edit cancel the turn / close popups esc",
"Edit clear input, twice to quit ctrl+c",
"Edit quit on an empty prompt ctrl+d",
"Edit move by char / word / line ← → alt+b alt+f ctrl+a ctrl+e",
"Edit delete word / to start / to end ctrl+w ctrl+u ctrl+k",
"Edit new line (kitty keyboard terminals) shift+enter ctrl+enter",
"Edit move by char / word / line ← → ctrl+← → alt+b alt+f ctrl+a ctrl+e",
"Edit delete word back / forward ctrl+w alt+backspace ctrl+backspace ctrl+del",
"Edit delete to start / to end ctrl+u ctrl+k",
"Edit prompt history ↑ ↓",
};
for (size_t i = 0; i < sizeof(FIXED) / sizeof(FIXED[0]); i++) {
@@ -5656,7 +5739,8 @@ static int run_keys_panel(tui_t *t, int busy, int *quit) {
#ifdef NCURSES_VERSION
static int g_k_alt_up, g_k_alt_down, g_k_ctrl_home, g_k_ctrl_end,
g_k_ctrl_up, g_k_ctrl_down;
g_k_ctrl_up, g_k_ctrl_down, g_k_ctrl_left, g_k_ctrl_right, g_k_alt_left,
g_k_alt_right, g_k_ctrl_dc, g_k_alt_dc;
static int ext_key(const char *cap) {
char *s = tigetstr(cap);
@@ -5675,9 +5759,30 @@ static void keys_init(void) {
g_k_ctrl_end = ext_key("kEND5");
g_k_ctrl_up = ext_key("kUP5");
g_k_ctrl_down = ext_key("kDN5");
g_k_ctrl_left = ext_key("kLFT5");
g_k_ctrl_right = ext_key("kRIT5");
g_k_alt_left = ext_key("kLFT3");
g_k_alt_right = ext_key("kRIT3");
g_k_ctrl_dc = ext_key("kDC5");
g_k_alt_dc = ext_key("kDC3");
#endif
}
/* ctrl/alt+left/right move by word, ctrl/alt+delete deletes the next word. */
static int word_key(int k, int *dir) {
#ifdef NCURSES_VERSION
if (k && (k == g_k_ctrl_left || k == g_k_alt_left))
return *dir = -1, 1;
if (k && (k == g_k_ctrl_right || k == g_k_alt_right))
return *dir = 1, 1;
if (k && (k == g_k_ctrl_dc || k == g_k_alt_dc))
return *dir = 0, 1;
#else
(void)k, (void)dir;
#endif
return 0;
}
/* ncurses event → cst_keymap code (0 for events that are not keys). */
static int tui_key_code(int rc, wint_t wch) {
if (rc != KEY_CODE_YES)
@@ -5747,7 +5852,7 @@ static void run_perm_modal(tui_t *t) {
draw_perm_modal(t);
draw_status(t);
wint_t wch = 0;
int rc = wget_wch(t->input, &wch);
int rc = tui_wget_wch(t->input, &wch);
if (rc == ERR)
continue;
if (rc == KEY_CODE_YES) {
@@ -5810,7 +5915,7 @@ static void run_ask_modal(tui_t *t) {
for (;;) {
draw_ask_modal(t);
wint_t wch = 0;
int rc = wget_wch(t->input, &wch);
int rc = tui_wget_wch(t->input, &wch);
if (rc == ERR)
continue;
if (rc == KEY_CODE_YES) {
@@ -5940,6 +6045,7 @@ int cst_tui_run(cst_agent_t *agent) {
define_key("\033[201~", KEY_PASTE_END);
printf("\033[?2004h");
fflush(stdout);
kitty_init();
#endif
tui_t t;
@@ -6083,7 +6189,7 @@ int cst_tui_run(cst_agent_t *agent) {
}
wint_t wch = 0;
int rc = wget_wch(t.input, &wch);
int rc = tui_wget_wch(t.input, &wch);
if (rc == ERR)
continue;
@@ -6097,7 +6203,7 @@ int cst_tui_run(cst_agent_t *agent) {
if (code == 27 && !t.leader_pending) {
/* Alt+key arrives as Esc followed by the key */
wtimeout(t.input, 30);
nrc = wget_wch(t.input, &nx);
nrc = tui_wget_wch(t.input, &nx);
wtimeout(t.input, 80);
if (nrc != ERR) {
int c2 = tui_key_code(nrc, nx);
@@ -6181,6 +6287,18 @@ int cst_tui_run(cst_agent_t *agent) {
}
continue;
}
int wdir;
if (word_key((int)wch, &wdir)) {
if (wdir < 0)
t.cur = word_left(&t, t.cur), draw_input(&t);
else if (wdir > 0)
t.cur = word_right(&t, t.cur), draw_input(&t);
else {
input_erase(&t, t.cur, word_right(&t, t.cur));
after_edit(&t);
}
continue;
}
switch (wch) {
case KEY_HOME:
if (t.in_len) {
@@ -6376,6 +6494,7 @@ out:
tui_free(&t);
#ifdef NCURSES_VERSION
kitty_restore();
printf("\033[?2004l");
fflush(stdout);
#endif