268 lines
8.8 KiB
C
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);
|
|
}
|
|
}
|
|
}
|