Files
sovereign_browser/src/shortcuts.c
T

268 lines
8.8 KiB
C

/*
* shortcuts.c — configurable keyboard shortcuts for sovereign_browser
*
* Bindings are stored in the SQLite key_value table as:
* shortcut.<action_id> = "<GTK accelerator string>"
* e.g. shortcut.new_tab = "<Control>t"
*
* On load, each stored string is parsed via gtk_accelerator_parse() into
* a (keyval, mods) pair kept in a static array. shortcuts_lookup() does
* a linear scan comparing incoming GdkEventKey values.
*/
#include "shortcuts.h"
#include "settings.h"
#include "db.h"
#include <string.h>
#include <stdlib.h>
#include "../nostr_core_lib/cjson/cJSON.h"
/* ── Registry ───────────────────────────────────────────────────────── */
static const shortcut_meta_t g_registry[SHORTCUT_COUNT] = {
[SHORTCUT_NEW_TAB] = {
"new_tab", "New tab",
"Open a new tab", "<Control>t"
},
[SHORTCUT_NEW_WINDOW] = {
"new_window", "New window",
"Open a new browser window", "<Control>n"
},
[SHORTCUT_CLOSE_TAB] = {
"close_tab", "Close tab",
"Close the active tab", "<Control>w"
},
[SHORTCUT_FOCUS_URL] = {
"focus_url", "Focus URL bar",
"Focus the URL entry of the active tab", "<Control>l"
},
[SHORTCUT_NEXT_TAB] = {
"next_tab", "Next tab (Ctrl+Tab)",
"Cycle to the next tab", "<Control>Tab"
},
[SHORTCUT_PREV_TAB] = {
"prev_tab", "Previous tab (Ctrl+Shift+Tab)",
"Cycle to the previous tab", "<Control><Shift>Tab"
},
[SHORTCUT_NEXT_TAB_PAGEDOWN] = {
"next_tab_pagedown", "Next tab (PageDown)",
"Switch to the next tab", "<Control>Page_Down"
},
[SHORTCUT_PREV_TAB_PAGEUP] = {
"prev_tab_pageup", "Previous tab (PageUp)",
"Switch to the previous tab", "<Control>Page_Up"
},
[SHORTCUT_RELOAD] = {
"reload", "Reload page",
"Reload the current page", "F5"
},
[SHORTCUT_FORCE_RELOAD] = {
"force_reload", "Force reload",
"Reload bypassing cache", "<Shift>F5"
},
[SHORTCUT_GO_BACK] = {
"go_back", "Go back",
"Navigate back in history", "<Alt>Left"
},
[SHORTCUT_GO_FORWARD] = {
"go_forward", "Go forward",
"Navigate forward in history", "<Alt>Right"
},
[SHORTCUT_FIND] = {
"find", "Find in page",
"Open the find bar", "<Control>f"
},
[SHORTCUT_OPEN_SETTINGS] = {
"open_settings", "Open settings",
"Open the sovereign://settings page", "<Control>comma"
},
[SHORTCUT_NEW_IDENTITY] = {
"new_identity", "Switch identity",
"Open the Nostr login dialog to switch identity",
"<Control><Shift>u"
},
[SHORTCUT_TOGGLE_FULLSCREEN] = {
"toggle_fullscreen", "Toggle fullscreen",
"Enter or exit fullscreen mode", "F11"
},
[SHORTCUT_TOGGLE_INSPECTOR] = {
"toggle_inspector", "Toggle inspector",
"Show or hide the WebKit Web Inspector", "<Control><Shift>i"
},
[SHORTCUT_TOGGLE_SIDEBAR] = {
"toggle_sidebar", "Toggle agent sidebar",
"Show or hide the agent chat sidebar", "<Control><Shift>a"
},
[SHORTCUT_ZOOM_IN] = {
"zoom_in", "Zoom in",
"Increase the zoom level of the active tab", "<Control>plus"
},
[SHORTCUT_ZOOM_OUT] = {
"zoom_out", "Zoom out",
"Decrease the zoom level of the active tab", "<Control>minus"
},
[SHORTCUT_ZOOM_RESET] = {
"zoom_reset", "Reset zoom",
"Reset the zoom level of the active tab to 100%", "<Control>0"
},
};
/* ── In-memory parsed bindings ──────────────────────────────────────── */
typedef struct {
guint keyval;
GdkModifierType mods;
char accel_str[64]; /* current string, for UI + sync */
} parsed_binding_t;
static parsed_binding_t g_parsed[SHORTCUT_COUNT];
/* ── Helpers ────────────────────────────────────────────────────────── */
/* db_kv key for an action: "shortcut.new_tab" */
static void kv_key(shortcut_action_t a, char *buf, size_t buflen) {
snprintf(buf, buflen, "shortcut.%s", g_registry[a].id);
}
/* Parse an accelerator string into the g_parsed slot and update accel_str.
* gtk_accelerator_parse() returns void in GTK3, so we check keyval==0
* to detect a parse failure. */
static int parse_and_store(shortcut_action_t a, const char *accel_str) {
if (accel_str == NULL || accel_str[0] == '\0') return -1;
guint keyval = 0;
GdkModifierType mods = 0;
gtk_accelerator_parse(accel_str, &keyval, &mods);
if (keyval == 0) {
return -1;
}
g_parsed[a].keyval = keyval;
g_parsed[a].mods = mods;
snprintf(g_parsed[a].accel_str, sizeof(g_parsed[a].accel_str),
"%s", accel_str);
return 0;
}
/* ── Public API ─────────────────────────────────────────────────────── */
void shortcuts_load(void) {
/* Start with defaults. */
for (int i = 0; i < SHORTCUT_COUNT; i++) {
const char *dflt = g_registry[i].dflt;
parse_and_store((shortcut_action_t)i, dflt);
}
/* Override with stored values from db_kv. */
for (int i = 0; i < SHORTCUT_COUNT; i++) {
char key[64];
kv_key((shortcut_action_t)i, key, sizeof(key));
const char *val = db_kv_get(key);
if (val && val[0]) {
if (parse_and_store((shortcut_action_t)i, val) != 0) {
/* Bad stored value — keep the default. */
g_printerr("[shortcuts] Bad stored value for %s: %s\n",
key, val);
}
}
}
}
int shortcuts_lookup(GdkEventKey *event) {
if (event == NULL) return -1;
/* Master toggle: if shortcuts are disabled, bail out. */
const browser_settings_t *s = settings_get();
if (!s->ctrl_tab_switch) return -1;
guint event_mods = event->state & gtk_accelerator_get_default_mod_mask();
/* gtk_accelerator_parse() stores lowercase keyvals for letter keys,
* but GdkEventKey delivers the uppercase keyval when Shift is held.
* Normalize the event keyval to lowercase for comparison. */
guint event_keyval = gdk_keyval_to_lower(event->keyval);
for (int i = 0; i < SHORTCUT_COUNT; i++) {
if (g_parsed[i].keyval == 0) continue;
if (g_parsed[i].keyval == event_keyval &&
g_parsed[i].mods == event_mods) {
return i;
}
}
return -1;
}
const char *shortcuts_get(shortcut_action_t action) {
if (action < 0 || action >= SHORTCUT_COUNT) return NULL;
return g_parsed[action].accel_str;
}
int shortcuts_set(shortcut_action_t action, const char *accel_str) {
if (action < 0 || action >= SHORTCUT_COUNT) return -1;
if (accel_str == NULL) return -1;
if (parse_and_store(action, accel_str) != 0) {
return -1;
}
char key[64];
kv_key(action, key, sizeof(key));
db_kv_set(key, g_parsed[action].accel_str);
return 0;
}
void shortcuts_reset(shortcut_action_t action) {
if (action < 0 || action >= SHORTCUT_COUNT) return;
shortcuts_set(action, g_registry[action].dflt);
}
void shortcuts_reset_all(void) {
for (int i = 0; i < SHORTCUT_COUNT; i++) {
shortcuts_reset((shortcut_action_t)i);
}
}
const shortcut_meta_t *shortcuts_meta(shortcut_action_t action) {
if (action < 0 || action >= SHORTCUT_COUNT) return NULL;
return &g_registry[action];
}
int shortcuts_action_from_id(const char *id) {
if (id == NULL) return -1;
for (int i = 0; i < SHORTCUT_COUNT; i++) {
if (strcmp(g_registry[i].id, id) == 0) return i;
}
return -1;
}
/* ── Serialization (for NIP-78 sync) ────────────────────────────────── */
cJSON *shortcuts_serialize(void) {
cJSON *obj = cJSON_CreateObject();
for (int i = 0; i < SHORTCUT_COUNT; i++) {
cJSON_AddStringToObject(obj, g_registry[i].id,
g_parsed[i].accel_str);
}
return obj;
}
void shortcuts_merge_from_json(const cJSON *json) {
if (json == NULL || !cJSON_IsObject(json)) return;
cJSON *item;
cJSON_ArrayForEach(item, json) {
if (!cJSON_IsString(item)) continue;
int action = shortcuts_action_from_id(item->string);
if (action < 0) continue; /* unknown key — ignore */
if (parse_and_store((shortcut_action_t)action,
item->valuestring) == 0) {
char key[64];
kv_key((shortcut_action_t)action, key, sizeof(key));
db_kv_set(key, g_parsed[action].accel_str);
}
}
}