commits
tags
#include "keymap.h"
#include <string.h>
#include <ctype.h>
#include <stddef.h>
KeyBinding g_keymap[ACTION_COUNT];
void keymap_set_defaults(KeyBinding *out) {
out[ACTION_SAVE] = (KeyBinding){ SDLK_s, true, false, false };
out[ACTION_OPEN] = (KeyBinding){ SDLK_o, true, false, false };
out[ACTION_COPY] = (KeyBinding){ SDLK_c, true, false, false };
out[ACTION_PASTE] = (KeyBinding){ SDLK_v, true, false, false };
out[ACTION_UNDO] = (KeyBinding){ SDLK_z, true, false, false };
out[ACTION_REDO] = (KeyBinding){ SDLK_z, true, true, false };
out[ACTION_QUIT] = (KeyBinding){ SDLK_x, true, false, false };
out[ACTION_HELP] = (KeyBinding){ SDLK_SLASH, true, false, false };
out[ACTION_SOURCE_VIEW] = (KeyBinding){ SDLK_m, true, false, false };
out[ACTION_ZOOM_IN] = (KeyBinding){ SDLK_PLUS, true, false, false };
out[ACTION_ZOOM_OUT] = (KeyBinding){ SDLK_MINUS, true, false, false };
out[ACTION_ZOOM_RESET] = (KeyBinding){ SDLK_0, true, false, false };
}
static const char *trim(const char *s, int len, int *outLen) {
while (len > 0 && isspace((unsigned char)s[0])) { s++; len--; }
while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
*outLen = len;
return s;
}
static bool token_is(const char *tok, int len, const char *name) {
size_t n = strlen(name);
if ((size_t)len != n) return false;
for (size_t i = 0; i < n; i++) {
if (tolower((unsigned char)tok[i]) != (unsigned char)name[i]) return false;
}
return true;
}
static int lookup_key_token(const char *tok, int len) {
if (len == 1) {
char c = tok[0];
if (c >= 'a' && c <= 'z') return SDLK_a + (c - 'a');
if (c >= 'A' && c <= 'Z') return SDLK_a + (c - 'A');
if (c >= '0' && c <= '9') return SDLK_0 + (c - '0');
switch (c) {
case '/': return SDLK_SLASH;
case '=': return SDLK_EQUALS;
case '+': return SDLK_PLUS;
case '-': return SDLK_MINUS;
case ',': return SDLK_COMMA;
case '.': return SDLK_PERIOD;
case ';': return SDLK_SEMICOLON;
case '\'': return SDLK_QUOTE;
case '\\': return SDLK_BACKSLASH;
case '[': return SDLK_LEFTBRACKET;
case ']': return SDLK_RIGHTBRACKET;
case '`': return SDLK_BACKQUOTE;
}
return SDLK_UNKNOWN;
}
static const struct { const char *name; int key; } NAMED[] = {
{ "f1", SDLK_F1 }, { "f2", SDLK_F2 }, { "f3", SDLK_F3 }, { "f4", SDLK_F4 },
{ "f5", SDLK_F5 }, { "f6", SDLK_F6 }, { "f7", SDLK_F7 }, { "f8", SDLK_F8 },
{ "f9", SDLK_F9 }, { "f10", SDLK_F10 }, { "f11", SDLK_F11 }, { "f12", SDLK_F12 },
{ "slash", SDLK_SLASH }, { "equal", SDLK_EQUALS }, { "plus", SDLK_PLUS }, { "minus", SDLK_MINUS },
{ "comma", SDLK_COMMA }, { "period", SDLK_PERIOD }, { "semicolon", SDLK_SEMICOLON },
{ "apostrophe", SDLK_QUOTE }, { "backslash", SDLK_BACKSLASH },
{ "leftbracket", SDLK_LEFTBRACKET }, { "rightbracket", SDLK_RIGHTBRACKET },
{ "grave", SDLK_BACKQUOTE }, { "space", SDLK_SPACE }, { "enter", SDLK_RETURN },
{ "escape", SDLK_ESCAPE }, { "tab", SDLK_TAB }, { "backspace", SDLK_BACKSPACE },
{ "delete", SDLK_DELETE }, { "home", SDLK_HOME }, { "end", SDLK_END },
{ "pageup", SDLK_PAGEUP }, { "pagedown", SDLK_PAGEDOWN },
{ "up", SDLK_UP }, { "down", SDLK_DOWN }, { "left", SDLK_LEFT }, { "right", SDLK_RIGHT },
{ "insert", SDLK_INSERT },
};
for (size_t i = 0; i < sizeof(NAMED) / sizeof(NAMED[0]); i++) {
if (token_is(tok, len, NAMED[i].name)) return NAMED[i].key;
}
return SDLK_UNKNOWN;
}
bool keymap_parse_spec(const char *spec, int len, KeyBinding *out) {
int trimmedLen;
const char *s = trim(spec, len, &trimmedLen);
if (trimmedLen == 0) return false;
bool ctrl = false, shift = false, alt = false;
int key = SDLK_UNKNOWN;
const char *p = s;
int remaining = trimmedLen;
while (remaining > 0) {
const char *plus = memchr(p, '+', (size_t)remaining);
/* A '+' that's the very last character left can't be a separator (nothing follows it
to separate) -- treat it as the literal '+' key instead, so e.g. "ctrl++" parses as
ctrl + the '+' key, not ctrl + an empty trailing token. */
if (plus && (plus - p) == remaining - 1) plus = NULL;
int tokLen = plus ? (int)(plus - p) : remaining;
int tlen;
const char *tok = trim(p, tokLen, &tlen);
if (tlen == 0) return false;
if (token_is(tok, tlen, "ctrl") || token_is(tok, tlen, "control")) ctrl = true;
else if (token_is(tok, tlen, "shift")) shift = true;
else if (token_is(tok, tlen, "alt")) alt = true;
else {
int k = lookup_key_token(tok, tlen);
if (k == SDLK_UNKNOWN) return false;
key = k;
}
if (!plus) break;
remaining -= (int)(plus - p) + 1;
p = plus + 1;
}
if (key == SDLK_UNKNOWN) return false;
out->key = key;
out->ctrl = ctrl;
out->shift = shift;
out->alt = alt;
return true;
}
static const char *ACTION_CONFIG_NAMES[ACTION_COUNT] = {
[ACTION_SAVE] = "key.save",
[ACTION_OPEN] = "key.open",
[ACTION_COPY] = "key.copy",
[ACTION_PASTE] = "key.paste",
[ACTION_UNDO] = "key.undo",
[ACTION_REDO] = "key.redo",
[ACTION_QUIT] = "key.quit",
[ACTION_HELP] = "key.help",
[ACTION_SOURCE_VIEW] = "key.source_view",
[ACTION_ZOOM_IN] = "key.zoom_in",
[ACTION_ZOOM_OUT] = "key.zoom_out",
[ACTION_ZOOM_RESET] = "key.zoom_reset",
};
bool keymap_parse_config_line(const char *key, int keyLen, const char *value, int valueLen, KeyBinding *keymap) {
for (int i = 0; i < ACTION_COUNT; i++) {
size_t nameLen = strlen(ACTION_CONFIG_NAMES[i]);
if ((size_t)keyLen == nameLen && memcmp(key, ACTION_CONFIG_NAMES[i], nameLen) == 0) {
KeyBinding b;
if (!keymap_parse_spec(value, valueLen, &b)) return false;
keymap[i] = b;
return true;
}
}
return false;
}
#define KEYMAP_MAX_KEYDOWNS_PER_FRAME 16
static SDL_Keycode s_keydowns[KEYMAP_MAX_KEYDOWNS_PER_FRAME];
static int s_keydownCount;
void keymap_begin_frame(void) {
s_keydownCount = 0;
}
void keymap_note_keydown(SDL_Keycode key) {
if (s_keydownCount < KEYMAP_MAX_KEYDOWNS_PER_FRAME) s_keydowns[s_keydownCount++] = key;
}
bool keymap_key_pressed_raw(SDL_Keycode key) {
for (int i = 0; i < s_keydownCount; i++) {
if (s_keydowns[i] == key) return true;
}
return false;
}
/* Unlike raylib's physical-scancode KEY_*, SDL_Keycode for a symbol/punctuation key already
reflects whatever the CURRENT layout needs to produce it -- e.g. "=" is Shift+0 on a Swedish
keyboard, so a KEYDOWN event carrying SDLK_EQUALS arrives with KMOD_SHIFT set. Requiring an
*additional* exact shift==false match on top of that (as stored in a "ctrl+=" binding, where
`shift` just means "the word 'shift' wasn't in the spec") would make such a binding permanently
unsatisfiable on layouts where shift is needed to type the base character at all. Letters and
digits don't have this problem -- SDLK_a/SDLK_0 etc. stay constant regardless of shift, so an
explicit shift modifier there (e.g. "ctrl+shift+z" vs "ctrl+z") is a real, independent, and
layout-independent distinction worth enforcing exactly. */
static bool key_is_alnum(int key) {
return (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_0 && key <= SDLK_9);
}
static bool modifiers_match(const KeyBinding *b, bool ctrl, bool shift, bool alt) {
if (b->ctrl != ctrl || b->alt != alt) return false;
if (key_is_alnum(b->key)) return b->shift == shift;
return true;
}
bool keymap_pressed(Action a, bool ctrl, bool shift, bool alt) {
KeyBinding *b = &g_keymap[a];
if (b->key == SDLK_UNKNOWN) return false;
if (!modifiers_match(b, ctrl, shift, alt)) return false;
return keymap_key_pressed_raw(b->key);
}
bool keymap_repeat_scancode(SDL_Scancode sc, double now, double *nextRepeatAt) {
if (!SDL_GetKeyboardState(NULL)[sc]) { *nextRepeatAt = 0.0; return false; }
if (now >= *nextRepeatAt) { *nextRepeatAt = now + (*nextRepeatAt == 0.0 ? 0.4 : 0.03); return true; }
return false;
}
bool keymap_repeat(Action a, bool ctrl, bool shift, bool alt, double now, double *nextRepeatAt) {
KeyBinding *b = &g_keymap[a];
/* Deliberately exact-match shift here (unlike keymap_pressed above), even for non-alnum
keys: this resolves the binding's keycode back to a physical scancode via
SDL_GetScancodeFromKey, which is a static/unshifted reverse mapping -- on a layout where
two different keycodes share one physical key (e.g. Swedish's "0" key types "0" plain,
"=" with shift), SDLK_EQUALS and SDLK_0 would both resolve to the same scancode, and
relaxing shift here would make holding plain Ctrl+0 ambiguously also satisfy a "ctrl+="
binding. The numpad fallbacks main.c wires in independently cover zoom reliably regardless
of whether this primary binding's repeat path is reachable on a given layout. */
if (b->key == SDLK_UNKNOWN || b->ctrl != ctrl || b->shift != shift || b->alt != alt) {
*nextRepeatAt = 0.0;
return false;
}
return keymap_repeat_scancode(SDL_GetScancodeFromKey(b->key), now, nextRepeatAt);
}