#include "cst_keymap.h" #include "cJSON.h" #include "cst_util.h" #include "os_compat.h" #include #include #include #include static const cst_key_action_info_t INFO[CST_KA_COUNT] = { [CST_KA_PALETTE] = {"palette", "General", "command palette", NULL}, [CST_KA_KEYS] = {"keys", "General", "keyboard shortcuts", "/keys"}, [CST_KA_MODE] = {"mode", "General", "switch Build / Plan mode", NULL}, [CST_KA_APPROVAL] = {"approval", "General", "cycle approval ask/auto/yolo", NULL}, [CST_KA_NEWLINE] = {"newline", "General", "insert a newline", NULL}, [CST_KA_REDRAW] = {"redraw", "General", "redraw the screen", NULL}, [CST_KA_QUIT] = {"quit", "General", "quit", "/quit"}, [CST_KA_TOGGLE_TOOL] = {"toggle_tool", "View", "expand/collapse the last tool output", NULL}, [CST_KA_TOGGLE_THOUGHT] = {"toggle_thought", "View", "expand/collapse the last thought", NULL}, [CST_KA_THINKING_ALL] = {"thinking", "View", "show/hide all thoughts by default", "/thinking"}, [CST_KA_DETAILS] = {"details", "View", "expand/collapse all tool output", "/details"}, [CST_KA_TODO] = {"todo", "View", "show/hide the task panel", NULL}, [CST_KA_SIDEBAR] = {"sidebar", "View", "show/hide the sidebar", "/sidebar"}, [CST_KA_SCROLL_UP] = {"scroll_up", "View", "scroll up half a page", NULL}, [CST_KA_SCROLL_DOWN] = {"scroll_down", "View", "scroll down half a page", NULL}, [CST_KA_LINE_UP] = {"line_up", "View", "scroll up one line", NULL}, [CST_KA_LINE_DOWN] = {"line_down", "View", "scroll down one line", NULL}, [CST_KA_TOP] = {"top", "View", "jump to the first message", NULL}, [CST_KA_BOTTOM] = {"bottom", "View", "jump to the latest message", NULL}, [CST_KA_NEW_SESSION] = {"new_session", "Session", "start a new session", "/new"}, [CST_KA_SESSIONS] = {"sessions", "Session", "pick a session", "/session"}, [CST_KA_UNDO] = {"undo", "Session", "revert the last turn's file changes", "/undo"}, [CST_KA_RETRY] = {"retry", "Session", "drop the last reply and ask again", "/retry"}, [CST_KA_COMPACT] = {"compact", "Session", "summarize the context", "/compact"}, [CST_KA_COPY] = {"copy", "Session", "copy the last reply", "/copy"}, [CST_KA_EXPORT] = {"export", "Session", "export the transcript", "/export"}, [CST_KA_PASTE_IMAGE] = {"paste_image", "Session", "attach the clipboard image", "/attach paste"}, [CST_KA_MODEL] = {"model", "Model", "pick the model", "/model"}, [CST_KA_PROVIDER] = {"provider", "Model", "switch provider", "/provider"}, [CST_KA_EFFORT] = {"effort", "Model", "reasoning effort", "/effort"}, [CST_KA_AGENTS] = {"agents", "Agents", "sub-agents and roles", "/agents"}, [CST_KA_SKILLS] = {"skills", "Agents", "skills", "/skills"}, [CST_KA_PLUGINS] = {"plugins", "Agents", "plugins and MCP servers", "/plugins"}, }; static const struct { int action; const char *keys; } DEFAULTS[] = { {CST_KA_PALETTE, "ctrl+p"}, {CST_KA_KEYS, " h"}, {CST_KA_KEYS, "f1"}, {CST_KA_MODE, "tab"}, {CST_KA_APPROVAL, "shift+tab"}, {CST_KA_NEWLINE, "ctrl+n"}, {CST_KA_NEWLINE, "alt+enter"}, {CST_KA_REDRAW, "ctrl+l"}, {CST_KA_QUIT, " q"}, {CST_KA_TOGGLE_TOOL, "ctrl+o"}, {CST_KA_TOGGLE_THOUGHT, "ctrl+r"}, {CST_KA_THINKING_ALL, " t"}, {CST_KA_DETAILS, " d"}, {CST_KA_TODO, "ctrl+t"}, {CST_KA_SIDEBAR, " s"}, {CST_KA_SCROLL_UP, "pgup"}, {CST_KA_SCROLL_DOWN, "pgdn"}, {CST_KA_LINE_UP, "alt+up"}, {CST_KA_LINE_DOWN, "alt+down"}, {CST_KA_TOP, "ctrl+home"}, {CST_KA_BOTTOM, "ctrl+end"}, {CST_KA_NEW_SESSION, " n"}, {CST_KA_SESSIONS, " l"}, {CST_KA_UNDO, " u"}, {CST_KA_RETRY, " r"}, {CST_KA_COMPACT, " c"}, {CST_KA_COPY, " y"}, {CST_KA_EXPORT, " x"}, {CST_KA_PASTE_IMAGE, "ctrl+v"}, {CST_KA_MODEL, " m"}, {CST_KA_PROVIDER, " p"}, {CST_KA_EFFORT, " e"}, {CST_KA_AGENTS, " a"}, {CST_KA_SKILLS, " k"}, {CST_KA_PLUGINS, " g"}, }; static const struct { const char *name; int code; } NAMED[] = { {"tab", 9}, {"enter", 13}, {"return", 13}, {"esc", 27}, {"escape", 27}, {"space", ' '}, {"backspace", 127}, {"up", CST_K_UP}, {"down", CST_K_DOWN}, {"left", CST_K_LEFT}, {"right", CST_K_RIGHT}, {"home", CST_K_HOME}, {"end", CST_K_END}, {"pgup", CST_K_PGUP}, {"pageup", CST_K_PGUP}, {"pgdn", CST_K_PGDN}, {"pagedown", CST_K_PGDN}, {"delete", CST_K_DEL}, {"del", CST_K_DEL}, {"insert", CST_K_INS}, }; const cst_key_action_info_t *cst_key_action_info(int action) { return action > CST_KA_NONE && action < CST_KA_COUNT ? &INFO[action] : NULL; } int cst_key_action_find(const char *name) { for (int i = 1; name && i < CST_KA_COUNT; i++) if (INFO[i].name && strcmp(INFO[i].name, name) == 0) return i; return CST_KA_NONE; } static int ieq(const char *a, const char *b, size_t n) { if (strlen(b) != n) return 0; for (size_t i = 0; i < n; i++) if (tolower((unsigned char)a[i]) != b[i]) return 0; return 1; } /* Decode one UTF-8 code point; returns bytes used (0 on error). */ static int utf8_one(const char *s, size_t n, int *cp) { const unsigned char *u = (const unsigned char *)s; if (n >= 1 && u[0] < 0x80) { *cp = u[0]; return 1; } if (n >= 2 && (u[0] & 0xE0) == 0xC0) { *cp = ((u[0] & 0x1F) << 6) | (u[1] & 0x3F); return 2; } if (n >= 3 && (u[0] & 0xF0) == 0xE0) { *cp = ((u[0] & 0x0F) << 12) | ((u[1] & 0x3F) << 6) | (u[2] & 0x3F); return 3; } if (n >= 4 && (u[0] & 0xF8) == 0xF0) { *cp = ((u[0] & 0x07) << 18) | ((u[1] & 0x3F) << 12) | ((u[2] & 0x3F) << 6) | (u[3] & 0x3F); return 4; } return 0; } static int parse_key_n(const char *s, size_t n) { int ctrl = 0, alt = 0, shift = 0; for (;;) { const char *plus = memchr(s, '+', n); if (!plus || plus == s + n - 1) break; size_t m = (size_t)(plus - s); if (ieq(s, "ctrl", m) || ieq(s, "c", m)) ctrl = 1; else if (ieq(s, "alt", m) || ieq(s, "meta", m) || ieq(s, "m", m)) alt = 1; else if (ieq(s, "shift", m) || ieq(s, "s", m)) shift = 1; else return -1; n -= m + 1; s = plus + 1; } if (n == 0) return -1; int code = -1; for (size_t i = 0; i < sizeof(NAMED) / sizeof(NAMED[0]); i++) if (ieq(s, NAMED[i].name, n)) code = NAMED[i].code; if (code < 0 && (s[0] == 'f' || s[0] == 'F') && n >= 2 && n <= 3) { int f = atoi(s + 1); if (f >= 1 && f <= 12 && isdigit((unsigned char)s[1])) code = CST_K_F1 + f - 1; } if (code < 0) { int cp = 0; if (utf8_one(s, n, &cp) != (int)n) return -1; code = cp; } if (shift) { if (code == 9) code = CST_K_BTAB; else if (code < 128 && isalpha(code)) code = toupper(code); else return -1; } if (ctrl) { if (code < 128 && isalpha(code)) code = tolower(code) - 'a' + 1; else if (code == CST_K_HOME) code = CST_K_CTRL_HOME; else if (code == CST_K_END) code = CST_K_CTRL_END; else if (code == CST_K_UP) code = CST_K_CTRL_UP; else if (code == CST_K_DOWN) code = CST_K_CTRL_DOWN; else return -1; } if (alt) { if (code == CST_K_UP) return CST_K_ALT_UP; if (code == CST_K_DOWN) return CST_K_ALT_DOWN; code |= CST_KEY_ALT; } return code; } int cst_key_parse(const char *s) { if (!s) return -1; while (*s == ' ') s++; size_t n = strlen(s); while (n && s[n - 1] == ' ') n--; return parse_key_n(s, n); } const char *cst_key_name(int code, char *buf, size_t n) { const char *alt = (code & CST_KEY_ALT) ? "alt+" : ""; int c = code & ~CST_KEY_ALT; static const char *const SPECIAL[] = { "up", "down", "left", "right", "home", "end", "pgup", "pgdn", "shift+tab", "delete", "insert", "ctrl+home", "ctrl+end", "ctrl+up", "ctrl+down", "alt+up", "alt+down"}; if (c == 9) snprintf(buf, n, "%stab", alt); else if (c == 13 || c == 10) snprintf(buf, n, "%senter", alt); else if (c == 27) snprintf(buf, n, "%sesc", alt); else if (c == ' ') snprintf(buf, n, "%sspace", alt); else if (c == 127 || c == 8) snprintf(buf, n, "%sbackspace", alt); else if (c >= 1 && c <= 26) snprintf(buf, n, "%sctrl+%c", alt, 'a' + c - 1); else if (c >= CST_K_F1 && c <= CST_K_LAST) snprintf(buf, n, "%sf%d", alt, c - CST_K_F1 + 1); else if (c >= CST_KEY_SPECIAL && c < CST_K_F1) snprintf(buf, n, "%s%s", alt, SPECIAL[c - CST_KEY_SPECIAL]); else if (c >= 'A' && c <= 'Z') snprintf(buf, n, "%sshift+%c", alt, c - 'A' + 'a'); else if (c < 0x80) snprintf(buf, n, "%s%c", alt, c); else { char u[5] = {0}; if (c < 0x800) { u[0] = (char)(0xC0 | (c >> 6)); u[1] = (char)(0x80 | (c & 0x3F)); } else if (c < 0x10000) { u[0] = (char)(0xE0 | (c >> 12)); u[1] = (char)(0x80 | ((c >> 6) & 0x3F)); u[2] = (char)(0x80 | (c & 0x3F)); } else { u[0] = (char)(0xF0 | (c >> 18)); u[1] = (char)(0x80 | ((c >> 12) & 0x3F)); u[2] = (char)(0x80 | ((c >> 6) & 0x3F)); u[3] = (char)(0x80 | (c & 0x3F)); } snprintf(buf, n, "%s%s", alt, u); } 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", " 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; while (*s) { while (*s == ' ') s++; if (!*s) break; size_t w = strcspn(s, " "); if (n == 2) return 0; int k = ieq(s, "", w) ? km->leader : parse_key_n(s, w); if (k < 0) return 0; keys[n++] = k; s += w; } return n; } static void add(cst_keymap_t *km, int action, const int keys[2], int nkeys, int user) { if (km->n >= CST_KEYMAP_MAX || nkeys < 1) return; cst_binding_t *b = &km->b[km->n++]; b->action = action; b->keys[0] = keys[0]; b->keys[1] = nkeys > 1 ? keys[1] : 0; b->nkeys = nkeys; b->user = user; } static void build_defaults(cst_keymap_t *km) { km->n = 0; for (size_t i = 0; i < sizeof(DEFAULTS) / sizeof(DEFAULTS[0]); i++) { int keys[2]; int n = parse_seq(km, DEFAULTS[i].keys, keys); add(km, DEFAULTS[i].action, keys, n, 0); } } void cst_keymap_defaults(cst_keymap_t *km) { memset(km, 0, sizeof(*km)); km->leader = 24; /* ctrl+x */ build_defaults(km); } static void drop_action(cst_keymap_t *km, int action) { int o = 0; for (int i = 0; i < km->n; i++) if (km->b[i].action != action) km->b[o++] = km->b[i]; km->n = o; } static void errf(char *err, size_t err_sz, int *bad, const char *what, const char *name) { if (err && err_sz && !*bad) snprintf(err, err_sz, "keybinds.json: %s \"%.60s\"", what, name); *bad = 1; } int cst_keymap_apply_json(cst_keymap_t *km, const char *json, char *err, size_t err_sz) { if (err && err_sz) err[0] = 0; cJSON *root = cJSON_Parse(json ? json : ""); if (!cJSON_IsObject(root)) { cJSON_Delete(root); if (err && err_sz) snprintf(err, err_sz, "keybinds.json: not a JSON object"); return -1; } int bad = 0; const cJSON *ld = cJSON_GetObjectItemCaseSensitive(root, "leader"); if (cJSON_IsString(ld)) { int k = cst_key_parse(ld->valuestring); if (k > 0) { km->leader = k; build_defaults(km); } else { errf(err, err_sz, &bad, "bad leader key", ld->valuestring); } } const cJSON *it; cJSON_ArrayForEach(it, root) { if (!it->string || strcmp(it->string, "leader") == 0) continue; int action = cst_key_action_find(it->string); if (action == CST_KA_NONE) { errf(err, err_sz, &bad, "unknown action", it->string); continue; } drop_action(km, action); const cJSON *v; if (cJSON_IsString(it)) { if (strcmp(it->valuestring, "none") == 0) continue; int keys[2], n = parse_seq(km, it->valuestring, keys); if (n) add(km, action, keys, n, 1); else errf(err, err_sz, &bad, "bad key", it->valuestring); } else if (cJSON_IsArray(it)) { cJSON_ArrayForEach(v, it) { if (!cJSON_IsString(v)) continue; int keys[2], n = parse_seq(km, v->valuestring, keys); if (n) add(km, action, keys, n, 1); else errf(err, err_sz, &bad, "bad key", v->valuestring); } } else { errf(err, err_sz, &bad, "expected a string or list for", it->string); } } cJSON_Delete(root); return bad ? -1 : 0; } int cst_keymap_load(cst_keymap_t *km, char *err, size_t err_sz) { cst_keymap_defaults(km); if (err && err_sz) err[0] = 0; char dir[OS_PATH_MAX], path[OS_PATH_MAX]; if (os_config_dir(dir, sizeof(dir)) != 0 || os_path_join(path, sizeof(path), dir, "keybinds.json") != 0) return 0; FILE *fp = fopen(path, "rb"); if (!fp) return 0; char buf[16384]; size_t n = fread(buf, 1, sizeof(buf) - 1, fp); fclose(fp); buf[n] = 0; return cst_keymap_apply_json(km, buf + cst_bom_len(buf, n), err, err_sz); } int cst_keymap_lookup(const cst_keymap_t *km, const int *seq, int n, int *action) { int prefix = 0; for (int i = 0; i < km->n; i++) { const cst_binding_t *b = &km->b[i]; if (b->keys[0] != seq[0]) continue; if (b->nkeys == n && (n == 1 || b->keys[1] == seq[1])) { *action = b->action; return CST_KM_MATCH; } if (n == 1 && b->nkeys == 2) prefix = 1; } return prefix ? CST_KM_PREFIX : CST_KM_NONE; } static void seq_name(const cst_binding_t *b, char *buf, size_t n) { char a[32], c[32]; cst_key_name(b->keys[0], a, sizeof(a)); if (b->nkeys > 1) snprintf(buf, n, "%s %s", a, cst_key_name(b->keys[1], c, sizeof(c))); else snprintf(buf, n, "%s", a); } void cst_keymap_describe(const cst_keymap_t *km, int action, char *buf, size_t n) { size_t o = 0; buf[0] = 0; for (int i = 0; i < km->n && o + 1 < n; i++) { if (km->b[i].action != action) continue; char s[80]; seq_name(&km->b[i], s, sizeof(s)); int w = snprintf(buf + o, n - o, "%s%s", o ? ", " : "", s); if (w < 0 || (size_t)w >= n - o) break; o += (size_t)w; } } int cst_keymap_conflicts(const cst_keymap_t *km, char *buf, size_t n) { int count = 0; size_t o = 0; if (n) buf[0] = 0; for (int i = 0; i < km->n; i++) { const cst_binding_t *a = &km->b[i]; for (int j = i + 1; j < km->n; j++) { const cst_binding_t *b = &km->b[j]; int same = a->keys[0] == b->keys[0] && a->nkeys == b->nkeys && (a->nkeys == 1 || a->keys[1] == b->keys[1]); int shadow = a->keys[0] == b->keys[0] && a->nkeys != b->nkeys; if ((!same && !shadow) || a->action == b->action) continue; count++; char s[80]; seq_name(a->nkeys <= b->nkeys ? a : b, s, sizeof(s)); if (o + 1 < n) { int w = snprintf(buf + o, n - o, "%s%s: %s, %s", o ? "\n" : "", s, INFO[a->action].name, INFO[b->action].name); if (w > 0 && (size_t)w < n - o) o += (size_t)w; } } } return count; }