aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/gsr-hotkeys.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gsr-hotkeys.c')
-rw-r--r--src/gsr-hotkeys.c435
1 files changed, 435 insertions, 0 deletions
diff --git a/src/gsr-hotkeys.c b/src/gsr-hotkeys.c
new file mode 100644
index 0000000..5c1b7f7
--- /dev/null
+++ b/src/gsr-hotkeys.c
@@ -0,0 +1,435 @@
+#include "gsr-hotkeys.h"
+#include "gsr-window.h"
+#include "gsr-config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * On X11 we use gsr-x11-hotkeys.h for XGrabKey + GSource polling.
+ * On Wayland we use global_shortcuts.h for the D-Bus portal.
+ *
+ * Each backend is conditionally compiled via HAVE_X11 / HAVE_WAYLAND.
+ */
+
+#ifdef HAVE_X11
+#include "gsr-x11-hotkeys.h"
+#include <X11/keysym.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/x11/gdkx.h>
+#endif
+#endif /* HAVE_X11 */
+
+#ifdef HAVE_WAYLAND
+#include "global_shortcuts.h"
+#endif
+
+/* ── Shortcut IDs (Wayland portal) ────────────────────────────────── */
+
+#ifdef HAVE_WAYLAND
+#define SHORTCUT_ID_START_STOP "gpu_screen_recorder_start_stop_recording"
+#define SHORTCUT_ID_PAUSE_UNPAUSE "gpu_screen_recorder_pause_unpause_recording"
+#define SHORTCUT_ID_SAVE_REPLAY "gpu_screen_recorder_save_replay"
+#endif
+
+/* ── Internal state ──────────────────────────────────────────────── */
+
+typedef enum {
+ HOTKEY_ACTION_START_STOP,
+ HOTKEY_ACTION_PAUSE_UNPAUSE,
+ HOTKEY_ACTION_SAVE_REPLAY,
+} HotkeyAction;
+
+#ifdef HAVE_X11
+typedef struct {
+ unsigned int x11_modifiers;
+ KeySym keysym;
+ HotkeyAction action;
+} GrabbedCombo;
+
+#define MAX_ACTIVE_COMBOS 4
+#endif /* HAVE_X11 */
+
+struct _GsrHotkeys {
+ GsrDisplayServer display_server;
+ GsrWindow *window; /* NOT owned */
+
+#ifdef HAVE_X11
+ /* X11 backend */
+ GsrX11Hotkeys *x11;
+
+ /* Currently grabbed combos and what action they map to */
+ GrabbedCombo active_combos[MAX_ACTIVE_COMBOS];
+ int n_active_combos;
+#endif /* HAVE_X11 */
+
+#ifdef HAVE_WAYLAND
+ /* Wayland backend */
+ gsr_global_shortcuts wayland;
+ bool wayland_initialized;
+ bool wayland_shortcuts_bound;
+ bool wayland_shortcuts_received;
+#endif /* HAVE_WAYLAND */
+};
+
+/* ── Forward declarations ────────────────────────────────────────── */
+
+#ifdef HAVE_X11
+static void on_x11_hotkey(unsigned int modifiers, KeySym keysym, void *userdata);
+#endif
+
+#ifdef HAVE_WAYLAND
+static void on_wayland_init(bool success, void *userdata);
+static void on_wayland_deactivated(const char *shortcut_id, void *userdata);
+static void on_wayland_shortcut_changed(gsr_shortcut shortcut, void *userdata);
+#endif
+
+/* ── Hotkey action dispatch (shared by both backends) ────────────── */
+
+/**
+ * Get the name of the currently visible page in the AdwViewStack.
+ * Returns a borrowed string like "stream", "record", "replay", "config".
+ */
+static const char *
+get_visible_page_name(GsrHotkeys *self)
+{
+ return gsr_window_get_visible_page_name(self->window);
+}
+
+static void
+dispatch_start_stop(GsrHotkeys *self)
+{
+ gsr_window_hotkey_start_stop(self->window);
+}
+
+static void
+dispatch_pause_unpause(GsrHotkeys *self)
+{
+ gsr_window_hotkey_pause_unpause(self->window);
+}
+
+static void
+dispatch_save_replay(GsrHotkeys *self)
+{
+ gsr_window_hotkey_save_replay(self->window);
+}
+
+/* ── X11 callback ────────────────────────────────────────────────── */
+
+#ifdef HAVE_X11
+static void
+on_x11_hotkey(unsigned int modifiers, KeySym keysym, void *userdata)
+{
+ GsrHotkeys *self = userdata;
+
+ /* Find which action this combo maps to */
+ HotkeyAction action = HOTKEY_ACTION_START_STOP;
+ bool found = false;
+
+ for (int i = 0; i < self->n_active_combos; i++) {
+ if (self->active_combos[i].keysym == keysym &&
+ self->active_combos[i].x11_modifiers == modifiers)
+ {
+ action = self->active_combos[i].action;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ /* Fallback: match by keysym only (modifier might have extra bits
+ * from NumLock/CapsLock that XGrabKey variants handle) */
+ unsigned int base_mods = modifiers & (ControlMask | ShiftMask | Mod1Mask | Mod4Mask);
+ for (int i = 0; i < self->n_active_combos; i++) {
+ if (self->active_combos[i].keysym == keysym &&
+ self->active_combos[i].x11_modifiers == base_mods)
+ {
+ action = self->active_combos[i].action;
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found)
+ return;
+
+ switch (action) {
+ case HOTKEY_ACTION_START_STOP:
+ dispatch_start_stop(self);
+ break;
+ case HOTKEY_ACTION_PAUSE_UNPAUSE:
+ dispatch_pause_unpause(self);
+ break;
+ case HOTKEY_ACTION_SAVE_REPLAY:
+ dispatch_save_replay(self);
+ break;
+ }
+}
+#endif /* HAVE_X11 */
+
+/* ── Wayland callbacks ───────────────────────────────────────────── */
+
+#ifdef HAVE_WAYLAND
+static void
+on_wayland_init(bool success, void *userdata)
+{
+ GsrHotkeys *self = userdata;
+ self->wayland_initialized = success;
+ if (success) {
+ g_debug("Wayland global shortcuts session created");
+ gsr_global_shortcuts_subscribe_activated_signal(
+ &self->wayland, on_wayland_deactivated,
+ on_wayland_shortcut_changed, self);
+ } else {
+ fprintf(stderr, "gsr warning: Wayland global shortcuts init failed\n");
+ }
+
+ /* Notify the window so it can update the UI */
+ gsr_window_on_wayland_hotkeys_init(self->window, success);
+}
+
+static void
+on_wayland_deactivated(const char *shortcut_id, void *userdata)
+{
+ GsrHotkeys *self = userdata;
+
+ const char *page = get_visible_page_name(self);
+ if (!page)
+ return;
+
+ /* The portal uses 3 shared IDs across all modes.
+ * Dispatch based on the currently visible page. */
+
+ if (strcmp(shortcut_id, SHORTCUT_ID_START_STOP) == 0) {
+ if (g_str_equal(page, "stream") ||
+ g_str_equal(page, "record") ||
+ g_str_equal(page, "replay"))
+ {
+ dispatch_start_stop(self);
+ }
+ } else if (strcmp(shortcut_id, SHORTCUT_ID_PAUSE_UNPAUSE) == 0) {
+ if (g_str_equal(page, "record"))
+ dispatch_pause_unpause(self);
+ } else if (strcmp(shortcut_id, SHORTCUT_ID_SAVE_REPLAY) == 0) {
+ if (g_str_equal(page, "replay"))
+ dispatch_save_replay(self);
+ }
+}
+
+static void
+on_wayland_shortcut_changed(gsr_shortcut shortcut, void *userdata)
+{
+ GsrHotkeys *self = userdata;
+ self->wayland_shortcuts_received = true;
+ g_debug("Wayland shortcut changed: id=%s trigger=%s",
+ shortcut.id, shortcut.trigger_description);
+}
+#endif /* HAVE_WAYLAND */
+
+/* ── Public API ──────────────────────────────────────────────────── */
+
+GsrHotkeys *
+gsr_hotkeys_new(GsrDisplayServer display_server, GsrWindow *window)
+{
+ GsrHotkeys *self = calloc(1, sizeof(GsrHotkeys));
+ if (!self)
+ return NULL;
+
+ self->display_server = display_server;
+ self->window = window;
+
+#ifdef HAVE_X11
+ if (display_server == GSR_DISPLAY_SERVER_X11) {
+ /*
+ * Get the X11 Display from GDK.
+ * We use gdk_x11_display_get_xdisplay() from <gdk/x11/gdkx.h>.
+ */
+ GdkDisplay *gdk_dpy = gdk_display_get_default();
+ if (!gdk_dpy) {
+ fprintf(stderr, "gsr warning: no default GDK display\n");
+ free(self);
+ return NULL;
+ }
+
+ /* GTK4 X11 backend */
+ Display *xdisplay = NULL;
+
+ /* Use the GDK X11 function if available */
+#ifdef GDK_WINDOWING_X11
+ G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+ xdisplay = gdk_x11_display_get_xdisplay(gdk_dpy);
+ G_GNUC_END_IGNORE_DEPRECATIONS
+#endif
+
+ if (!xdisplay) {
+ fprintf(stderr, "gsr warning: could not get X11 display from GDK\n");
+ free(self);
+ return NULL;
+ }
+
+ self->x11 = gsr_x11_hotkeys_new(xdisplay, on_x11_hotkey, self);
+ if (!self->x11) {
+ fprintf(stderr, "gsr warning: failed to create X11 hotkey watcher\n");
+ free(self);
+ return NULL;
+ }
+
+ g_debug("X11 hotkey manager initialized");
+
+ }
+#endif /* HAVE_X11 */
+
+#ifdef HAVE_WAYLAND
+ if (display_server == GSR_DISPLAY_SERVER_WAYLAND) {
+ /* TODO: Skip Hyprland due to portal crash bug */
+ if (!gsr_global_shortcuts_init(&self->wayland, on_wayland_init, self)) {
+ fprintf(stderr, "gsr warning: failed to initialize Wayland global shortcuts\n");
+ /* Non-fatal: hotkeys just won't work */
+ }
+ }
+#endif /* HAVE_WAYLAND */
+
+ return self;
+}
+
+void
+gsr_hotkeys_free(GsrHotkeys *self)
+{
+ if (!self)
+ return;
+
+#ifdef HAVE_X11
+ if (self->x11) {
+ gsr_x11_hotkeys_free(self->x11);
+ self->x11 = NULL;
+ }
+#endif
+
+#ifdef HAVE_WAYLAND
+ if (self->wayland_initialized) {
+ gsr_global_shortcuts_deinit(&self->wayland);
+ self->wayland_initialized = false;
+ }
+#endif
+
+ free(self);
+}
+
+/* ── Helper: grab a hotkey from config and track its action ──────── */
+
+#ifdef HAVE_X11
+static void
+grab_hotkey_from_config(GsrHotkeys *self,
+ const GsrConfigHotkey *hk,
+ HotkeyAction action)
+{
+ if (gsr_config_hotkey_is_empty(hk))
+ return;
+
+ unsigned int x11_mods = 0;
+ uint64_t keysym = 0;
+ gsr_config_hotkey_to_x11(hk, &x11_mods, &keysym);
+
+ if (keysym == 0)
+ return;
+
+ GsrX11HotkeyCombo combo = { .modifiers = x11_mods, .keysym = (KeySym)keysym };
+ if (!gsr_x11_hotkeys_grab(self->x11, combo)) {
+ g_warning("Failed to grab hotkey (keysym=0x%lx, mods=0x%x)",
+ (unsigned long)keysym, x11_mods);
+ return;
+ }
+
+ if (self->n_active_combos < MAX_ACTIVE_COMBOS) {
+ self->active_combos[self->n_active_combos] = (GrabbedCombo){
+ .x11_modifiers = x11_mods,
+ .keysym = (KeySym)keysym,
+ .action = action,
+ };
+ self->n_active_combos++;
+ }
+}
+
+void
+gsr_hotkeys_regrab_for_visible_page(GsrHotkeys *self)
+{
+ if (!self || self->display_server != GSR_DISPLAY_SERVER_X11 || !self->x11)
+ return;
+
+ /* Ungrab everything first */
+ gsr_x11_hotkeys_ungrab_all(self->x11);
+ self->n_active_combos = 0;
+
+ const char *page = get_visible_page_name(self);
+ if (!page)
+ return;
+
+ const GsrConfig *config = gsr_window_get_config(self->window);
+ if (!config)
+ return;
+
+ /* Only grab hotkeys for action pages (not config) */
+ if (g_str_equal(page, "stream")) {
+ grab_hotkey_from_config(self,
+ &config->streaming_config.start_stop_hotkey,
+ HOTKEY_ACTION_START_STOP);
+
+ } else if (g_str_equal(page, "record")) {
+ grab_hotkey_from_config(self,
+ &config->record_config.start_stop_hotkey,
+ HOTKEY_ACTION_START_STOP);
+ grab_hotkey_from_config(self,
+ &config->record_config.pause_unpause_hotkey,
+ HOTKEY_ACTION_PAUSE_UNPAUSE);
+
+ } else if (g_str_equal(page, "replay")) {
+ grab_hotkey_from_config(self,
+ &config->replay_config.start_stop_hotkey,
+ HOTKEY_ACTION_START_STOP);
+ grab_hotkey_from_config(self,
+ &config->replay_config.save_hotkey,
+ HOTKEY_ACTION_SAVE_REPLAY);
+ }
+}
+#endif /* HAVE_X11 */
+
+#ifdef HAVE_WAYLAND
+void
+gsr_hotkeys_register_wayland_shortcuts_once(GsrHotkeys *self)
+{
+ if (!self || self->display_server != GSR_DISPLAY_SERVER_WAYLAND)
+ return;
+
+ if (!self->wayland_initialized)
+ return;
+
+ /* On GNOME: bind once. On KDE: the user clicks a button (deferred to Phase 7). */
+ if (self->wayland_shortcuts_bound || self->wayland_shortcuts_received)
+ return;
+
+ self->wayland_shortcuts_bound = true;
+
+ const gsr_bind_shortcut shortcuts[3] = {
+ {
+ .description = "Start/stop recording/replay/streaming",
+ .shortcut = { .id = SHORTCUT_ID_START_STOP, .trigger_description = "ALT+1" },
+ },
+ {
+ .description = "Pause/unpause recording",
+ .shortcut = { .id = SHORTCUT_ID_PAUSE_UNPAUSE, .trigger_description = "ALT+2" },
+ },
+ {
+ .description = "Save replay",
+ .shortcut = { .id = SHORTCUT_ID_SAVE_REPLAY, .trigger_description = "ALT+3" },
+ },
+ };
+
+ if (!gsr_global_shortcuts_bind_shortcuts(&self->wayland, shortcuts, 3,
+ on_wayland_shortcut_changed, self))
+ {
+ fprintf(stderr, "gsr warning: failed to bind Wayland shortcuts\n");
+ }
+}
+#endif /* HAVE_WAYLAND */