Images - vision gate: text-only models get a note instead of image parts and no view_image tool; unknown models retry once without images on HTTP 400 - paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip, pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image file paths attaches them - MCP image content is stored in the session and sent with the tool result - attach: rel path validation, base64 decode, /retry keeps images Model registry - built-in src/models_builtin.json embedded at build time, merged with <config>/models.json and cached GET /models metadata (context, vision, pricing); most specific rule wins per field - /model labels, /status source, --list-models [provider] Cache and cost - stable request prefix (frozen tools, notes as messages), DeepSeek prefix completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
571 lines
17 KiB
C
571 lines
17 KiB
C
#include "cst_keymap.h"
|
|
|
|
#include "cJSON.h"
|
|
#include "cst_util.h"
|
|
#include "os_compat.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
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, "<leader> 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, "<leader> q"},
|
|
{CST_KA_TOGGLE_TOOL, "ctrl+o"},
|
|
{CST_KA_TOGGLE_THOUGHT, "ctrl+r"},
|
|
{CST_KA_THINKING_ALL, "<leader> t"},
|
|
{CST_KA_DETAILS, "<leader> d"},
|
|
{CST_KA_TODO, "ctrl+t"},
|
|
{CST_KA_SIDEBAR, "<leader> 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, "<leader> n"},
|
|
{CST_KA_SESSIONS, "<leader> l"},
|
|
{CST_KA_UNDO, "<leader> u"},
|
|
{CST_KA_RETRY, "<leader> r"},
|
|
{CST_KA_COMPACT, "<leader> c"},
|
|
{CST_KA_COPY, "<leader> y"},
|
|
{CST_KA_EXPORT, "<leader> x"},
|
|
{CST_KA_PASTE_IMAGE, "ctrl+v"},
|
|
{CST_KA_MODEL, "<leader> m"},
|
|
{CST_KA_PROVIDER, "<leader> p"},
|
|
{CST_KA_EFFORT, "<leader> e"},
|
|
{CST_KA_AGENTS, "<leader> a"},
|
|
{CST_KA_SKILLS, "<leader> k"},
|
|
{CST_KA_PLUGINS, "<leader> 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", "<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;
|
|
while (*s) {
|
|
while (*s == ' ')
|
|
s++;
|
|
if (!*s)
|
|
break;
|
|
size_t w = strcspn(s, " ");
|
|
if (n == 2)
|
|
return 0;
|
|
int k = ieq(s, "<leader>", 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;
|
|
}
|