TUI: kitty keyboard protocol, word movement and deletion keys
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:
@@ -291,6 +291,70 @@ const char *cst_key_name(int code, char *buf, size_t n) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Shift on a US layout; the kitty report names the unshifted key. */
|
||||
static int us_shift(int c) {
|
||||
static const char PLAIN[] = "`1234567890-=[]\\;',./";
|
||||
static const char SHIFTED[] = "~!@#$%^&*()_+{}|:\"<>?";
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return c - 'a' + 'A';
|
||||
const char *p = c > 0 && c < 0x80 ? strchr(PLAIN, c) : NULL;
|
||||
return p ? SHIFTED[p - PLAIN] : c;
|
||||
}
|
||||
|
||||
/* The control character xterm sends for ctrl+c, or c when there is none. */
|
||||
static int ctrl_char(int c) {
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
|
||||
return c & 0x1f;
|
||||
switch (c) {
|
||||
case ' ': case '2': case '@': return 0;
|
||||
case '3': case '[': return 27;
|
||||
case '4': case '\\': return 28;
|
||||
case '5': case ']': return 29;
|
||||
case '6': case '^': return 30;
|
||||
case '7': case '-': case '_': case '/': return 31;
|
||||
case '8': case '?': return 127;
|
||||
default: return c;
|
||||
}
|
||||
}
|
||||
|
||||
int cst_key_from_kitty(int cp, int mods, int out[2]) {
|
||||
int shift = mods & 1, alt = mods & 2, ctrl = mods & 4;
|
||||
if (mods & ~7)
|
||||
return 0; /* super, hyper, meta, caps/num lock: not ours */
|
||||
int c;
|
||||
switch (cp) {
|
||||
case 13:
|
||||
if (!mods)
|
||||
return out[0] = 13, 1;
|
||||
out[0] = 27, out[1] = 13; /* newline */
|
||||
return 2;
|
||||
case 127:
|
||||
if (!alt && !ctrl)
|
||||
return out[0] = 127, 1;
|
||||
out[0] = 27, out[1] = 127; /* delete word */
|
||||
return 2;
|
||||
case 9:
|
||||
if (shift && !alt && !ctrl)
|
||||
return out[0] = CST_K_BTAB, 1;
|
||||
c = 9;
|
||||
break;
|
||||
case 27:
|
||||
c = 27;
|
||||
break;
|
||||
default:
|
||||
if (cp < ' ' || cp > 0x10FFFF)
|
||||
return 0;
|
||||
c = shift ? us_shift(cp) : cp;
|
||||
if (ctrl)
|
||||
c = ctrl_char(c);
|
||||
break;
|
||||
}
|
||||
if (!alt)
|
||||
return out[0] = c, 1;
|
||||
out[0] = 27, out[1] = c;
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* "ctrl+x n", "<leader> n" or "f2" → keys; returns the count (0 on error). */
|
||||
static int parse_seq(const cst_keymap_t *km, const char *s, int keys[2]) {
|
||||
int n = 0;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user