diff --git a/data/io.elementary.settings-daemon.gschema.xml b/data/io.elementary.settings-daemon.gschema.xml
index 637e4a7a..f3ec639c 100644
--- a/data/io.elementary.settings-daemon.gschema.xml
+++ b/data/io.elementary.settings-daemon.gschema.xml
@@ -127,4 +127,17 @@
+
+
+
+ []
+ Application shortcuts
+
+ The first argument is a type (0 - launch desktop file, 1 - launch cli).
+ The second argument is the 'target', desktop file name if type is 0, commandline if it's 1.
+ The third argument is 'parameters'
+ And the last argument is a list of keyboard shortcuts.
+
+
+
diff --git a/src/Application.vala b/src/Application.vala
index 018b7b9b..ae9f8542 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -20,6 +20,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
private Backends.Housekeeping housekeeping;
private Backends.PowerProfilesSync power_profiles_sync;
+ private Backends.ApplicationShortcuts application_shortcuts;
private const string FDO_ACCOUNTS_NAME = "org.freedesktop.Accounts";
private const string FDO_ACCOUNTS_PATH = "/org/freedesktop/Accounts";
@@ -56,6 +57,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
housekeeping = new Backends.Housekeeping ();
power_profiles_sync = new Backends.PowerProfilesSync ();
+ application_shortcuts = new Backends.ApplicationShortcuts ();
var check_firmware_updates_action = new GLib.SimpleAction ("check-firmware-updates", null);
check_firmware_updates_action.activate.connect (check_firmware_updates);
diff --git a/src/Backends/ApplicationShortcuts.vala b/src/Backends/ApplicationShortcuts.vala
new file mode 100644
index 00000000..94df8215
--- /dev/null
+++ b/src/Backends/ApplicationShortcuts.vala
@@ -0,0 +1,217 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
+ */
+
+public class SettingsDaemon.Backends.ApplicationShortcuts : Object {
+ private enum ActionType {
+ DESKTOP_FILE,
+ COMMAND_LINE
+ }
+
+ private struct Parsed {
+ ActionType type;
+ string target;
+ GLib.HashTable parameters;
+ string[] keybindings;
+ }
+
+ private struct ActionInfo {
+ ActionType type;
+ string target;
+ GLib.HashTable parameters;
+ }
+
+ private GLib.Settings application_settings;
+ private ShellKeyGrabber? key_grabber;
+ private DesktopIntegration? desktop_integration;
+ private ulong key_grabber_id = 0;
+ private GLib.HashTable saved_action_ids;
+
+ construct {
+ application_settings = new GLib.Settings ("io.elementary.settings-daemon.applications");
+ saved_action_ids = new GLib.HashTable (null, null);
+
+ migrate_gsd_shortcuts.begin ();
+
+ application_settings.changed.connect (() => {
+ if (key_grabber != null) {
+ try {
+ key_grabber.ungrab_accelerators (saved_action_ids.get_keys_as_array ());
+ } catch (Error e) {
+ critical ("Couldn't ungrab accelerators: %s", e.message);
+ }
+
+ if (key_grabber_id != 0) {
+ key_grabber.disconnect (key_grabber_id);
+ key_grabber_id = 0;
+ }
+
+ setup_grabs ();
+ }
+ });
+
+ Bus.get_proxy.begin (
+ BusType.SESSION,
+ "org.gnome.Shell", "/org/gnome/Shell",
+ NONE, null,
+ (obj, res) => {
+ try {
+ key_grabber = Bus.get_proxy.end (res);
+ setup_grabs ();
+ } catch (Error e) {
+ critical (e.message);
+ key_grabber = null;
+ }
+ }
+ );
+
+ Bus.get_proxy.begin (
+ BusType.SESSION,
+ "org.pantheon.gala", "/org/pantheon/gala/DesktopInterface",
+ NONE, null,
+ (obj, res) => {
+ try {
+ desktop_integration = Bus.get_proxy.end (res);
+ } catch (Error e) {
+ critical (e.message);
+ desktop_integration = null;
+ }
+ }
+ );
+ }
+
+ private async void migrate_gsd_shortcuts () {
+ unowned var settings_schema = GLib.SettingsSchemaSource.get_default ();
+ if (settings_schema.lookup ("org.gnome.settings-daemon.plugins.media-keys", false) != null) {
+ var value = (Parsed[]) application_settings.get_value ("application-shortcuts");
+
+ var gsd_settings = new GLib.Settings ("org.gnome.settings-daemon.plugins.media-keys");
+ var enabled_keybindings = gsd_settings.get_strv ("custom-keybindings");
+
+ for (var i = 0; i < enabled_keybindings.length; i++) {
+ var settings = new GLib.Settings.with_path ("org.gnome.settings-daemon.plugins.media-keys.custom-keybinding", enabled_keybindings[i]);
+ Parsed new_shortcut = {
+ ActionType.COMMAND_LINE,
+ settings.get_string ("command"),
+ new GLib.HashTable (null, null),
+ { settings.get_string ("binding") }
+ };
+ value += new_shortcut;
+ }
+
+ application_settings.set_value ("application-shortcuts", value);
+ gsd_settings.set_strv ("custom-keybindings", {});
+ }
+ }
+
+ private void setup_grabs () requires (key_grabber != null) {
+ Accelerator[] accelerators = {};
+
+ var parsed_value = (Parsed[]) application_settings.get_value ("application-shortcuts");
+ for (var i = 0; i < parsed_value.length; i++) {
+ var keybindings = parsed_value[i].keybindings;
+ for (var j = 0; j < keybindings.length; j++) {
+ accelerators += Accelerator () {
+ name = keybindings[j],
+ mode_flags = ActionMode.NONE,
+ grab_flags = Meta.KeyBindingFlags.NONE
+ };
+ }
+ }
+
+ uint[] action_ids;
+ try {
+ action_ids = key_grabber.grab_accelerators (accelerators);
+ } catch (Error e) {
+ critical (e.message);
+ return;
+ }
+
+ for (int i = 0; i < action_ids.length; i++) {
+ var parsed_value_i = parsed_value[i];
+ saved_action_ids[action_ids[i]] = { parsed_value_i.type, parsed_value_i.target, parsed_value_i.parameters };
+ }
+
+ key_grabber_id = key_grabber.accelerator_activated.connect (on_accelerator_activated);
+ }
+
+ private void on_accelerator_activated (uint action, GLib.HashTable parameters_dict) {
+ var action_info = saved_action_ids[action];
+ if (action_info == null) {
+ return;
+ }
+
+ var context = Gdk.Display.get_default ().get_app_launch_context ();
+ context.set_timestamp ("timestamp" in parameters_dict ? (uint32) parameters_dict["timestamp"] : Gdk.CURRENT_TIME);
+
+ var action_parameters = action_info.parameters;
+
+ switch (action_info.type) {
+ case DESKTOP_FILE:
+ var desktop_file_name = action_info.target;
+
+ DesktopIntegration.RunningApplication[] apps = {};
+ if (desktop_integration != null) {
+ try {
+ apps = desktop_integration.get_running_applications ();
+ } catch (Error e) {
+ warning (e.message);
+ }
+ }
+
+ var already_launched = false;
+ for (var i = 0; i < apps.length; i++) {
+ if (apps[i].app_id == desktop_file_name) {
+ already_launched = true;
+ break;
+ }
+ }
+
+ if ("action" in action_parameters) {
+ unowned var action_name = action_parameters["action"].get_string ();
+ new DesktopAppInfo (desktop_file_name).launch_action (action_name, context);
+ } else if (!already_launched || desktop_integration == null) {
+ launch_app (desktop_file_name, context);
+ } else {
+ try {
+ var found_window = false;
+ var windows = desktop_integration.get_windows ();
+ for (var i = 0; i < windows.length; i++) {
+ if (windows[i].properties["app-id"].get_string () == desktop_file_name) {
+ found_window = true;
+ desktop_integration.focus_window (windows[i].uid);
+ break;
+ }
+ }
+
+ if (!found_window) {
+ launch_app (desktop_file_name, context);
+ }
+ } catch (Error e) {
+ warning (e.message);
+ launch_app (desktop_file_name, context);
+ }
+ }
+ break;
+
+ case COMMAND_LINE:
+ var commandline = action_info.target;
+
+ try {
+ AppInfo.create_from_commandline (commandline, null, NONE).launch (null, context);
+ } catch (Error e) {
+ warning ("Couldn't launch %s: %s", commandline, e.message);
+ }
+ break;
+ }
+ }
+
+ private void launch_app (string desktop_file_name, Gdk.AppLaunchContext context) {
+ try {
+ new DesktopAppInfo (desktop_file_name).launch (null, context);
+ } catch (Error e) {
+ warning ("Couldn't launch %s: %s", desktop_file_name, e.message);
+ }
+ }
+}
diff --git a/src/DBus/DesktopIntegration.vala b/src/DBus/DesktopIntegration.vala
new file mode 100644
index 00000000..a7157f88
--- /dev/null
+++ b/src/DBus/DesktopIntegration.vala
@@ -0,0 +1,16 @@
+[DBus (name="org.pantheon.gala.DesktopIntegration")]
+public interface DesktopIntegration : GLib.Object {
+ public struct RunningApplication {
+ string app_id;
+ GLib.HashTable details;
+ }
+
+ public struct Window {
+ uint64 uid;
+ GLib.HashTable properties;
+ }
+
+ public abstract RunningApplication[] get_running_applications () throws GLib.DBusError, GLib.IOError;
+ public abstract Window[] get_windows () throws GLib.DBusError, GLib.IOError;
+ public abstract void focus_window (uint64 uid) throws GLib.DBusError, GLib.IOError;
+}
diff --git a/src/DBus/ShellKeyGrabber.vala b/src/DBus/ShellKeyGrabber.vala
new file mode 100644
index 00000000..17d5788d
--- /dev/null
+++ b/src/DBus/ShellKeyGrabber.vala
@@ -0,0 +1,52 @@
+/**
+ * ActionMode:
+ * @NONE: block action
+ * @NORMAL: allow action when in window mode, e.g. when the focus is in an application window
+ * @OVERVIEW: allow action while the overview is active
+ * @LOCK_SCREEN: allow action when the screen is locked, e.g. when the screen shield is shown
+ * @UNLOCK_SCREEN: allow action in the unlock dialog
+ * @LOGIN_SCREEN: allow action in the login screen
+ * @SYSTEM_MODAL: allow action when a system modal dialog (e.g. authentification or session dialogs) is open
+ * @LOOKING_GLASS: allow action in looking glass
+ * @POPUP: allow action while a shell menu is open
+ */
+[Flags]
+public enum ActionMode {
+ NONE = 0,
+ NORMAL = 1 << 0,
+ OVERVIEW = 1 << 1,
+ LOCK_SCREEN = 1 << 2,
+ UNLOCK_SCREEN = 1 << 3,
+ LOGIN_SCREEN = 1 << 4,
+ SYSTEM_MODAL = 1 << 5,
+ LOOKING_GLASS = 1 << 6,
+ POPUP = 1 << 7,
+}
+
+[Flags]
+public enum Meta.KeyBindingFlags {
+ NONE = 0,
+ PER_WINDOW = 1 << 0,
+ BUILTIN = 1 << 1,
+ IS_REVERSED = 1 << 2,
+ NON_MASKABLE = 1 << 3,
+ IGNORE_AUTOREPEAT = 1 << 4,
+}
+
+public struct Accelerator {
+ public string name;
+ public ActionMode mode_flags;
+ public Meta.KeyBindingFlags grab_flags;
+}
+
+[DBus (name = "org.gnome.Shell")]
+public interface ShellKeyGrabber : GLib.Object {
+ public abstract signal void accelerator_activated (uint action, GLib.HashTable parameters_dict);
+
+ public abstract uint grab_accelerator (string accelerator, ActionMode mode_flags, Meta.KeyBindingFlags grab_flags) throws GLib.DBusError, GLib.IOError;
+ public abstract uint[] grab_accelerators (Accelerator[] accelerators) throws GLib.DBusError, GLib.IOError;
+ public abstract bool ungrab_accelerator (uint action) throws GLib.DBusError, GLib.IOError;
+ public abstract bool ungrab_accelerators (uint[] actions) throws GLib.DBusError, GLib.IOError;
+ [DBus (name = "ShowOSD")]
+ public abstract void show_osd (GLib.HashTable parameters_dict) throws GLib.DBusError, GLib.IOError;
+}
diff --git a/src/meson.build b/src/meson.build
index 472148b9..e5cb5503 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -2,6 +2,7 @@ sources = files(
'AccountsService.vala',
'Application.vala',
'Backends/AccentColorManager.vala',
+ 'Backends/ApplicationShortcuts.vala',
'Backends/Housekeeping.vala',
'Backends/InterfaceSettings.vala',
'Backends/KeyboardSettings.vala',
@@ -10,6 +11,8 @@ sources = files(
'Backends/PowerProfilesSync.vala',
'Backends/PrefersColorSchemeSettings.vala',
'Backends/SystemUpdate.vala',
+ 'DBus/DesktopIntegration.vala',
+ 'DBus/ShellKeyGrabber.vala',
'Utils/PkUtils.vala',
'Utils/SessionUtils.vala',
'Utils/SunriseSunsetCalculator.vala',