Extensions: skills, plugins, MCP, sub-agents; configurable keys
- Skills (SKILL.md discovery, skill tool, /skills /skill) - plugin.json exec tools with permission prompts, timeouts and workspace trust (/plugins) - MCP stdio client: JSON-RPC initialize/tools/list/tools/call, mcp__server__tool, /mcp and sidebar status - task tool with general/explore/plan and .agent/agents/*.md roles, parallel read-only runs, cancel propagation, /agents - Keymap with Ctrl+X leader, keybinds.json overrides and /keys panel - Collapsed thinking, throttled streaming Markdown, leaner tool output, parallel read-only tools - Spawn: close-on-exec pipes and a process-wide fork lock - Tests for keymap, skills, plugins, MCP, sub-agents, shell compaction; docs/plugins.md
This commit is contained in:
+502
@@ -0,0 +1,502 @@
|
||||
#include "cst_keymap.h"
|
||||
|
||||
#include "cJSON.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_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_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;
|
||||
}
|
||||
|
||||
/* "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, 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;
|
||||
}
|
||||
Reference in New Issue
Block a user