From a9b22b6569460db7b0ba54349eeaafa10cca5268 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 14 Jul 2026 16:56:27 +0100 Subject: [PATCH 01/10] Initial working hotkeys with xdg-desktop-portal. --- libobs/CMakeLists.txt | 3 + libobs/obs-hotkey-portal.c | 366 +++++++++++++++++++++++++++++++++++++ libobs/obs-hotkey-portal.h | 41 +++++ libobs/obs-hotkey.c | 7 + libobs/obs-nix-wayland.c | 9 + 5 files changed, 426 insertions(+) create mode 100644 libobs/obs-hotkey-portal.c create mode 100644 libobs/obs-hotkey-portal.h diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index b20c2fa476ce5c..52afae2519fd8b 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -48,6 +48,8 @@ target_sources( obs-hotkey.c obs-hotkey.h obs-hotkeys.h + obs-hotkey-portal.c + obs-hotkey-portal.h obs-interaction.h obs-internal.h obs-missing-files.c @@ -312,6 +314,7 @@ set( obs-encoder.h obs-hotkey.h obs-hotkeys.h + obs-hotkey-portal.h obs-interaction.h obs-missing-files.h obs-module.h diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c new file mode 100644 index 00000000000000..d64ae5406c9a21 --- /dev/null +++ b/libobs/obs-hotkey-portal.c @@ -0,0 +1,366 @@ +#include "obs-hotkey-portal.h" +#include "obs.h" +#include "util/base.h" +#include "util/c99defs.h" +#include +#include + +#define PORTAL_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" +#define SHORTCUTS_IFACE "org.freedesktop.portal.GlobalShortcuts" +#define REQUEST_IFACE "org.freedesktop.portal.Request" +#define SESSION_IFACE "org.freedesktop.portal.Session" + +static struct obs_hotkey_portal_state { + GDBusConnection *conn; + // GVariant *session_path; + char *session_path; + // tracks the timestamp when pending_hotkeys was last added to + long add_to_pending_hotkeys_timestamp_us; + GQueue *pending_hotkeys; + // TODO: use the hash table that obs-hotkey already uses instead of + // this array. I just want to get this working first + GArray *registered_hotkeys; +} *state = NULL; + +typedef struct obs_hotkey_portal_state obs_hotkey_portal_state_t; + +bool obs_hotkey_portal_session_active() +{ + return state != NULL && state->session_path != NULL; +} + +GString *get_formatted_sender(GDBusConnection *conn) { + g_autoptr(GString) temp = NULL; + GString *unique_name = NULL; + temp = g_string_new(g_dbus_connection_get_unique_name(conn)); + + g_string_replace(temp, ".", "_", 0); + + if (temp->len < 1) { + return NULL; + } + + unique_name = g_string_new(NULL); + g_string_printf(unique_name, "%s", &temp->str[1]); + return unique_name; +} + +GString *get_hotkey_portal_id(obs_hotkey_t *hotkey) { + if (hotkey == NULL || hotkey->name == NULL) { + return NULL; + } + + GString *ret_val = g_string_new(hotkey->name); + const char *id = NULL; + switch(hotkey->registerer_type) { + case OBS_HOTKEY_REGISTERER_FRONTEND: + break; + case OBS_HOTKEY_REGISTERER_SOURCE: { + id = obs_source_get_id(hotkey->registerer); + break; + } + case OBS_HOTKEY_REGISTERER_OUTPUT: { + id = obs_output_get_id(hotkey->registerer); + break; + } + case OBS_HOTKEY_REGISTERER_ENCODER: { + id = obs_encoder_get_id(hotkey->registerer); + break; + } + case OBS_HOTKEY_REGISTERER_SERVICE: { + id = obs_service_get_id(hotkey->registerer); + break; + } + default: + break; + } + + // if (id != NULL) { + // g_print("Hotkey portal id: %s\n", ret_val->str); + // g_string_append_printf(ret_val, ".%s", id); + // g_print("Hotkey portal id2: %s\n", ret_val->str); + // } + return ret_val; +} + +void obs_hotkey_portal_free() { + if (state == NULL) { + return; + } + g_dbus_connection_close_sync(state->conn, NULL, NULL); + g_object_unref(state->conn); + g_free(state->session_path); + g_queue_free(state->pending_hotkeys); + g_array_free(state->registered_hotkeys, true); + g_free(state); + state = NULL; +} + +// register all pending hotkeys +static gboolean obs_hotkey_portal_finish_registration() { + assert(obs_hotkey_portal_session_active()); + + // check if at least 0.5 seconds have passed since the last time we added to pending_hotkeys + long current_timestamp = g_get_monotonic_time(); + if (current_timestamp - state->add_to_pending_hotkeys_timestamp_us < 500000) { + return G_SOURCE_CONTINUE; + } + + // check if there are any pending hotkeys + if (g_queue_is_empty(state->pending_hotkeys)) { + return G_SOURCE_CONTINUE; + } + + // bind some shortcuts + GVariantBuilder shortcuts_builder; + g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a(sa{sv})")); + for (obs_hotkey_t *hotkey = g_queue_pop_head(state->pending_hotkeys); + hotkey != NULL; + hotkey = g_queue_pop_head(state->pending_hotkeys)) + { + // description + GVariantBuilder vardict_builder; + g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(hotkey->description)); + blog(LOG_INFO, "Registering hotkey with portal: %s: %s\n", hotkey->name, hotkey->description); + + // add shortcut to builder + GVariant *data[] = {g_variant_new_string(get_hotkey_portal_id(hotkey)->str), g_variant_builder_end(&vardict_builder)}; + g_variant_builder_add_value(&shortcuts_builder, g_variant_new_tuple(data, 2)); + + // add shortcut to registered list + g_array_append_val(state->registered_hotkeys, *hotkey); + } + + // options + GVariantBuilder options_builder; + char request_handle_token[64] = {}; + snprintf(request_handle_token, sizeof(request_handle_token), "obs_globalshortcuts_request%d", rand()); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&options_builder, "{sv}", "handle_token", g_variant_new_string(request_handle_token)); + + GVariant *options[] = {g_variant_new_object_path(state->session_path), g_variant_builder_end(&shortcuts_builder), g_variant_new_string(""), g_variant_builder_end(&options_builder)}; + g_dbus_connection_call(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "BindShortcuts", g_variant_new_tuple(options, 4), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + + return G_SOURCE_CONTINUE; +} + +static void activated_cb( + GDBusConnection* conn, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data +) { + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + + g_autofree char *session_path = NULL; + g_autofree char *shortcut_name = NULL; + uint64_t timestamp; + g_autoptr(GVariant) options = NULL; + + g_variant_get(parameters, "(ost@a{sv})", &session_path, &shortcut_name, ×tamp, &options); + blog(LOG_INFO, "Portal hotkey activated: %s\n", shortcut_name); + + // find shortcut id in registered list + for (unsigned i = 0; i < state->registered_hotkeys->len; ++i) { + obs_hotkey_t *hotkey = &g_array_index(state->registered_hotkeys, obs_hotkey_t, i); + + if (strcmp(hotkey->name, shortcut_name) != 0) { + continue; + } + + hotkey->func(hotkey->data, hotkey->id, hotkey, true); + return; + } +} + +static void deactivated_cb( + GDBusConnection* conn, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data +) { + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + + g_print("Button released!\n"); +} + +static void shortcuts_changed_cb( + GDBusConnection* conn, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data +) { + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + + // TODO: update UI? + g_print("Shortcuts changed!\n"); +} + +static void create_session_cb( + GDBusConnection* conn, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data +) { + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(user_data); + + int response; + g_autoptr(GVariant) options = NULL; + const char *session_handle = NULL; + + g_variant_get(parameters, "(u@a{sv})", &response, &options); + g_variant_lookup(options, "session_handle", "&s", &session_handle); + + // start listening to shortcuts + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Activated", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, activated_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Deactivated", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, deactivated_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "ShortcutsChanged", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, shortcuts_changed_cb, NULL, NULL); + + state->session_path = bstrdup(session_handle); + + // schedule a timer to register all pending hotkeys every 0.1 seconds + g_timeout_add(500, (GSourceFunc)obs_hotkey_portal_finish_registration, NULL); +} + +void obs_hotkey_portal_init() +{ + // only allow this function to run once + static bool init_run = false; + if (init_run) { + return; + } + init_run = true; + + g_autoptr(GDBusConnection) conn = NULL; + GError *error = NULL; + GVariant *options = NULL; + g_autoptr(GString) request_handle_token = NULL; + g_autoptr(GString) session_token = NULL; + g_autoptr(GString) request_path = NULL; + g_autoptr(GVariant) result = NULL; + g_autoptr(GString) sender = NULL; + GVariantBuilder options_builder; + + // get connection + conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + if (!conn) { + g_printerr("Failed to connect to dbus: %s\n", error->message); + return; + } + + sender = get_formatted_sender(conn); + + // get tokens + session_token = g_string_new(NULL); + g_string_printf(session_token, "obs_globalshortcuts_session%d", rand()); + + request_handle_token = g_string_new(NULL); + g_string_printf(request_handle_token, "obs_globalshortcuts_request%d", rand()); + + // connect to session parameters + g_variant_builder_init(&options_builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&options_builder, "{sv}", "handle_token", g_variant_new_string(request_handle_token->str)); + g_variant_builder_add(&options_builder, "{sv}", "session_handle_token", g_variant_new_string(session_token->str)); + options = g_variant_builder_end(&options_builder); + + // start to listen to request object in advance + request_path = g_string_new(NULL); + g_string_printf(request_path, "/org/freedesktop/portal/desktop/request/%s/%s", sender->str, request_handle_token->str); + g_print("Listening to request at path: %s\n", request_path->str); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path->str, NULL, G_DBUS_SIGNAL_FLAGS_NONE, create_session_cb, NULL, NULL); + + // finally, connect to session + result = g_dbus_connection_call_sync(conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "CreateSession", g_variant_new_tuple(&options, 1), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + + if (result == NULL) { + blog(LOG_WARNING, "Failed to create global shortcuts portal session: %s", error->message); + return; + } + + // initialise state + state = malloc(sizeof(obs_hotkey_portal_state_t)); + state->conn = conn; + state->session_path = NULL; + state->pending_hotkeys = g_queue_new(); + state->registered_hotkeys = g_array_new(false, false, sizeof(obs_hotkey_t)); +} + +void obs_hotkey_portal_register(obs_hotkey_t *hotkey) +{ + // TODO: check if there is a potential race condition here + // TODO: check if this is even reachable in the first place + if (state == NULL) { + blog(LOG_WARNING, "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + return; + } + + blog(LOG_WARNING, "Registering hotkey with id %lu to xdg-desktop-portal", hotkey->id); + + // add to pending list + g_queue_push_tail(state->pending_hotkeys, hotkey); + state->add_to_pending_hotkeys_timestamp_us = g_get_monotonic_time(); +} + +void obs_hotkey_portal_unregister(obs_hotkey_id id) { + if (state == NULL) { + blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + return; + } + + blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); + + // remove hotkey from registered list + for (unsigned i = 0; i < state->registered_hotkeys->len; ++i) { + obs_hotkey_t *hotkey = &g_array_index(state->registered_hotkeys, obs_hotkey_t, i); + if (hotkey->id == id) { + g_array_remove_index(state->registered_hotkeys, i); + break; + } + } + + // pending list + for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { + obs_hotkey_t *hotkey = (obs_hotkey_t *)l->data; + if (hotkey->id == id) { + g_queue_delete_link(state->pending_hotkeys, l); + break; + } + } +} diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h new file mode 100644 index 00000000000000..a30da5432fa8e3 --- /dev/null +++ b/libobs/obs-hotkey-portal.h @@ -0,0 +1,41 @@ +/****************************************************************************** + Copyright (C) 2026-2026 by Adam Fallon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "obs-internal.h" +#include "obs-hotkey.h" +#include +#include + +// must be called before registering any hotkeys with the portal. +// does nothing if it has already been called +void obs_hotkey_portal_init(); +void obs_hotkey_portal_free(); + +// returns true if the portal session has been started +bool obs_hotkey_portal_session_active(); + +// queues hotkey to be added to portal +// should always be called, even if it has no effect (i.e. if unsupported on +// the current platform) +void obs_hotkey_portal_register(obs_hotkey_t *hotkey); + +// stops listening to a hotkey +// should always be called, even if it has no effect (i.e. if unsupported on +// the current platform) +void obs_hotkey_portal_unregister(obs_hotkey_id id); diff --git a/libobs/obs-hotkey.c b/libobs/obs-hotkey.c index 0503b303390409..33317f84d553b9 100644 --- a/libobs/obs-hotkey.c +++ b/libobs/obs-hotkey.c @@ -17,6 +17,7 @@ #include +#include "obs-hotkey-portal.h" #include "obs-internal.h" /* Since ids are just sequential size_t integers, we don't really need a @@ -181,6 +182,10 @@ static inline obs_hotkey_id obs_hotkey_register_internal(obs_hotkey_registerer_t context_add_hotkey(context, result); } + // add with xdg-desktop-portal + obs_hotkey_portal_init(hotkey); + obs_hotkey_portal_register(hotkey); + hotkey_signal("hotkey_register", hotkey); return result; @@ -866,6 +871,8 @@ static inline void unregister_hotkey(obs_hotkey_id id) bfree(hotkey->description); bfree(hotkey); + obs_hotkey_portal_unregister(id); + remove_bindings(id); } diff --git a/libobs/obs-nix-wayland.c b/libobs/obs-nix-wayland.c index 3155db91adc7a1..d01f704df0d216 100644 --- a/libobs/obs-nix-wayland.c +++ b/libobs/obs-nix-wayland.c @@ -17,8 +17,14 @@ #include "obs-internal.h" #include "obs-nix-platform.h" +#include "util/bmem.h" +#include "util/c99defs.h" +#include "util/dstr.h" #include "obs-nix-wayland.h" +#include +#include +#include #include #include #include @@ -31,6 +37,9 @@ // X11 keymaps only have 4 shift levels, im not sure xkbcommon supports a way to shift the state into a higher level anyway. #define MAX_SHIFT_LEVELS 4 +#define PORTAL_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" + struct obs_hotkeys_platform { struct wl_display *display; struct wl_seat *seat; From 13856053b798676e4e391dcd5c1aa14762500ca9 Mon Sep 17 00:00:00 2001 From: Adam Eric Fallon Date: Wed, 15 Jul 2026 23:24:13 +0100 Subject: [PATCH 02/10] Put name of scene in descriptions of hotkeys. --- libobs/obs-hotkey-portal.c | 136 +++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 58 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index d64ae5406c9a21..a80d0c8154ba24 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -1,9 +1,12 @@ #include "obs-hotkey-portal.h" +#include "obs-hotkey.h" #include "obs.h" #include "util/base.h" +#include "util/bmem.h" #include "util/c99defs.h" #include #include +#include #define PORTAL_NAME "org.freedesktop.portal.Desktop" #define PORTAL_PATH "/org/freedesktop/portal/desktop" @@ -20,7 +23,7 @@ static struct obs_hotkey_portal_state { GQueue *pending_hotkeys; // TODO: use the hash table that obs-hotkey already uses instead of // this array. I just want to get this working first - GArray *registered_hotkeys; + // GArray *registered_hotkeys; } *state = NULL; typedef struct obs_hotkey_portal_state obs_hotkey_portal_state_t; @@ -46,42 +49,67 @@ GString *get_formatted_sender(GDBusConnection *conn) { return unique_name; } -GString *get_hotkey_portal_id(obs_hotkey_t *hotkey) { - if (hotkey == NULL || hotkey->name == NULL) { +char *get_hotkey_portal_id(obs_hotkey_t *hotkey) { + if (hotkey == NULL) { return NULL; } - GString *ret_val = g_string_new(hotkey->name); - const char *id = NULL; - switch(hotkey->registerer_type) { - case OBS_HOTKEY_REGISTERER_FRONTEND: - break; - case OBS_HOTKEY_REGISTERER_SOURCE: { - id = obs_source_get_id(hotkey->registerer); - break; + const char *append_to_id = NULL; + void *registerer = obs_weak_object_get_object(hotkey->registerer); + if (registerer) { + switch(obs_hotkey_get_registerer_type(hotkey)) { + case OBS_HOTKEY_REGISTERER_SOURCE: + append_to_id = obs_source_get_uuid(registerer); + obs_object_release(registerer); + break; + default: + append_to_id = obs_obj_get_id(obs_weak_object_get_object(hotkey->registerer)); + break; + } } - case OBS_HOTKEY_REGISTERER_OUTPUT: { - id = obs_output_get_id(hotkey->registerer); - break; + + // append to id + char *id_dst; + if (append_to_id != NULL) { + id_dst = bzalloc(strlen(hotkey->name) + strlen(append_to_id) + 8); + sprintf(id_dst, "%s.%s", hotkey->name, append_to_id); + return id_dst; + } else { + id_dst = bstrdup(hotkey->name); } - case OBS_HOTKEY_REGISTERER_ENCODER: { - id = obs_encoder_get_id(hotkey->registerer); - break; + + obs_object_release(registerer); + return id_dst; +} + +char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { + if (hotkey == NULL) { + return NULL; } - case OBS_HOTKEY_REGISTERER_SERVICE: { - id = obs_service_get_id(hotkey->registerer); - break; + + const char *append_to_description = NULL; + void *registerer = obs_weak_object_get_object(hotkey->registerer); + if (registerer) { + switch(obs_hotkey_get_registerer_type(hotkey)) { + case OBS_HOTKEY_REGISTERER_SOURCE: + append_to_description = obs_source_get_name(registerer); + blog(LOG_INFO, "Desc: %s", append_to_description); + break; + default: + break; + } } - default: - break; + + char *description_dst; + if (append_to_description != NULL) { + description_dst = bzalloc(strlen(hotkey->description) + strlen(append_to_description) + 8); + sprintf(description_dst, "%s (%s)", hotkey->description, append_to_description); + } else { + description_dst = bstrdup(hotkey->description); } - // if (id != NULL) { - // g_print("Hotkey portal id: %s\n", ret_val->str); - // g_string_append_printf(ret_val, ".%s", id); - // g_print("Hotkey portal id2: %s\n", ret_val->str); - // } - return ret_val; + obs_object_release(registerer); + return description_dst; } void obs_hotkey_portal_free() { @@ -90,10 +118,9 @@ void obs_hotkey_portal_free() { } g_dbus_connection_close_sync(state->conn, NULL, NULL); g_object_unref(state->conn); - g_free(state->session_path); + bfree(state->session_path); g_queue_free(state->pending_hotkeys); - g_array_free(state->registered_hotkeys, true); - g_free(state); + bfree(state); state = NULL; } @@ -119,18 +146,21 @@ static gboolean obs_hotkey_portal_finish_registration() { hotkey != NULL; hotkey = g_queue_pop_head(state->pending_hotkeys)) { + char *id = get_hotkey_portal_id(hotkey); + // description + char *description = get_hotkey_portal_description(hotkey); GVariantBuilder vardict_builder; g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); - g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(hotkey->description)); - blog(LOG_INFO, "Registering hotkey with portal: %s: %s\n", hotkey->name, hotkey->description); + g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(description)); + blog(LOG_INFO, "Registering hotkey with portal: %s: %s\n", hotkey->name, hotkey->description); // add shortcut to builder - GVariant *data[] = {g_variant_new_string(get_hotkey_portal_id(hotkey)->str), g_variant_builder_end(&vardict_builder)}; + GVariant *data[] = {g_variant_new_string(id), g_variant_builder_end(&vardict_builder)}; g_variant_builder_add_value(&shortcuts_builder, g_variant_new_tuple(data, 2)); - // add shortcut to registered list - g_array_append_val(state->registered_hotkeys, *hotkey); + bfree(id); + bfree(description); } // options @@ -165,21 +195,22 @@ static void activated_cb( UNUSED_PARAMETER(user_data); g_autofree char *session_path = NULL; - g_autofree char *shortcut_name = NULL; + g_autofree char *received_id = NULL; uint64_t timestamp; g_autoptr(GVariant) options = NULL; - g_variant_get(parameters, "(ost@a{sv})", &session_path, &shortcut_name, ×tamp, &options); - blog(LOG_INFO, "Portal hotkey activated: %s\n", shortcut_name); + g_variant_get(parameters, "(ost@a{sv})", &session_path, &received_id, ×tamp, &options); + blog(LOG_INFO, "Portal hotkey activated: %s\n", received_id); - // find shortcut id in registered list - for (unsigned i = 0; i < state->registered_hotkeys->len; ++i) { - obs_hotkey_t *hotkey = &g_array_index(state->registered_hotkeys, obs_hotkey_t, i); - - if (strcmp(hotkey->name, shortcut_name) != 0) { + char *iter_id = NULL; + for (obs_hotkey_t *hotkey = obs->hotkeys.hotkeys; hotkey != NULL; hotkey = hotkey->hh.next) { + iter_id = get_hotkey_portal_id(hotkey); + if (strcmp(iter_id, received_id) != 0) { + bfree(iter_id); continue; } + bfree(iter_id); hotkey->func(hotkey->data, hotkey->id, hotkey, true); return; } @@ -315,11 +346,10 @@ void obs_hotkey_portal_init() } // initialise state - state = malloc(sizeof(obs_hotkey_portal_state_t)); + state = bzalloc(sizeof(obs_hotkey_portal_state_t)); state->conn = conn; state->session_path = NULL; state->pending_hotkeys = g_queue_new(); - state->registered_hotkeys = g_array_new(false, false, sizeof(obs_hotkey_t)); } void obs_hotkey_portal_register(obs_hotkey_t *hotkey) @@ -346,19 +376,9 @@ void obs_hotkey_portal_unregister(obs_hotkey_id id) { blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); - // remove hotkey from registered list - for (unsigned i = 0; i < state->registered_hotkeys->len; ++i) { - obs_hotkey_t *hotkey = &g_array_index(state->registered_hotkeys, obs_hotkey_t, i); - if (hotkey->id == id) { - g_array_remove_index(state->registered_hotkeys, i); - break; - } - } - - // pending list + // remove from pending list (if it's there) for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { - obs_hotkey_t *hotkey = (obs_hotkey_t *)l->data; - if (hotkey->id == id) { + if (*(obs_hotkey_id*)l->data == id) { g_queue_delete_link(state->pending_hotkeys, l); break; } From d0f45d29f204a81df0f29fda6decd5f2b6ce41eb Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Sat, 18 Jul 2026 22:54:53 +0100 Subject: [PATCH 03/10] Properly clean up obs-hotkey-portal --- libobs/obs-hotkey-portal.c | 8 +++----- libobs/obs-hotkey-portal.h | 1 - libobs/obs-hotkey.c | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index a80d0c8154ba24..2a67c3c1901b3f 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -93,7 +93,6 @@ char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { switch(obs_hotkey_get_registerer_type(hotkey)) { case OBS_HOTKEY_REGISTERER_SOURCE: append_to_description = obs_source_get_name(registerer); - blog(LOG_INFO, "Desc: %s", append_to_description); break; default: break; @@ -200,7 +199,6 @@ static void activated_cb( g_autoptr(GVariant) options = NULL; g_variant_get(parameters, "(ost@a{sv})", &session_path, &received_id, ×tamp, &options); - blog(LOG_INFO, "Portal hotkey activated: %s\n", received_id); char *iter_id = NULL; for (obs_hotkey_t *hotkey = obs->hotkeys.hotkeys; hotkey != NULL; hotkey = hotkey->hh.next) { @@ -290,7 +288,7 @@ static void create_session_cb( g_timeout_add(500, (GSourceFunc)obs_hotkey_portal_finish_registration, NULL); } -void obs_hotkey_portal_init() +static void obs_hotkey_portal_init() { // only allow this function to run once static bool init_run = false; @@ -354,8 +352,8 @@ void obs_hotkey_portal_init() void obs_hotkey_portal_register(obs_hotkey_t *hotkey) { - // TODO: check if there is a potential race condition here - // TODO: check if this is even reachable in the first place + // init if haven't already + obs_hotkey_portal_init(); if (state == NULL) { blog(LOG_WARNING, "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); return; diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h index a30da5432fa8e3..7de354f9382ba6 100644 --- a/libobs/obs-hotkey-portal.h +++ b/libobs/obs-hotkey-portal.h @@ -24,7 +24,6 @@ // must be called before registering any hotkeys with the portal. // does nothing if it has already been called -void obs_hotkey_portal_init(); void obs_hotkey_portal_free(); // returns true if the portal session has been started diff --git a/libobs/obs-hotkey.c b/libobs/obs-hotkey.c index 33317f84d553b9..554621af03cbef 100644 --- a/libobs/obs-hotkey.c +++ b/libobs/obs-hotkey.c @@ -183,7 +183,6 @@ static inline obs_hotkey_id obs_hotkey_register_internal(obs_hotkey_registerer_t } // add with xdg-desktop-portal - obs_hotkey_portal_init(hotkey); obs_hotkey_portal_register(hotkey); hotkey_signal("hotkey_register", hotkey); @@ -972,6 +971,8 @@ void obs_hotkeys_free(void) obs->hotkeys.translations[i] = NULL; } } + + obs_hotkey_portal_free(); } void obs_enum_hotkeys(obs_hotkey_enum_func func, void *data) From e0f1bc7fdbb6fc2dfbda0e0a785fbd57d6863e34 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 16:52:16 +0100 Subject: [PATCH 04/10] Use hotkey callback function when shortcut is deactivated, and add hotkey pairs. --- libobs/obs-hotkey-portal.c | 165 +++++++++++++++++++++++-------------- libobs/obs-hotkey-portal.h | 4 + 2 files changed, 109 insertions(+), 60 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index 2a67c3c1901b3f..2cc645c6622b98 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -6,6 +6,7 @@ #include "util/c99defs.h" #include #include +#include #include #define PORTAL_NAME "org.freedesktop.portal.Desktop" @@ -14,12 +15,17 @@ #define REQUEST_IFACE "org.freedesktop.portal.Request" #define SESSION_IFACE "org.freedesktop.portal.Session" +#undef HASH_FUNCTION +#define HASH_FUNCTION(s, len, hashv) (hashv) = *s % UINT_MAX +#define HASH_FIND_HKEY(head, id, out) HASH_FIND(hh, head, &(id), sizeof(size_t), out) + static struct obs_hotkey_portal_state { GDBusConnection *conn; // GVariant *session_path; char *session_path; // tracks the timestamp when pending_hotkeys was last added to long add_to_pending_hotkeys_timestamp_us; + // stores list of ids that need to be registered with the portal GQueue *pending_hotkeys; // TODO: use the hash table that obs-hotkey already uses instead of // this array. I just want to get this working first @@ -112,9 +118,8 @@ char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { } void obs_hotkey_portal_free() { - if (state == NULL) { + if (state == NULL) return; - } g_dbus_connection_close_sync(state->conn, NULL, NULL); g_object_unref(state->conn); bfree(state->session_path); @@ -123,6 +128,16 @@ void obs_hotkey_portal_free() { state = NULL; } +void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *description) { + GVariantBuilder vardict_builder; + g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(description)); + + // add shortcut to builder + GVariant *data[] = {g_variant_new_string(id), g_variant_builder_end(&vardict_builder)}; + g_variant_builder_add_value(shortcuts_builder, g_variant_new_tuple(data, 2)); +} + // register all pending hotkeys static gboolean obs_hotkey_portal_finish_registration() { assert(obs_hotkey_portal_session_active()); @@ -141,22 +156,66 @@ static gboolean obs_hotkey_portal_finish_registration() { // bind some shortcuts GVariantBuilder shortcuts_builder; g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a(sa{sv})")); - for (obs_hotkey_t *hotkey = g_queue_pop_head(state->pending_hotkeys); - hotkey != NULL; - hotkey = g_queue_pop_head(state->pending_hotkeys)) + + // when adding a hotkey h1 that has a pair h2, then a hotkey for + // toggling between h1/h2 is added, then h1, then h2's id is recorded + // in this hash table so this process doesn't repeat for that one too. + g_autoptr(GHashTable) added_ids = g_hash_table_new(g_int_hash, g_int_equal); + + for (obs_hotkey_id *queued_id = g_queue_pop_head(state->pending_hotkeys); + queued_id != NULL; + queued_id = g_queue_pop_head(state->pending_hotkeys)) { - char *id = get_hotkey_portal_id(hotkey); + // if this hotkey has already been added, skip + if (g_hash_table_contains(added_ids, queued_id)) + continue; + + obs_hotkey_t *hotkey; + HASH_FIND(hh, obs->hotkeys.hotkeys, queued_id, sizeof(size_t), hotkey); + blog(LOG_INFO, "Registering hotkey id with portal: %lu\n", *queued_id); + + if (!hotkey) + continue; - // description + char *id = get_hotkey_portal_id(hotkey); char *description = get_hotkey_portal_description(hotkey); - GVariantBuilder vardict_builder; - g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); - g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(description)); - blog(LOG_INFO, "Registering hotkey with portal: %s: %s\n", hotkey->name, hotkey->description); - // add shortcut to builder - GVariant *data[] = {g_variant_new_string(id), g_variant_builder_end(&vardict_builder)}; - g_variant_builder_add_value(&shortcuts_builder, g_variant_new_tuple(data, 2)); + blog(LOG_INFO, "Registering hotkey with portal: %s: %s\n", + hotkey->name, hotkey->description); + + add_hotkey_to_builder(&shortcuts_builder, id, description); + g_hash_table_add(added_ids, &hotkey->id); + + // // handle pair + // if (hotkey->pair_partner_id != OBS_INVALID_HOTKEY_PAIR_ID) { + // obs_hotkey_t *hotkey_pair; + // HASH_FIND_HKEY(obs->hotkeys.hotkeys, hotkey->pair_partner_id, hotkey_pair); + // + // if (hotkey_pair) { + // char *id_pair = get_hotkey_portal_id(hotkey_pair); + // size_t joint_id_len = strlen(id_pair) + strlen(id) + 2; + // char *joint_id = bzalloc(joint_id_len); + // snprintf(joint_id, joint_id_len, "%s.%s", id, id_pair); + // + // char *desc_pair = get_hotkey_portal_description(hotkey_pair); + // size_t joint_desc_len = strlen(desc_pair) + strlen(description) + 2; + // char *joint_desc = bzalloc(joint_desc_len); + // snprintf(joint_desc, joint_desc_len, "%s/%s", description, desc_pair); + // + // // add pair + // add_hotkey_to_builder(&shortcuts_builder, id_pair, desc_pair); + // g_hash_table_add(added_ids, &hotkey_pair->id); + // + // // ...and then add joint hotkey + // add_hotkey_to_builder(&shortcuts_builder, joint_id, joint_desc); + // + // bfree(id_pair); + // bfree(joint_id); + // bfree(desc_pair); + // bfree(joint_desc); + // } + // + // } bfree(id); bfree(description); @@ -176,30 +235,7 @@ static gboolean obs_hotkey_portal_finish_registration() { return G_SOURCE_CONTINUE; } -static void activated_cb( - GDBusConnection* conn, - const char *sender_name, - const char *object_path, - const char *interface_name, - const char *signal_name, - GVariant *parameters, - gpointer user_data -) { - UNUSED_PARAMETER(conn); - UNUSED_PARAMETER(sender_name); - UNUSED_PARAMETER(object_path); - UNUSED_PARAMETER(interface_name); - UNUSED_PARAMETER(signal_name); - UNUSED_PARAMETER(parameters); - UNUSED_PARAMETER(user_data); - - g_autofree char *session_path = NULL; - g_autofree char *received_id = NULL; - uint64_t timestamp; - g_autoptr(GVariant) options = NULL; - - g_variant_get(parameters, "(ost@a{sv})", &session_path, &received_id, ×tamp, &options); - +static void call_hotkey_cb(const char *received_id, bool pressed) { char *iter_id = NULL; for (obs_hotkey_t *hotkey = obs->hotkeys.hotkeys; hotkey != NULL; hotkey = hotkey->hh.next) { iter_id = get_hotkey_portal_id(hotkey); @@ -208,21 +244,21 @@ static void activated_cb( continue; } + blog(LOG_INFO, "%s\n", iter_id); + + if (!obs->hotkeys.reroute_hotkeys) + hotkey->func(hotkey->data, hotkey->id, hotkey, pressed); + else if (obs->hotkeys.router_func) + obs->hotkeys.router_func(obs->hotkeys.router_func_data, hotkey->id, pressed); + bfree(iter_id); - hotkey->func(hotkey->data, hotkey->id, hotkey, true); return; } + } -static void deactivated_cb( - GDBusConnection* conn, - const char *sender_name, - const char *object_path, - const char *interface_name, - const char *signal_name, - GVariant *parameters, - gpointer user_data -) { +static void activated_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +{ UNUSED_PARAMETER(conn); UNUSED_PARAMETER(sender_name); UNUSED_PARAMETER(object_path); @@ -230,19 +266,27 @@ static void deactivated_cb( UNUSED_PARAMETER(signal_name); UNUSED_PARAMETER(parameters); UNUSED_PARAMETER(user_data); + g_autofree char *received_id = NULL; + g_variant_get(parameters, "(ost@a{sv})", NULL, &received_id, NULL, NULL); + call_hotkey_cb(received_id, true); +} - g_print("Button released!\n"); +static void deactivated_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +{ + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + g_autofree char *received_id = NULL; + g_variant_get(parameters, "(ost@a{sv})", NULL, &received_id, NULL, NULL); + call_hotkey_cb(received_id, false); } -static void shortcuts_changed_cb( - GDBusConnection* conn, - const char *sender_name, - const char *object_path, - const char *interface_name, - const char *signal_name, - GVariant *parameters, - gpointer user_data -) { +static void shortcuts_changed_cb( GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +{ UNUSED_PARAMETER(conn); UNUSED_PARAMETER(sender_name); UNUSED_PARAMETER(object_path); @@ -362,11 +406,12 @@ void obs_hotkey_portal_register(obs_hotkey_t *hotkey) blog(LOG_WARNING, "Registering hotkey with id %lu to xdg-desktop-portal", hotkey->id); // add to pending list - g_queue_push_tail(state->pending_hotkeys, hotkey); + g_queue_push_tail(state->pending_hotkeys, &hotkey->id); state->add_to_pending_hotkeys_timestamp_us = g_get_monotonic_time(); } -void obs_hotkey_portal_unregister(obs_hotkey_id id) { +void obs_hotkey_portal_unregister(obs_hotkey_id id) +{ if (state == NULL) { blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); return; diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h index 7de354f9382ba6..0e67d8c587f93f 100644 --- a/libobs/obs-hotkey-portal.h +++ b/libobs/obs-hotkey-portal.h @@ -34,6 +34,10 @@ bool obs_hotkey_portal_session_active(); // the current platform) void obs_hotkey_portal_register(obs_hotkey_t *hotkey); +// // specifies hotkeys which should be a considered a pair +// // must only be called after these two hotkeys have been registered +// void obs_hotkey_portal_register_pair(obs_hotkey_id first, obs_hotkey_id second); + // stops listening to a hotkey // should always be called, even if it has no effect (i.e. if unsupported on // the current platform) From 77740eb727423fad4072d264aee3f077e8e81fc2 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 19:10:53 +0100 Subject: [PATCH 05/10] Make hotkey pairs work. --- libobs/obs-hotkey-portal.c | 278 ++++++++++++++++++++++++------------- libobs/obs-hotkey-portal.h | 1 + libobs/obs-hotkey.c | 2 + 3 files changed, 186 insertions(+), 95 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index 2cc645c6622b98..67fea7364c638c 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #define PORTAL_NAME "org.freedesktop.portal.Desktop" @@ -25,15 +26,51 @@ static struct obs_hotkey_portal_state { char *session_path; // tracks the timestamp when pending_hotkeys was last added to long add_to_pending_hotkeys_timestamp_us; - // stores list of ids that need to be registered with the portal + // stores list of hotkeys that need to be registered with the portal GQueue *pending_hotkeys; // TODO: use the hash table that obs-hotkey already uses instead of // this array. I just want to get this working first // GArray *registered_hotkeys; + + // maps portal id -> hotkey id + GHashTable *registered_hotkeys; } *state = NULL; typedef struct obs_hotkey_portal_state obs_hotkey_portal_state_t; +enum obs_hotkey_portal_registered_type { + SINGLE, + PAIR, +}; +typedef enum obs_hotkey_portal_registered_type obs_hotkey_portal_registered_type_t; + +struct obs_hotkey_portal_registered_hotkey { + obs_hotkey_portal_registered_type_t type; + size_t id; +}; +typedef struct obs_hotkey_portal_registered_hotkey obs_hotkey_portal_registered_hotkey_t; + +gboolean destroy_registered_hotkey(gpointer key, gpointer value, gpointer user_data) { + UNUSED_PARAMETER(user_data); + bfree(key); + bfree(value); + return TRUE; +} + +void obs_hotkey_portal_free() { + if (state == NULL) + return; + g_dbus_connection_close_sync(state->conn, NULL, NULL); + g_object_unref(state->conn); + bfree(state->session_path); + g_queue_free_full(state->pending_hotkeys, bfree); + g_hash_table_foreach_remove(state->registered_hotkeys, destroy_registered_hotkey, NULL); + g_hash_table_destroy(state->registered_hotkeys); + bfree(state); + state = NULL; +} + + bool obs_hotkey_portal_session_active() { return state != NULL && state->session_path != NULL; @@ -117,17 +154,6 @@ char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { return description_dst; } -void obs_hotkey_portal_free() { - if (state == NULL) - return; - g_dbus_connection_close_sync(state->conn, NULL, NULL); - g_object_unref(state->conn); - bfree(state->session_path); - g_queue_free(state->pending_hotkeys); - bfree(state); - state = NULL; -} - void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *description) { GVariantBuilder vardict_builder; g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); @@ -138,7 +164,7 @@ void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *d g_variant_builder_add_value(shortcuts_builder, g_variant_new_tuple(data, 2)); } -// register all pending hotkeys +// register all pending hotkeys, if any static gboolean obs_hotkey_portal_finish_registration() { assert(obs_hotkey_portal_session_active()); @@ -157,68 +183,74 @@ static gboolean obs_hotkey_portal_finish_registration() { GVariantBuilder shortcuts_builder; g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a(sa{sv})")); - // when adding a hotkey h1 that has a pair h2, then a hotkey for - // toggling between h1/h2 is added, then h1, then h2's id is recorded - // in this hash table so this process doesn't repeat for that one too. - g_autoptr(GHashTable) added_ids = g_hash_table_new(g_int_hash, g_int_equal); - - for (obs_hotkey_id *queued_id = g_queue_pop_head(state->pending_hotkeys); - queued_id != NULL; - queued_id = g_queue_pop_head(state->pending_hotkeys)) + for (obs_hotkey_portal_registered_hotkey_t *queued_hk = g_queue_pop_head(state->pending_hotkeys); + queued_hk != NULL; + queued_hk = g_queue_pop_head(state->pending_hotkeys)) { - // if this hotkey has already been added, skip - if (g_hash_table_contains(added_ids, queued_id)) - continue; - obs_hotkey_t *hotkey; - HASH_FIND(hh, obs->hotkeys.hotkeys, queued_id, sizeof(size_t), hotkey); - blog(LOG_INFO, "Registering hotkey id with portal: %lu\n", *queued_id); - - if (!hotkey) - continue; - - char *id = get_hotkey_portal_id(hotkey); - char *description = get_hotkey_portal_description(hotkey); - - blog(LOG_INFO, "Registering hotkey with portal: %s: %s\n", - hotkey->name, hotkey->description); - - add_hotkey_to_builder(&shortcuts_builder, id, description); - g_hash_table_add(added_ids, &hotkey->id); - - // // handle pair - // if (hotkey->pair_partner_id != OBS_INVALID_HOTKEY_PAIR_ID) { - // obs_hotkey_t *hotkey_pair; - // HASH_FIND_HKEY(obs->hotkeys.hotkeys, hotkey->pair_partner_id, hotkey_pair); - // - // if (hotkey_pair) { - // char *id_pair = get_hotkey_portal_id(hotkey_pair); - // size_t joint_id_len = strlen(id_pair) + strlen(id) + 2; - // char *joint_id = bzalloc(joint_id_len); - // snprintf(joint_id, joint_id_len, "%s.%s", id, id_pair); - // - // char *desc_pair = get_hotkey_portal_description(hotkey_pair); - // size_t joint_desc_len = strlen(desc_pair) + strlen(description) + 2; - // char *joint_desc = bzalloc(joint_desc_len); - // snprintf(joint_desc, joint_desc_len, "%s/%s", description, desc_pair); - // - // // add pair - // add_hotkey_to_builder(&shortcuts_builder, id_pair, desc_pair); - // g_hash_table_add(added_ids, &hotkey_pair->id); - // - // // ...and then add joint hotkey - // add_hotkey_to_builder(&shortcuts_builder, joint_id, joint_desc); - // - // bfree(id_pair); - // bfree(joint_id); - // bfree(desc_pair); - // bfree(joint_desc); - // } - // - // } - - bfree(id); - bfree(description); + if (queued_hk->type == SINGLE) { + obs_hotkey_t *hotkey; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, queued_hk->id, hotkey); + blog(LOG_INFO, "Registering hotkey id with portal: %lu", queued_hk->id); + + if (!hotkey) + continue; + + char *id = get_hotkey_portal_id(hotkey); + char *description = get_hotkey_portal_description(hotkey); + + blog(LOG_INFO, "Registering hotkey with portal: %s: %s", + hotkey->name, hotkey->description); + + add_hotkey_to_builder(&shortcuts_builder, id, description); + g_hash_table_insert(state->registered_hotkeys, id, queued_hk); + + // id is now owned by state->registered_hotkeys + bfree(description); + } else if (queued_hk->type == PAIR) { + blog(LOG_INFO, "Registering hotkey pair id with portal: %lu", queued_hk->id); + // get pair + obs_hotkey_pair_t *pair; + HASH_FIND_HKEY(obs->hotkeys.hotkey_pairs, queued_hk->id, pair); + + if (!pair) + continue; + + // get both hotkeys in that pair + obs_hotkey_t *hotkey1, *hotkey2; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, pair->id[0], hotkey1); + HASH_FIND_HKEY(obs->hotkeys.hotkeys, pair->id[1], hotkey2); + + if (!hotkey1 || !hotkey2) + continue; + + // get id + char *hotkey1_id = get_hotkey_portal_id(hotkey1); + char *hotkey2_id = get_hotkey_portal_id(hotkey2); + size_t joint_id_len = strlen(hotkey1_id) + strlen(hotkey2_id) + 2; + char *joint_id = bzalloc(joint_id_len); + snprintf(joint_id, joint_id_len, "%s.%s", hotkey1_id, hotkey2_id); + + // get description + char *hotkey1_desc = get_hotkey_portal_description(hotkey1); + char *hotkey2_desc = get_hotkey_portal_description(hotkey2); + size_t joint_desc_len = strlen(hotkey1_desc) + strlen(hotkey2_desc) + 2; + char *joint_desc = bzalloc(joint_desc_len); + snprintf(joint_desc, joint_desc_len, "%s/%s", hotkey1_desc, hotkey2_desc); + + // add hotkey + add_hotkey_to_builder(&shortcuts_builder, joint_id, joint_desc); + g_hash_table_insert(state->registered_hotkeys, joint_id, queued_hk); + + bfree(hotkey1_id); + bfree(hotkey1_desc); + bfree(hotkey2_id); + bfree(hotkey2_desc); + // joint_id is now owned by state->registered_hotkeys + bfree(joint_desc); + } else { + blog(LOG_ERROR, "Invalid hotkey type: %d", queued_hk->type); + } } // options @@ -237,14 +269,25 @@ static gboolean obs_hotkey_portal_finish_registration() { static void call_hotkey_cb(const char *received_id, bool pressed) { char *iter_id = NULL; - for (obs_hotkey_t *hotkey = obs->hotkeys.hotkeys; hotkey != NULL; hotkey = hotkey->hh.next) { - iter_id = get_hotkey_portal_id(hotkey); - if (strcmp(iter_id, received_id) != 0) { - bfree(iter_id); - continue; - } - blog(LOG_INFO, "%s\n", iter_id); + obs_hotkey_portal_registered_hotkey_t *received_hk; + received_hk = g_hash_table_lookup(state->registered_hotkeys, received_id); + + if (!received_hk) { + blog(LOG_ERROR, "Failed to find activated hotkey for portal"); + return; + } + + if (received_hk->type == SINGLE) { + obs_hotkey_t *hotkey; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, received_hk->id, hotkey); + + if (!hotkey) + return; + if (pressed == hotkey->pressed) + return; + + hotkey->pressed = pressed; if (!obs->hotkeys.reroute_hotkeys) hotkey->func(hotkey->data, hotkey->id, hotkey, pressed); @@ -252,9 +295,25 @@ static void call_hotkey_cb(const char *received_id, bool pressed) { obs->hotkeys.router_func(obs->hotkeys.router_func_data, hotkey->id, pressed); bfree(iter_id); - return; - } + } else if (received_hk->type == PAIR) { + obs_hotkey_pair_t *pair; + HASH_FIND_HKEY(obs->hotkeys.hotkey_pairs, received_hk->id, pair); + + if (!pair) + return; + + for (size_t i = 0; i < 2; ++i) { + obs_hotkey_t *hk; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, pair->id[i], hk); + if (!obs->hotkeys.reroute_hotkeys) + pair->func[i](hk->data, hk->id, hk, pressed); + else if (obs->hotkeys.router_func) + obs->hotkeys.router_func(obs->hotkeys.router_func_data, hk->id, pressed); + } + } else { + blog(LOG_ERROR, "Invalid hotkey type: %d", received_hk->type); + } } static void activated_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) @@ -392,6 +451,7 @@ static void obs_hotkey_portal_init() state->conn = conn; state->session_path = NULL; state->pending_hotkeys = g_queue_new(); + state->registered_hotkeys = g_hash_table_new(g_str_hash, g_str_equal); } void obs_hotkey_portal_register(obs_hotkey_t *hotkey) @@ -403,27 +463,55 @@ void obs_hotkey_portal_register(obs_hotkey_t *hotkey) return; } - blog(LOG_WARNING, "Registering hotkey with id %lu to xdg-desktop-portal", hotkey->id); - // add to pending list - g_queue_push_tail(state->pending_hotkeys, &hotkey->id); + obs_hotkey_portal_registered_hotkey_t *hk; + hk = bzalloc(sizeof(obs_hotkey_portal_registered_hotkey_t)); + hk->type = SINGLE; + hk->id = hotkey->id; + + blog(LOG_WARNING, "Registering hotkey with id %lu to xdg-desktop-portal", hk->id); + + g_queue_push_tail(state->pending_hotkeys, hk); state->add_to_pending_hotkeys_timestamp_us = g_get_monotonic_time(); } -void obs_hotkey_portal_unregister(obs_hotkey_id id) +void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair) { + // init if haven't already + obs_hotkey_portal_init(); if (state == NULL) { - blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + blog(LOG_WARNING, "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); return; } - blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); + blog(LOG_WARNING, "Registering hotkey pair with id %lu to xdg-desktop-portal", pair->pair_id); - // remove from pending list (if it's there) - for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { - if (*(obs_hotkey_id*)l->data == id) { - g_queue_delete_link(state->pending_hotkeys, l); - break; - } - } + // add to pending list + obs_hotkey_portal_registered_hotkey_t *hk; + hk = bzalloc(sizeof(obs_hotkey_portal_registered_hotkey_t)); + hk->type = PAIR; + hk->id = pair->pair_id; + + g_queue_push_tail(state->pending_hotkeys, hk); + state->add_to_pending_hotkeys_timestamp_us = g_get_monotonic_time(); +} + +void obs_hotkey_portal_unregister(obs_hotkey_id id) +{ + UNUSED_PARAMETER(id); + // TODO + // if (state == NULL) { + // blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + // return; + // } + // + // blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); + // + // // remove from pending list (if it's there) + // for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { + // if (*(obs_hotkey_id*)l->data == id) { + // g_queue_delete_link(state->pending_hotkeys, l); + // break; + // } + // } } diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h index 0e67d8c587f93f..ac7972b066f01a 100644 --- a/libobs/obs-hotkey-portal.h +++ b/libobs/obs-hotkey-portal.h @@ -33,6 +33,7 @@ bool obs_hotkey_portal_session_active(); // should always be called, even if it has no effect (i.e. if unsupported on // the current platform) void obs_hotkey_portal_register(obs_hotkey_t *hotkey); +void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair); // // specifies hotkeys which should be a considered a pair // // must only be called after these two hotkeys have been registered diff --git a/libobs/obs-hotkey.c b/libobs/obs-hotkey.c index 554621af03cbef..dfce58ba3247df 100644 --- a/libobs/obs-hotkey.c +++ b/libobs/obs-hotkey.c @@ -340,6 +340,8 @@ static obs_hotkey_pair_id register_hotkey_pair_internal(obs_hotkey_registerer_t obs_hotkey_pair_id id = pair->pair_id; + obs_hotkey_portal_register_pair(pair); + unlock(); return id; } From 44e1f2a8e3e05c2624ffb92fe073d913aef68465 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 19:51:43 +0100 Subject: [PATCH 06/10] Allow adding more hotkeys after the initial bunch. --- libobs/obs-hotkey-portal.c | 136 +++++++++++++++++++++++++------------ libobs/obs-hotkey-portal.h | 2 +- 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index 67fea7364c638c..75c9e609e60108 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -22,16 +22,12 @@ static struct obs_hotkey_portal_state { GDBusConnection *conn; - // GVariant *session_path; char *session_path; + bool bind_shortcuts_called_for_session; // tracks the timestamp when pending_hotkeys was last added to long add_to_pending_hotkeys_timestamp_us; // stores list of hotkeys that need to be registered with the portal GQueue *pending_hotkeys; - // TODO: use the hash table that obs-hotkey already uses instead of - // this array. I just want to get this working first - // GArray *registered_hotkeys; - // maps portal id -> hotkey id GHashTable *registered_hotkeys; } *state = NULL; @@ -50,6 +46,9 @@ struct obs_hotkey_portal_registered_hotkey { }; typedef struct obs_hotkey_portal_registered_hotkey obs_hotkey_portal_registered_hotkey_t; +static void close_session(); +static void create_session(); + gboolean destroy_registered_hotkey(gpointer key, gpointer value, gpointer user_data) { UNUSED_PARAMETER(user_data); bfree(key); @@ -71,11 +70,6 @@ void obs_hotkey_portal_free() { } -bool obs_hotkey_portal_session_active() -{ - return state != NULL && state->session_path != NULL; -} - GString *get_formatted_sender(GDBusConnection *conn) { g_autoptr(GString) temp = NULL; GString *unique_name = NULL; @@ -166,7 +160,9 @@ void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *d // register all pending hotkeys, if any static gboolean obs_hotkey_portal_finish_registration() { - assert(obs_hotkey_portal_session_active()); + if (!state || !state->session_path) { + return G_SOURCE_CONTINUE; + } // check if at least 0.5 seconds have passed since the last time we added to pending_hotkeys long current_timestamp = g_get_monotonic_time(); @@ -179,6 +175,31 @@ static gboolean obs_hotkey_portal_finish_registration() { return G_SOURCE_CONTINUE; } + // if BindShortcuts has already been called + if (state->bind_shortcuts_called_for_session) { + // destroy session + close_session(); + + // re-queue all hotkeys + GHashTableIter iter; + gpointer portal_id, dst; + g_hash_table_iter_init(&iter, state->registered_hotkeys); + while (g_hash_table_iter_next(&iter, &portal_id, &dst)) { + obs_hotkey_portal_registered_hotkey_t *hk = dst; + g_queue_push_head(state->pending_hotkeys, hk); + bfree(portal_id); + } + + g_hash_table_remove_all(state->registered_hotkeys); + + // open session back up + create_session(); + return G_SOURCE_CONTINUE; + } + + blog(LOG_WARNING, "Here"); + state->bind_shortcuts_called_for_session = true; + // bind some shortcuts GVariantBuilder shortcuts_builder; g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a(sa{sv})")); @@ -344,7 +365,7 @@ static void deactivated_cb(GDBusConnection* conn, const char *sender_name, const call_hotkey_cb(received_id, false); } -static void shortcuts_changed_cb( GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +static void shortcuts_changed_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) { UNUSED_PARAMETER(conn); UNUSED_PARAMETER(sender_name); @@ -358,15 +379,12 @@ static void shortcuts_changed_cb( GDBusConnection* conn, const char *sender_name g_print("Shortcuts changed!\n"); } -static void create_session_cb( - GDBusConnection* conn, - const char *sender_name, - const char *object_path, - const char *interface_name, - const char *signal_name, - GVariant *parameters, - gpointer user_data -) { +static void create_session_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +{ + if (!state || state->session_path) { + return; + } + UNUSED_PARAMETER(sender_name); UNUSED_PARAMETER(object_path); UNUSED_PARAMETER(interface_name); @@ -385,39 +403,44 @@ static void create_session_cb( g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Deactivated", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, deactivated_cb, NULL, NULL); g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "ShortcutsChanged", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, shortcuts_changed_cb, NULL, NULL); + blog(LOG_WARNING, "HOASJOID"); state->session_path = bstrdup(session_handle); - - // schedule a timer to register all pending hotkeys every 0.1 seconds - g_timeout_add(500, (GSourceFunc)obs_hotkey_portal_finish_registration, NULL); } -static void obs_hotkey_portal_init() -{ - // only allow this function to run once - static bool init_run = false; - if (init_run) { +static void close_session() { + if (!state || !state->session_path || !state->conn) { return; } - init_run = true; - g_autoptr(GDBusConnection) conn = NULL; GError *error = NULL; - GVariant *options = NULL; - g_autoptr(GString) request_handle_token = NULL; + g_dbus_connection_call_sync(state->conn, PORTAL_NAME, state->session_path, SESSION_IFACE, "Close", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + + if (error != NULL) { + blog(LOG_ERROR, "Failed to destroy global shortcuts portal session: %s", error->message); + return; + } + bfree(state->session_path); + state->bind_shortcuts_called_for_session = false; + state->session_path = NULL; +} + +static void create_session() { + if (!state || state->session_path || !state->conn) { + return; + } + + state->bind_shortcuts_called_for_session = false; + g_autoptr(GString) session_token = NULL; + g_autoptr(GString) request_handle_token = NULL; + GVariant *options = NULL; g_autoptr(GString) request_path = NULL; - g_autoptr(GVariant) result = NULL; g_autoptr(GString) sender = NULL; GVariantBuilder options_builder; + g_autoptr(GVariant) result = NULL; + GError *error = NULL; - // get connection - conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); - if (!conn) { - g_printerr("Failed to connect to dbus: %s\n", error->message); - return; - } - - sender = get_formatted_sender(conn); + sender = get_formatted_sender(state->conn); // get tokens session_token = g_string_new(NULL); @@ -436,15 +459,35 @@ static void obs_hotkey_portal_init() request_path = g_string_new(NULL); g_string_printf(request_path, "/org/freedesktop/portal/desktop/request/%s/%s", sender->str, request_handle_token->str); g_print("Listening to request at path: %s\n", request_path->str); - g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path->str, NULL, G_DBUS_SIGNAL_FLAGS_NONE, create_session_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(state->conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path->str, NULL, G_DBUS_SIGNAL_FLAGS_NONE, create_session_cb, NULL, NULL); // finally, connect to session - result = g_dbus_connection_call_sync(conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "CreateSession", g_variant_new_tuple(&options, 1), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + result = g_dbus_connection_call_sync(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "CreateSession", g_variant_new_tuple(&options, 1), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (result == NULL) { blog(LOG_WARNING, "Failed to create global shortcuts portal session: %s", error->message); return; } +} + +static void obs_hotkey_portal_init() +{ + // only allow this function to run once + static bool init_run = false; + if (init_run) { + return; + } + init_run = true; + + g_autoptr(GDBusConnection) conn = NULL; + GError *error = NULL; + + // get connection + conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + if (!conn) { + g_printerr("Failed to connect to dbus: %s\n", error->message); + return; + } // initialise state state = bzalloc(sizeof(obs_hotkey_portal_state_t)); @@ -452,6 +495,11 @@ static void obs_hotkey_portal_init() state->session_path = NULL; state->pending_hotkeys = g_queue_new(); state->registered_hotkeys = g_hash_table_new(g_str_hash, g_str_equal); + + create_session(); + + // schedule a timer to register all pending hotkeys every 0.1 seconds + g_timeout_add(500, (GSourceFunc)obs_hotkey_portal_finish_registration, NULL); } void obs_hotkey_portal_register(obs_hotkey_t *hotkey) diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h index ac7972b066f01a..731bb2776e5811 100644 --- a/libobs/obs-hotkey-portal.h +++ b/libobs/obs-hotkey-portal.h @@ -39,7 +39,7 @@ void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair); // // must only be called after these two hotkeys have been registered // void obs_hotkey_portal_register_pair(obs_hotkey_id first, obs_hotkey_id second); -// stops listening to a hotkey +// unqueues hotkey to be registered // should always be called, even if it has no effect (i.e. if unsupported on // the current platform) void obs_hotkey_portal_unregister(obs_hotkey_id id); From 0ce75c33177c2a8be0edb3331ac63b58ff7f039a Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 22:34:12 +0100 Subject: [PATCH 07/10] Run clang-format --- libobs/obs-hotkey-portal.c | 118 +++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index 75c9e609e60108..850b28a59d956f 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -49,14 +49,16 @@ typedef struct obs_hotkey_portal_registered_hotkey obs_hotkey_portal_registered_ static void close_session(); static void create_session(); -gboolean destroy_registered_hotkey(gpointer key, gpointer value, gpointer user_data) { +gboolean destroy_registered_hotkey(gpointer key, gpointer value, gpointer user_data) +{ UNUSED_PARAMETER(user_data); bfree(key); bfree(value); return TRUE; } -void obs_hotkey_portal_free() { +void obs_hotkey_portal_free() +{ if (state == NULL) return; g_dbus_connection_close_sync(state->conn, NULL, NULL); @@ -69,24 +71,25 @@ void obs_hotkey_portal_free() { state = NULL; } +GString *get_formatted_sender(GDBusConnection *conn) +{ + g_autoptr(GString) temp = NULL; + GString *unique_name = NULL; + temp = g_string_new(g_dbus_connection_get_unique_name(conn)); -GString *get_formatted_sender(GDBusConnection *conn) { - g_autoptr(GString) temp = NULL; - GString *unique_name = NULL; - temp = g_string_new(g_dbus_connection_get_unique_name(conn)); - - g_string_replace(temp, ".", "_", 0); + g_string_replace(temp, ".", "_", 0); - if (temp->len < 1) { - return NULL; - } + if (temp->len < 1) { + return NULL; + } - unique_name = g_string_new(NULL); - g_string_printf(unique_name, "%s", &temp->str[1]); - return unique_name; + unique_name = g_string_new(NULL); + g_string_printf(unique_name, "%s", &temp->str[1]); + return unique_name; } -char *get_hotkey_portal_id(obs_hotkey_t *hotkey) { +char *get_hotkey_portal_id(obs_hotkey_t *hotkey) +{ if (hotkey == NULL) { return NULL; } @@ -94,7 +97,7 @@ char *get_hotkey_portal_id(obs_hotkey_t *hotkey) { const char *append_to_id = NULL; void *registerer = obs_weak_object_get_object(hotkey->registerer); if (registerer) { - switch(obs_hotkey_get_registerer_type(hotkey)) { + switch (obs_hotkey_get_registerer_type(hotkey)) { case OBS_HOTKEY_REGISTERER_SOURCE: append_to_id = obs_source_get_uuid(registerer); obs_object_release(registerer); @@ -119,7 +122,8 @@ char *get_hotkey_portal_id(obs_hotkey_t *hotkey) { return id_dst; } -char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { +char *get_hotkey_portal_description(obs_hotkey_t *hotkey) +{ if (hotkey == NULL) { return NULL; } @@ -127,7 +131,7 @@ char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { const char *append_to_description = NULL; void *registerer = obs_weak_object_get_object(hotkey->registerer); if (registerer) { - switch(obs_hotkey_get_registerer_type(hotkey)) { + switch (obs_hotkey_get_registerer_type(hotkey)) { case OBS_HOTKEY_REGISTERER_SOURCE: append_to_description = obs_source_get_name(registerer); break; @@ -148,7 +152,8 @@ char *get_hotkey_portal_description(obs_hotkey_t *hotkey) { return description_dst; } -void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *description) { +void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *description) +{ GVariantBuilder vardict_builder; g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(description)); @@ -159,7 +164,8 @@ void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *d } // register all pending hotkeys, if any -static gboolean obs_hotkey_portal_finish_registration() { +static gboolean obs_hotkey_portal_finish_registration() +{ if (!state || !state->session_path) { return G_SOURCE_CONTINUE; } @@ -205,9 +211,7 @@ static gboolean obs_hotkey_portal_finish_registration() { g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a(sa{sv})")); for (obs_hotkey_portal_registered_hotkey_t *queued_hk = g_queue_pop_head(state->pending_hotkeys); - queued_hk != NULL; - queued_hk = g_queue_pop_head(state->pending_hotkeys)) - { + queued_hk != NULL; queued_hk = g_queue_pop_head(state->pending_hotkeys)) { if (queued_hk->type == SINGLE) { obs_hotkey_t *hotkey; @@ -220,8 +224,7 @@ static gboolean obs_hotkey_portal_finish_registration() { char *id = get_hotkey_portal_id(hotkey); char *description = get_hotkey_portal_description(hotkey); - blog(LOG_INFO, "Registering hotkey with portal: %s: %s", - hotkey->name, hotkey->description); + blog(LOG_INFO, "Registering hotkey with portal: %s: %s", hotkey->name, hotkey->description); add_hotkey_to_builder(&shortcuts_builder, id, description); g_hash_table_insert(state->registered_hotkeys, id, queued_hk); @@ -282,13 +285,17 @@ static gboolean obs_hotkey_portal_finish_registration() { g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&options_builder, "{sv}", "handle_token", g_variant_new_string(request_handle_token)); - GVariant *options[] = {g_variant_new_object_path(state->session_path), g_variant_builder_end(&shortcuts_builder), g_variant_new_string(""), g_variant_builder_end(&options_builder)}; - g_dbus_connection_call(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "BindShortcuts", g_variant_new_tuple(options, 4), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + GVariant *options[] = {g_variant_new_object_path(state->session_path), + g_variant_builder_end(&shortcuts_builder), g_variant_new_string(""), + g_variant_builder_end(&options_builder)}; + g_dbus_connection_call(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "BindShortcuts", + g_variant_new_tuple(options, 4), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); return G_SOURCE_CONTINUE; } -static void call_hotkey_cb(const char *received_id, bool pressed) { +static void call_hotkey_cb(const char *received_id, bool pressed) +{ char *iter_id = NULL; obs_hotkey_portal_registered_hotkey_t *received_hk; @@ -337,7 +344,8 @@ static void call_hotkey_cb(const char *received_id, bool pressed) { } } -static void activated_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +static void activated_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) { UNUSED_PARAMETER(conn); UNUSED_PARAMETER(sender_name); @@ -351,7 +359,9 @@ static void activated_cb(GDBusConnection* conn, const char *sender_name, const c call_hotkey_cb(received_id, true); } -static void deactivated_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +static void deactivated_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, + gpointer user_data) { UNUSED_PARAMETER(conn); UNUSED_PARAMETER(sender_name); @@ -365,7 +375,9 @@ static void deactivated_cb(GDBusConnection* conn, const char *sender_name, const call_hotkey_cb(received_id, false); } -static void shortcuts_changed_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +static void shortcuts_changed_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, + gpointer user_data) { UNUSED_PARAMETER(conn); UNUSED_PARAMETER(sender_name); @@ -379,7 +391,9 @@ static void shortcuts_changed_cb(GDBusConnection* conn, const char *sender_name, g_print("Shortcuts changed!\n"); } -static void create_session_cb(GDBusConnection* conn, const char *sender_name, const char *object_path, const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +static void create_session_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, + gpointer user_data) { if (!state || state->session_path) { return; @@ -399,21 +413,26 @@ static void create_session_cb(GDBusConnection* conn, const char *sender_name, co g_variant_lookup(options, "session_handle", "&s", &session_handle); // start listening to shortcuts - g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Activated", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, activated_cb, NULL, NULL); - g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Deactivated", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, deactivated_cb, NULL, NULL); - g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "ShortcutsChanged", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, shortcuts_changed_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Activated", PORTAL_PATH, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, activated_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Deactivated", PORTAL_PATH, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, deactivated_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "ShortcutsChanged", PORTAL_PATH, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, shortcuts_changed_cb, NULL, NULL); blog(LOG_WARNING, "HOASJOID"); state->session_path = bstrdup(session_handle); } -static void close_session() { +static void close_session() +{ if (!state || !state->session_path || !state->conn) { return; } GError *error = NULL; - g_dbus_connection_call_sync(state->conn, PORTAL_NAME, state->session_path, SESSION_IFACE, "Close", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + g_dbus_connection_call_sync(state->conn, PORTAL_NAME, state->session_path, SESSION_IFACE, "Close", NULL, NULL, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); if (error != NULL) { blog(LOG_ERROR, "Failed to destroy global shortcuts portal session: %s", error->message); @@ -424,7 +443,8 @@ static void close_session() { state->session_path = NULL; } -static void create_session() { +static void create_session() +{ if (!state || state->session_path || !state->conn) { return; } @@ -451,18 +471,24 @@ static void create_session() { // connect to session parameters g_variant_builder_init(&options_builder, G_VARIANT_TYPE("a{sv}")); - g_variant_builder_add(&options_builder, "{sv}", "handle_token", g_variant_new_string(request_handle_token->str)); - g_variant_builder_add(&options_builder, "{sv}", "session_handle_token", g_variant_new_string(session_token->str)); + g_variant_builder_add(&options_builder, "{sv}", "handle_token", + g_variant_new_string(request_handle_token->str)); + g_variant_builder_add(&options_builder, "{sv}", "session_handle_token", + g_variant_new_string(session_token->str)); options = g_variant_builder_end(&options_builder); // start to listen to request object in advance request_path = g_string_new(NULL); - g_string_printf(request_path, "/org/freedesktop/portal/desktop/request/%s/%s", sender->str, request_handle_token->str); + g_string_printf(request_path, "/org/freedesktop/portal/desktop/request/%s/%s", sender->str, + request_handle_token->str); g_print("Listening to request at path: %s\n", request_path->str); - g_dbus_connection_signal_subscribe(state->conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path->str, NULL, G_DBUS_SIGNAL_FLAGS_NONE, create_session_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(state->conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path->str, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, create_session_cb, NULL, NULL); // finally, connect to session - result = g_dbus_connection_call_sync(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "CreateSession", g_variant_new_tuple(&options, 1), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + result = g_dbus_connection_call_sync(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "CreateSession", + g_variant_new_tuple(&options, 1), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, + &error); if (result == NULL) { blog(LOG_WARNING, "Failed to create global shortcuts portal session: %s", error->message); @@ -507,7 +533,8 @@ void obs_hotkey_portal_register(obs_hotkey_t *hotkey) // init if haven't already obs_hotkey_portal_init(); if (state == NULL) { - blog(LOG_WARNING, "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + blog(LOG_WARNING, + "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); return; } @@ -528,7 +555,8 @@ void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair) // init if haven't already obs_hotkey_portal_init(); if (state == NULL) { - blog(LOG_WARNING, "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + blog(LOG_WARNING, + "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); return; } From 126e3d9bc691a28e711a94814cc3d9d9c4e16cd0 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 22:35:40 +0100 Subject: [PATCH 08/10] Revert changes to obs-nix-wayland.c --- libobs/obs-nix-wayland.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/libobs/obs-nix-wayland.c b/libobs/obs-nix-wayland.c index d01f704df0d216..3155db91adc7a1 100644 --- a/libobs/obs-nix-wayland.c +++ b/libobs/obs-nix-wayland.c @@ -17,14 +17,8 @@ #include "obs-internal.h" #include "obs-nix-platform.h" -#include "util/bmem.h" -#include "util/c99defs.h" -#include "util/dstr.h" #include "obs-nix-wayland.h" -#include -#include -#include #include #include #include @@ -37,9 +31,6 @@ // X11 keymaps only have 4 shift levels, im not sure xkbcommon supports a way to shift the state into a higher level anyway. #define MAX_SHIFT_LEVELS 4 -#define PORTAL_NAME "org.freedesktop.portal.Desktop" -#define PORTAL_PATH "/org/freedesktop/portal/desktop" - struct obs_hotkeys_platform { struct wl_display *display; struct wl_seat *seat; From 2a083ac6013ce9a64dd8e05d3a60c15a5433d830 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 23:12:27 +0100 Subject: [PATCH 09/10] Implement unregistering for portal hotkeys. --- libobs/obs-hotkey-portal.c | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index 850b28a59d956f..9e2f7fc4e23de7 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -203,7 +203,6 @@ static gboolean obs_hotkey_portal_finish_registration() return G_SOURCE_CONTINUE; } - blog(LOG_WARNING, "Here"); state->bind_shortcuts_called_for_session = true; // bind some shortcuts @@ -420,7 +419,6 @@ static void create_session_cb(GDBusConnection *conn, const char *sender_name, co g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "ShortcutsChanged", PORTAL_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, shortcuts_changed_cb, NULL, NULL); - blog(LOG_WARNING, "HOASJOID"); state->session_path = bstrdup(session_handle); } @@ -574,20 +572,18 @@ void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair) void obs_hotkey_portal_unregister(obs_hotkey_id id) { - UNUSED_PARAMETER(id); - // TODO - // if (state == NULL) { - // blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); - // return; - // } - // - // blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); - // - // // remove from pending list (if it's there) - // for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { - // if (*(obs_hotkey_id*)l->data == id) { - // g_queue_delete_link(state->pending_hotkeys, l); - // break; - // } - // } + if (state == NULL) { + blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + return; + } + + blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); + + // remove from pending list (if it's there) + for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { + if (*(obs_hotkey_id*)l->data == id) { + g_queue_delete_link(state->pending_hotkeys, l); + break; + } + } } From 95a006113fd99cea22e936b5e592e77a0aabf646 Mon Sep 17 00:00:00 2001 From: Adam Fallon Date: Tue, 21 Jul 2026 23:15:52 +0100 Subject: [PATCH 10/10] Amend copyright notices. --- libobs/obs-hotkey-portal.c | 22 ++++++++++++++++++++-- libobs/obs-hotkey-portal.h | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c index 9e2f7fc4e23de7..5a43a8e84ee2e1 100644 --- a/libobs/obs-hotkey-portal.c +++ b/libobs/obs-hotkey-portal.c @@ -1,3 +1,20 @@ +/****************************************************************************** + Copyright (C) 2026 by Adam Fallon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + #include "obs-hotkey-portal.h" #include "obs-hotkey.h" #include "obs.h" @@ -573,7 +590,8 @@ void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair) void obs_hotkey_portal_unregister(obs_hotkey_id id) { if (state == NULL) { - blog(LOG_WARNING, "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + blog(LOG_WARNING, + "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); return; } @@ -581,7 +599,7 @@ void obs_hotkey_portal_unregister(obs_hotkey_id id) // remove from pending list (if it's there) for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { - if (*(obs_hotkey_id*)l->data == id) { + if (*(obs_hotkey_id *)l->data == id) { g_queue_delete_link(state->pending_hotkeys, l); break; } diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h index 731bb2776e5811..0667164b57a7cd 100644 --- a/libobs/obs-hotkey-portal.h +++ b/libobs/obs-hotkey-portal.h @@ -1,5 +1,5 @@ /****************************************************************************** - Copyright (C) 2026-2026 by Adam Fallon + Copyright (C) 2026 by Adam Fallon This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by