Repository: oliverschwendener/ueli
Pull request #1293 (2024-12-10 15:46:37)

feat: added option to disable hotkey
The goal of this PR is to add the possibility to quickly disable/enable the hotkey. This might be helpful when playing games in fullscreen and one doesn't want to press the hotkey by accident.

Enable/disable the hotkey from the settings:
<img width="765" alt="image" src="https://github.com/user-attachments/assets/a8ce09fc-3936-469e-987d-f42f88bfcab2">

Enable/disable the hotkey from the tray icon menu:
<img width="372" alt="image" src="https://github.com/user-attachments/assets/4d488408-5e66-4c2f-ba2b-3efe2925023b">

Enable/disable the hotkey from the Ueli Command extension:
<img width="615" alt="image" src="https://github.com/user-attachments/assets/8d0613ae-cd92-4705-9f0f-6b78db6e2048">



Patch:
diff --git a/src/common/Core/FluentIcon.ts b/src/common/Core/FluentIcon.ts
index 24f6be15..d74bda52 100644
--- a/src/common/Core/FluentIcon.ts
+++ b/src/common/Core/FluentIcon.ts
@@ -7,6 +7,7 @@ export type FluentIcon =
     | "AppsAddInRegular"
     | "ArrowClockwiseRegular"
     | "ArrowSquareUpRightRegular"
+    | "CheckmarkCircleRegular"
     | "CopyRegular"
     | "DeleteDismissRegular"
     | "DismissCircleRegular"
diff --git a/src/main/Core/GlobalShortcut/GlobalShortcutModule.ts b/src/main/Core/GlobalShortcut/GlobalShortcutModule.ts
index 226d7d59..4499f8da 100644
--- a/src/main/Core/GlobalShortcut/GlobalShortcutModule.ts
+++ b/src/main/Core/GlobalShortcut/GlobalShortcutModule.ts
@@ -10,6 +10,8 @@ export class GlobalShortcutModule {
         const settingsManager = dependencyRegistry.get("SettingsManager");
         const logger = dependencyRegistry.get("Logger");
 
+        const hotkeyIsEnabled = () => settingsManager.getValue("general.hotkey.enabled", true);
+
         const registerHotkey = () => {
             const hotkey = settingsManager.getValue("general.hotkey", "Alt+Space");
 
@@ -21,8 +23,18 @@ export class GlobalShortcutModule {
             globalShortcut.register(hotkey, () => eventEmitter.emitEvent("hotkeyPressed"));
         };
 
-        registerHotkey();
+        if (hotkeyIsEnabled()) {
+            registerHotkey();
+        }
 
         eventSubscriber.subscribe("settingUpdated[general.hotkey]", () => registerHotkey());
+
+        eventSubscriber.subscribe("settingUpdated[general.hotkey.enabled]", () => {
+            if (hotkeyIsEnabled()) {
+                registerHotkey();
+            } else {
+                globalShortcut.unregisterAll();
+            }
+        });
     }
 }
diff --git a/src/main/Core/TrayIcon/ContextMenuTemplateProvider.ts b/src/main/Core/TrayIcon/ContextMenuTemplateProvider.ts
index 4b7fb810..e3a7123f 100644
--- a/src/main/Core/TrayIcon/ContextMenuTemplateProvider.ts
+++ b/src/main/Core/TrayIcon/ContextMenuTemplateProvider.ts
@@ -1,3 +1,4 @@
+import type { SettingsManager } from "@Core/SettingsManager";
 import type { Translator } from "@Core/Translator";
 import type { UeliCommandInvoker } from "@Core/UeliCommand";
 import type { Resources } from "@common/Core/Translator";
@@ -8,12 +9,15 @@ export class ContextMenuTemplateProvider {
     public constructor(
         private readonly translator: Translator,
         private readonly ueliCommandInvoker: UeliCommandInvoker,
+        private readonly settingsManager: SettingsManager,
         private readonly resources: Resources<TrayIconTranslations>,
     ) {}
 
     public async get(): Promise<MenuItemConstructorOptions[]> {
         const { t } = this.translator.createT(this.resources);
 
+        const hotkeyEnabled = this.settingsManager.getValue("general.hotkey.enabled", true);
+
         return [
             {
                 label: t("trayIcon.contextMenu.show"),
@@ -31,6 +35,16 @@ export class ContextMenuTemplateProvider {
                 label: t("trayIcon.contextMenu.quit"),
                 click: () => this.ueliCommandInvoker.invokeUeliCommand("quit"),
             },
+            {
+                label: t("trayIcon.contextMenu.hotkey"),
+                checked: hotkeyEnabled,
+                type: "checkbox",
+                toolTip: t("trayIcon.contextMenu.hotkey.tooltip"),
+                click: () =>
+                    hotkeyEnabled
+                        ? this.ueliCommandInvoker.invokeUeliCommand("disableHotkey")
+                        : this.ueliCommandInvoker.invokeUeliCommand("enableHotkey"),
+            },
         ];
     }
 }
diff --git a/src/main/Core/TrayIcon/TrayIconModule.ts b/src/main/Core/TrayIcon/TrayIconModule.ts
index 4fdc1f85..c090f675 100644
--- a/src/main/Core/TrayIcon/TrayIconModule.ts
+++ b/src/main/Core/TrayIcon/TrayIconModule.ts
@@ -18,9 +18,6 @@ export class TrayIconModule {
         const nativeTheme = dependencyRegistry.get("NativeTheme");
         const assetPathResolver = dependencyRegistry.get("AssetPathResolver");
         const operatingSystem = dependencyRegistry.get("OperatingSystem");
-        const translator = dependencyRegistry.get("Translator");
-        const ueliCommandInvoker = dependencyRegistry.get("UeliCommandInvoker");
-        const eventSubscriber = dependencyRegistry.get("EventSubscriber");
 
         const trayIconFilePathResolvers: Record<OperatingSystem, () => TrayIconFilePathResolver> = {
             Linux: () => new LinuxTrayIconFilePathResolver(nativeTheme, assetPathResolver),
@@ -31,7 +28,12 @@ export class TrayIconModule {
         const trayIconManager = new TrayIconManager(
             new TrayCreator(),
             trayIconFilePathResolvers[operatingSystem](),
-            new ContextMenuTemplateProvider(translator, ueliCommandInvoker, resources),
+            new ContextMenuTemplateProvider(
+                dependencyRegistry.get("Translator"),
+                dependencyRegistry.get("UeliCommandInvoker"),
+                dependencyRegistry.get("SettingsManager"),
+                resources,
+            ),
             new ContextMenuBuilder(),
         );
 
@@ -39,6 +41,9 @@ export class TrayIconModule {
 
         nativeTheme.on("updated", () => trayIconManager.updateImage());
 
+        const eventSubscriber = dependencyRegistry.get("EventSubscriber");
+
         eventSubscriber.subscribe("settingUpdated[general.language]", () => trayIconManager.updateContextMenu());
+        eventSubscriber.subscribe("settingUpdated[general.hotkey.enabled]", () => trayIconManager.updateContextMenu());
     }
 }
diff --git a/src/main/Core/TrayIcon/TrayIconTranslations.ts b/src/main/Core/TrayIcon/TrayIconTranslations.ts
index dd5a0bdb..9dd4c441 100644
--- a/src/main/Core/TrayIcon/TrayIconTranslations.ts
+++ b/src/main/Core/TrayIcon/TrayIconTranslations.ts
@@ -3,4 +3,6 @@ export type TrayIconTranslations = {
     "trayIcon.contextMenu.quit": string;
     "trayIcon.contextMenu.settings": string;
     "trayIcon.contextMenu.show": string;
+    "trayIcon.contextMenu.hotkey": string;
+    "trayIcon.contextMenu.hotkey.tooltip": string;
 };
diff --git a/src/main/Core/TrayIcon/resources.ts b/src/main/Core/TrayIcon/resources.ts
index 885e841c..06c84159 100644
--- a/src/main/Core/TrayIcon/resources.ts
+++ b/src/main/Core/TrayIcon/resources.ts
@@ -7,11 +7,15 @@ export const resources: Resources<TrayIconTranslations> = {
         "trayIcon.contextMenu.quit": "Quit",
         "trayIcon.contextMenu.settings": "Settings",
         "trayIcon.contextMenu.show": "Show",
+        "trayIcon.contextMenu.hotkey": "Hotkey",
+        "trayIcon.contextMenu.hotkey.tooltip": "Click to enable/disable the hotkey",
     },
     "de-CH": {
         "trayIcon.contextMenu.about": "Über",
         "trayIcon.contextMenu.quit": "Beenden",
         "trayIcon.contextMenu.settings": "Einstellungen",
         "trayIcon.contextMenu.show": "Anzeigen",
+        "trayIcon.contextMenu.hotkey": "Tastenkombination",
+        "trayIcon.contextMenu.hotkey.tooltip": "Klick um die Tastenkombination zu aktivieren/deaktivieren",
     },
 };
diff --git a/src/main/Core/UeliCommand/Contract/UeliCommand.ts b/src/main/Core/UeliCommand/Contract/UeliCommand.ts
index f1eeaf81..ad7e45fe 100644
--- a/src/main/Core/UeliCommand/Contract/UeliCommand.ts
+++ b/src/main/Core/UeliCommand/Contract/UeliCommand.ts
@@ -1,5 +1,7 @@
 export type UeliCommand =
     | "centerWindow"
+    | "disableHotkey"
+    | "enableHotkey"
     | "openAbout"
     | "openExtensions"
     | "openSettings"
diff --git a/src/main/Core/UeliCommand/UeliCommandActionHandler.ts b/src/main/Core/UeliCommand/UeliCommandActionHandler.ts
new file mode 100644
index 00000000..52f0dee3
--- /dev/null
+++ b/src/main/Core/UeliCommand/UeliCommandActionHandler.ts
@@ -0,0 +1,13 @@
+import type { SearchResultItemAction } from "@common/Core";
+import type { ActionHandler } from "@Core/ActionHandler";
+import type { UeliCommand, UeliCommandInvoker } from "./Contract";
+
+export class UeliCommandActionHandler implements ActionHandler {
+    public readonly id = "UeliCommand";
+
+    public constructor(private readonly ueliCommandInvoker: UeliCommandInvoker) {}
+
+    public async invokeAction(action: SearchResultItemAction): Promise<void> {
+        await this.ueliCommandInvoker.invokeUeliCommand(action.argument as UeliCommand);
+    }
+}
diff --git a/src/main/Core/UeliCommand/UeliCommandInvoker.ts b/src/main/Core/UeliCommand/UeliCommandInvoker.ts
index 005ae2f2..63579d0e 100644
--- a/src/main/Core/UeliCommand/UeliCommandInvoker.ts
+++ b/src/main/Core/UeliCommand/UeliCommandInvoker.ts
@@ -1,3 +1,4 @@
+import type { SettingsManager } from "@Core/SettingsManager";
 import type { App } from "electron";
 import type { EventEmitter } from "../EventEmitter";
 import type {
@@ -10,10 +11,13 @@ export class UeliCommandInvoker implements UeliCommandInvokerInterface {
     public constructor(
         private readonly app: App,
         private readonly eventEmitter: EventEmitter,
+        private readonly settingsManager: SettingsManager,
     ) {}
 
     public invokeUeliCommand(ueliCommand: UeliCommand): Promise<void> {
         const map: Record<UeliCommand, () => Promise<void>> = {
+            disableHotkey: async () => await this.settingsManager.updateValue("general.hotkey.enabled", false),
+            enableHotkey: async () => await this.settingsManager.updateValue("general.hotkey.enabled", true),
             openExtensions: async () =>
                 this.emitUeliCommandInvokedEvent({
                     ueliCommand: "openExtensions",
@@ -47,6 +51,10 @@ export class UeliCommandInvoker implements UeliCommandInvokerInterface {
                 }),
         };
 
+        if (!Object.keys(map).includes(ueliCommand)) {
+            throw new Error(`Invalid ueli command: ${ueliCommand}`);
+        }
+
         return map[ueliCommand]();
     }
 
diff --git a/src/main/Core/UeliCommand/UeliCommandModule.ts b/src/main/Core/UeliCommand/UeliCommandModule.ts
index a7363c36..6f76dd3e 100644
--- a/src/main/Core/UeliCommand/UeliCommandModule.ts
+++ b/src/main/Core/UeliCommand/UeliCommandModule.ts
@@ -1,12 +1,19 @@
 import type { Dependencies } from "@Core/Dependencies";
 import type { DependencyRegistry } from "@Core/DependencyRegistry";
+import { UeliCommandActionHandler } from "./UeliCommandActionHandler";
 import { UeliCommandInvoker } from "./UeliCommandInvoker";
 
 export class UeliCommandModule {
     public static bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>) {
         const app = dependencyRegistry.get("App");
         const eventEmitter = dependencyRegistry.get("EventEmitter");
+        const actionHandlerRegistry = dependencyRegistry.get("ActionHandlerRegistry");
+        const settingsManager = dependencyRegistry.get("SettingsManager");
 
-        dependencyRegistry.register("UeliCommandInvoker", new UeliCommandInvoker(app, eventEmitter));
+        const ueliCommandInvoker = new UeliCommandInvoker(app, eventEmitter, settingsManager);
+
+        dependencyRegistry.register("UeliCommandInvoker", ueliCommandInvoker);
+
+        actionHandlerRegistry.register(new UeliCommandActionHandler(ueliCommandInvoker));
     }
 }
diff --git a/src/main/Extensions/UeliCommand/UeliCommandActionHandler.ts b/src/main/Extensions/UeliCommand/UeliCommandActionHandler.ts
deleted file mode 100644
index 1ae6b20f..00000000
--- a/src/main/Extensions/UeliCommand/UeliCommandActionHandler.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { SearchResultItemAction } from "@common/Core";
-import type { ActionHandler } from "@Core/ActionHandler";
-import type { UeliCommandInvoker } from "@Core/UeliCommand";
-
-export class UeliCommandActionHandler implements ActionHandler {
-    public readonly id = "UeliCommand";
-
-    public constructor(private readonly ueliCommandInvoker: UeliCommandInvoker) {}
-
-    public async invokeAction(action: SearchResultItemAction): Promise<void> {
-        const map: Record<string, () => Promise<void>> = {
-            quit: () => this.ueliCommandInvoker.invokeUeliCommand("quit"),
-            settings: () => this.ueliCommandInvoker.invokeUeliCommand("openSettings"),
-            extensions: () => this.ueliCommandInvoker.invokeUeliCommand("openExtensions"),
-            centerWindow: () => this.ueliCommandInvoker.invokeUeliCommand("centerWindow"),
-            rescanExtensions: () => this.ueliCommandInvoker.invokeUeliCommand("rescanExtensions"),
-        };
-
-        if (!Object.keys(map).includes(action.argument)) {
-            throw new Error(`Unexpected argument: ${action.argument}`);
-        }
-
-        await map[action.argument]();
-    }
-}
diff --git a/src/main/Extensions/UeliCommand/UeliCommandExtension.ts b/src/main/Extensions/UeliCommand/UeliCommandExtension.ts
index 54a26c1f..c136c3ad 100644
--- a/src/main/Extensions/UeliCommand/UeliCommandExtension.ts
+++ b/src/main/Extensions/UeliCommand/UeliCommandExtension.ts
@@ -1,6 +1,8 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
 import type { Extension } from "@Core/Extension";
+import type { SettingsManager } from "@Core/SettingsManager";
 import type { Translator } from "@Core/Translator";
+import type { UeliCommand } from "@Core/UeliCommand";
 import type { SearchResultItem } from "@common/Core";
 import type { Image } from "@common/Core/Image";
 
@@ -21,77 +23,110 @@ export class UeliCommandExtension implements Extension {
     public constructor(
         private readonly assetPathResolver: AssetPathResolver,
         private readonly translator: Translator,
+        private readonly settingsManager: SettingsManager,
     ) {}
 
     public async getSearchResultItems(): Promise<SearchResultItem[]> {
         const { t } = this.translator.createT(this.getI18nResources());
 
-        const map: Record<string, SearchResultItem> = {
-            quit: {
+        const commonSearchResultItems: SearchResultItem[] = [
+            {
                 id: "ueliCommand:quit",
                 description: t("description"),
                 name: t("quitUeli"),
                 image: this.getImage(),
                 defaultAction: {
                     handlerId: "UeliCommand",
-                    argument: "quit",
+                    argument: <UeliCommand>"quit",
                     description: t("quitUeli"),
                     requiresConfirmation: true,
                     fluentIcon: "DismissCircleRegular",
                     hideWindowAfterInvocation: true,
                 },
             },
-            settings: {
+            {
                 id: "ueliCommand:settings",
                 description: t("description"),
                 name: t("openSettings"),
                 image: this.getImage(),
                 defaultAction: {
                     handlerId: "UeliCommand",
-                    argument: "settings",
+                    argument: <UeliCommand>"openSettings",
                     description: t("openSettings"),
                     fluentIcon: "SettingsRegular",
                 },
             },
-            extensions: {
+            {
                 id: "ueliCommand:extensions",
                 description: t("description"),
                 name: t("openExtensions"),
                 image: this.getImage(),
                 defaultAction: {
                     handlerId: "UeliCommand",
-                    argument: "extensions",
+                    argument: <UeliCommand>"openExtensions",
                     description: t("openExtensions"),
                     fluentIcon: "AppsAddInRegular",
                 },
             },
-            centerWindow: {
+            {
                 id: "ueliCommand:centerWindow",
                 description: t("description"),
                 name: t("centerWindow"),
                 image: this.getImage(),
                 defaultAction: {
                     handlerId: "UeliCommand",
-                    argument: "centerWindow",
+                    argument: <UeliCommand>"centerWindow",
                     description: t("centerWindow"),
                     fluentIcon: "AppsAddInRegular",
                 },
             },
-            rescanExtensions: {
+            {
                 id: "ueliCommand:rescanExtensions",
                 description: t("description"),
                 name: t("rescanExtensions"),
                 image: this.getImage(),
                 defaultAction: {
                     handlerId: "UeliCommand",
-                    argument: "rescanExtensions",
+                    argument: <UeliCommand>"rescanExtensions",
                     description: t("rescanExtensions"),
                     fluentIcon: "ArrowClockwiseRegular",
                 },
             },
-        };
+        ];
+
+        const hotkeyIsEnabled = this.settingsManager.getValue("general.hotkey.enabled", true);
+
+        const hotkeySearchResultItems: SearchResultItem[] = hotkeyIsEnabled
+            ? [
+                  {
+                      id: "ueliCommand:toggleHotkey",
+                      description: t("description"),
+                      name: t("disableHotkey"),
+                      image: this.getImage(),
+                      defaultAction: {
+                          handlerId: "UeliCommand",
+                          argument: <UeliCommand>"disableHotkey",
+                          description: t("disableHotkey"),
+                          fluentIcon: "DismissCircleRegular",
+                      },
+                  },
+              ]
+            : [
+                  {
+                      id: "ueliCommand:toggleHotkey",
+                      description: t("description"),
+                      name: t("enableHotkey"),
+                      image: this.getImage(),
+                      defaultAction: {
+                          handlerId: "UeliCommand",
+                          argument: <UeliCommand>"enableHotkey",
+                          description: t("enableHotkey"),
+                          fluentIcon: "CheckmarkCircleRegular",
+                      },
+                  },
+              ];
 
-        return Object.values(map);
+        return [...commonSearchResultItems, ...hotkeySearchResultItems];
     }
 
     public getImage(): Image {
@@ -111,7 +146,7 @@ export class UeliCommandExtension implements Extension {
     }
 
     public getSettingKeysTriggeringRescan() {
-        return ["general.language"];
+        return ["general.language", "general.hotkey.enabled"];
     }
 
     public getI18nResources() {
@@ -124,6 +159,8 @@ export class UeliCommandExtension implements Extension {
                 centerWindow: "Center Ueli window",
                 quitUeli: "Quit Ueli",
                 rescanExtensions: "Rescan extensions",
+                disableHotkey: "Disable hotkey",
+                enableHotkey: "Enable hotkey",
             },
             "de-CH": {
                 extensionName: "Ueli Befehle",
@@ -133,6 +170,8 @@ export class UeliCommandExtension implements Extension {
                 centerWindow: "Ueli-Fenster zentrieren",
                 quitUeli: "Ueli Beenden",
                 rescanExtensions: "Erweiterungen neu scannen",
+                disableHotkey: "Tastenkombination deaktivieren",
+                enableHotkey: "Tastenkombination aktivieren",
             },
         };
     }
diff --git a/src/main/Extensions/UeliCommand/UeliCommandModule.ts b/src/main/Extensions/UeliCommand/UeliCommandModule.ts
index 0150b74a..1ccface5 100644
--- a/src/main/Extensions/UeliCommand/UeliCommandModule.ts
+++ b/src/main/Extensions/UeliCommand/UeliCommandModule.ts
@@ -2,18 +2,16 @@ import type { Dependencies } from "@Core/Dependencies";
 import type { DependencyRegistry } from "@Core/DependencyRegistry";
 import type { ExtensionBootstrapResult } from "../ExtensionBootstrapResult";
 import type { ExtensionModule } from "../ExtensionModule";
-import { UeliCommandActionHandler } from "./UeliCommandActionHandler";
 import { UeliCommandExtension } from "./UeliCommandExtension";
 
 export class UeliCommandModule implements ExtensionModule {
     public bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>): ExtensionBootstrapResult {
-        const assetPathResolver = dependencyRegistry.get("AssetPathResolver");
-        const ueliCommandInvoker = dependencyRegistry.get("UeliCommandInvoker");
-        const translator = dependencyRegistry.get("Translator");
-
         return {
-            extension: new UeliCommandExtension(assetPathResolver, translator),
-            actionHandlers: [new UeliCommandActionHandler(ueliCommandInvoker)],
+            extension: new UeliCommandExtension(
+                dependencyRegistry.get("AssetPathResolver"),
+                dependencyRegistry.get("Translator"),
+                dependencyRegistry.get("SettingsManager"),
+            ),
         };
     }
 }
diff --git a/src/renderer/Core/I18n/getCoreResources.ts b/src/renderer/Core/I18n/getCoreResources.ts
index b5c46082..f26f351f 100644
--- a/src/renderer/Core/I18n/getCoreResources.ts
+++ b/src/renderer/Core/I18n/getCoreResources.ts
@@ -44,10 +44,12 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
                 "en-US": {
                     title: "General",
                     hotkey: "Hotkey",
-                    validHotkey: "Valid hotkey",
-                    invalidHotkey: "Invalid hotkey",
-                    saveHotkey: "Save",
-                    hotkeyMoreInfo: "More info",
+                    hotkeyEnabled: "Hotkey enabled",
+                    hotkeyBinding: "Hotkey binding",
+                    validHotkeyBinding: "Valid hotkey",
+                    invalidHotkeyBinding: "Invalid hotkey",
+                    saveHotkeyBinding: "Save",
+                    hotkeyBindingMoreInfo: "More info",
                     language: "Language",
                     autostart: "Autostart",
                     autostartDescription: "Start Ueli automatically after you log into the computer",
@@ -65,11 +67,13 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
                 },
                 "de-CH": {
                     title: "Allgemein",
-                    hotkey: "Tastenkombination",
-                    validHotkey: "Gültige Tastenkombination",
-                    invalidHotkey: "Ungültige Tastenkombination",
-                    saveHotkey: "Speichern",
-                    hotkeyMoreInfo: "Mehr Informationen",
+                    hotkey: "Hotkey",
+                    hotkeyEnabled: "Hotkey enabled",
+                    hotkeyBinding: "Tastenkombination",
+                    validHotkeyBinding: "Gültige Tastenkombination",
+                    invalidHotkeyBinding: "Ungültige Tastenkombination",
+                    saveHotkeyBinding: "Speichern",
+                    hotkeyBindingMoreInfo: "Mehr Informationen",
                     language: "Sprache",
                     autostart: "Autostart",
                     autostartDescription: "Ueli automatisch starten, nach Anmelden am Computer",
diff --git a/src/renderer/Core/Search/FluentIcon.tsx b/src/renderer/Core/Search/FluentIcon.tsx
index 763df09e..5919f0a9 100644
--- a/src/renderer/Core/Search/FluentIcon.tsx
+++ b/src/renderer/Core/Search/FluentIcon.tsx
@@ -3,6 +3,7 @@ import {
     AppsAddInRegular,
     ArrowClockwiseRegular,
     ArrowSquareUpRightRegular,
+    CheckmarkCircleRegular,
     CopyRegular,
     DeleteDismissRegular,
     DismissCircleRegular,
@@ -23,6 +24,7 @@ export const FluentIcon = ({ icon }: { icon: FluentIconString }) => {
         AppsAddInRegular: <AppsAddInRegular />,
         ArrowClockwiseRegular: <ArrowClockwiseRegular />,
         ArrowSquareUpRightRegular: <ArrowSquareUpRightRegular />,
+        CheckmarkCircleRegular: <CheckmarkCircleRegular />,
         CopyRegular: <CopyRegular />,
         DeleteDismissRegular: <DeleteDismissRegular />,
         DismissCircleRegular: <DismissCircleRegular />,
diff --git a/src/renderer/Core/Settings/Pages/General/General.tsx b/src/renderer/Core/Settings/Pages/General/General.tsx
index 459c0b38..7ad065ab 100644
--- a/src/renderer/Core/Settings/Pages/General/General.tsx
+++ b/src/renderer/Core/Settings/Pages/General/General.tsx
@@ -3,7 +3,7 @@ import { SettingGroupList } from "../../SettingGroupList";
 import { Autostart } from "./Autostart";
 import { CustomWebBrowser } from "./CustomWebBrowser";
 import { DockSettings } from "./DockSettings";
-import { HotKey } from "./HotKey";
+import { HotkeySettings } from "./HotKey";
 import { Language } from "./Language";
 import { SearchHistory } from "./SearchHistory";
 import { UrlImageGenerator } from "./UrlImageGenerator";
@@ -15,10 +15,10 @@ export const General = () => {
         <SettingGroupList>
             <SettingGroup title="General">
                 <Language />
-                <HotKey />
                 <Autostart />
                 {operatingSystem === "macOS" && <DockSettings />}
             </SettingGroup>
+            <HotkeySettings />
             <SettingGroup title="Search History">
                 <SearchHistory />
             </SettingGroup>
diff --git a/src/renderer/Core/Settings/Pages/General/HotKey.tsx b/src/renderer/Core/Settings/Pages/General/HotKey/HotkeyBinding.tsx
similarity index 76%
rename from src/renderer/Core/Settings/Pages/General/HotKey.tsx
rename to src/renderer/Core/Settings/Pages/General/HotKey/HotkeyBinding.tsx
index 56dedebd..40b322ca 100644
--- a/src/renderer/Core/Settings/Pages/General/HotKey.tsx
+++ b/src/renderer/Core/Settings/Pages/General/HotKey/HotkeyBinding.tsx
@@ -6,7 +6,11 @@ import { InfoRegular } from "@fluentui/react-icons";
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
 
-export const HotKey = () => {
+type HotkeyBindingProps = {
+    hotkeyEnabled: boolean;
+};
+
+export const HotKeyBinding = ({ hotkeyEnabled }: HotkeyBindingProps) => {
     const { t } = useTranslation("settingsGeneral");
 
     const { value: hotkey, updateValue: setHotkey } = useSetting({ key: "general.hotkey", defaultValue: "Alt+Space" });
@@ -15,21 +19,25 @@ export const HotKey = () => {
 
     return (
         <Setting
-            label={t("hotkey")}
+            label={t("hotkeyBinding")}
             control={
                 <Field
-                    validationMessage={isValidHotkey(temporaryHotkey) ? t("validHotkey") : t("invalidHotkey")}
+                    validationMessage={
+                        isValidHotkey(temporaryHotkey) ? t("validHotkeyBinding") : t("invalidHotkeyBinding")
+                    }
                     validationState={isValidHotkey(temporaryHotkey) ? "success" : "error"}
                 >
                     <Input
+                        disabled={!hotkeyEnabled}
                         value={temporaryHotkey}
                         onChange={(_, { value }) => setTemporaryHotkey(value)}
                         onBlur={() =>
                             isValidHotkey(temporaryHotkey) ? setHotkey(temporaryHotkey) : setTemporaryHotkey(hotkey)
                         }
                         contentAfter={
-                            <Tooltip content={t("hotkeyMoreInfo")} relationship="label" withArrow>
+                            <Tooltip content={t("hotkeyBindingMoreInfo")} relationship="label" withArrow>
                                 <Button
+                                    disabled={!hotkeyEnabled}
                                     appearance="subtle"
                                     size="small"
                                     icon={<InfoRegular />}
diff --git a/src/renderer/Core/Settings/Pages/General/HotKey/HotkeyEnabled.tsx b/src/renderer/Core/Settings/Pages/General/HotKey/HotkeyEnabled.tsx
new file mode 100644
index 00000000..b4b748ff
--- /dev/null
+++ b/src/renderer/Core/Settings/Pages/General/HotKey/HotkeyEnabled.tsx
@@ -0,0 +1,19 @@
+import { Setting } from "@Core/Settings/Setting";
+import { Switch } from "@fluentui/react-components";
+import { useTranslation } from "react-i18next";
+
+type HotkeyEnabledProps = {
+    hotkeyEnabled: boolean;
+    setHotkeyEnabled: (value: boolean) => void;
+};
+
+export const HotkeyEnabled = ({ hotkeyEnabled, setHotkeyEnabled }: HotkeyEnabledProps) => {
+    const { t } = useTranslation("settingsGeneral");
+
+    return (
+        <Setting
+            label={t("hotkeyEnabled")}
+            control={<Switch checked={hotkeyEnabled} onChange={(_, { checked }) => setHotkeyEnabled(checked)} />}
+        />
+    );
+};
diff --git a/src/renderer/Core/Settings/Pages/General/HotKey/HotkeySettings.tsx b/src/renderer/Core/Settings/Pages/General/HotKey/HotkeySettings.tsx
new file mode 100644
index 00000000..b03cec9b
--- /dev/null
+++ b/src/renderer/Core/Settings/Pages/General/HotKey/HotkeySettings.tsx
@@ -0,0 +1,21 @@
+import { useSetting } from "@Core/Hooks";
+import { SettingGroup } from "@Core/Settings/SettingGroup";
+import { useTranslation } from "react-i18next";
+import { HotKeyBinding } from "./HotkeyBinding";
+import { HotkeyEnabled } from "./HotkeyEnabled";
+
+export const HotkeySettings = () => {
+    const { t } = useTranslation("settingsGeneral");
+
+    const { value: hotkeyEnabled, updateValue: setHotkeyEnabled } = useSetting({
+        key: "general.hotkey.enabled",
+        defaultValue: true,
+    });
+
+    return (
+        <SettingGroup title={t("hotkey")}>
+            <HotkeyEnabled hotkeyEnabled={hotkeyEnabled} setHotkeyEnabled={setHotkeyEnabled} />
+            <HotKeyBinding hotkeyEnabled={hotkeyEnabled} />
+        </SettingGroup>
+    );
+};
diff --git a/src/renderer/Core/Settings/Pages/General/HotKey/index.ts b/src/renderer/Core/Settings/Pages/General/HotKey/index.ts
new file mode 100644
index 00000000..637c4bb6
--- /dev/null
+++ b/src/renderer/Core/Settings/Pages/General/HotKey/index.ts
@@ -0,0 +1,1 @@
+export * from "./HotkeySettings";


Test patch:
diff --git a/src/main/Core/TrayIcon/ContextMenuTemplateProvider.test.ts b/src/main/Core/TrayIcon/ContextMenuTemplateProvider.test.ts
index 6f86cc5d..4f771080 100644
--- a/src/main/Core/TrayIcon/ContextMenuTemplateProvider.test.ts
+++ b/src/main/Core/TrayIcon/ContextMenuTemplateProvider.test.ts
@@ -1,3 +1,4 @@
+import type { SettingsManager } from "@Core/SettingsManager";
 import type { Translator } from "@Core/Translator";
 import type { UeliCommandInvoker } from "@Core/UeliCommand";
 import type { MenuItemConstructorOptions } from "electron";
@@ -10,19 +11,36 @@ describe(ContextMenuTemplateProvider, () => {
         it("should return context menu template with translations", async () => {
             const t = (key: string) => `translation[${key}]`;
             const createTMock = vi.fn().mockReturnValue({ t });
+
+            const getValueMock = vi.fn().mockReturnValue(true);
+
             const translator = <Translator>{ createT: createTMock };
             const ueliCommandInvoker = <UeliCommandInvoker>{};
+            const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };
 
-            const actual = await new ContextMenuTemplateProvider(translator, ueliCommandInvoker, resources).get();
+            const actual = await new ContextMenuTemplateProvider(
+                translator,
+                ueliCommandInvoker,
+                settingsManager,
+                resources,
+            ).get();
 
             const expected: MenuItemConstructorOptions[] = [
                 { label: "translation[trayIcon.contextMenu.show]", click: () => null },
                 { label: "translation[trayIcon.contextMenu.settings]", click: () => null },
                 { label: "translation[trayIcon.contextMenu.about]", click: () => null },
                 { label: "translation[trayIcon.contextMenu.quit]", click: () => null },
+                {
+                    label: "translation[trayIcon.contextMenu.hotkey]",
+                    click: () => null,
+                    checked: true,
+                    type: "checkbox",
+                    toolTip: "translation[trayIcon.contextMenu.hotkey.tooltip]",
+                },
             ];
 
             expect(createTMock).toHaveBeenCalledWith(resources);
+            expect(getValueMock).toHaveBeenCalledWith("general.hotkey.enabled", true);
             expect(JSON.stringify(actual)).toEqual(JSON.stringify(expected));
         });
     });
diff --git a/src/main/Core/UeliCommand/UeliCommandActionHandler.test.ts b/src/main/Core/UeliCommand/UeliCommandActionHandler.test.ts
new file mode 100644
index 00000000..8cd33b8c
--- /dev/null
+++ b/src/main/Core/UeliCommand/UeliCommandActionHandler.test.ts
@@ -0,0 +1,16 @@
+import type { SearchResultItemAction } from "@common/Core";
+import { describe, expect, it, vi } from "vitest";
+import type { UeliCommandInvoker } from "./Contract";
+import { UeliCommandActionHandler } from "./UeliCommandActionHandler";
+
+describe(UeliCommandActionHandler, () => {
+    it("should invoke an ueli command", async () => {
+        const invokeUeliCommandMock = vi.fn();
+        const ueliCommandInvoker = <UeliCommandInvoker>{ invokeUeliCommand: (u) => invokeUeliCommandMock(u) };
+        const ueliCommandActionHandler = new UeliCommandActionHandler(ueliCommandInvoker);
+
+        await ueliCommandActionHandler.invokeAction(<SearchResultItemAction>{ argument: "test ueli command" });
+
+        expect(invokeUeliCommandMock).toHaveBeenCalledWith("test ueli command");
+    });
+});
diff --git a/src/main/Extensions/UeliCommand/UeliCommandActionHandler.test.ts b/src/main/Extensions/UeliCommand/UeliCommandActionHandler.test.ts
deleted file mode 100644
index 153a0184..00000000
--- a/src/main/Extensions/UeliCommand/UeliCommandActionHandler.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import type { UeliCommand, UeliCommandInvoker } from "@Core/UeliCommand";
-import type { SearchResultItemAction } from "@common/Core";
-import { describe, expect, it, vi } from "vitest";
-import { UeliCommandActionHandler } from "./UeliCommandActionHandler";
-
-describe(UeliCommandActionHandler, () => {
-    const testInvokeAction = async ({
-        expectedUeliCommand,
-        searchResultItemAction,
-    }: {
-        expectedUeliCommand: UeliCommand;
-        searchResultItemAction: SearchResultItemAction;
-    }) => {
-        const invokeUeliCommandMock = vi.fn().mockReturnValue(Promise.resolve());
-        const ueliCommandInvoker = <UeliCommandInvoker>{ invokeUeliCommand: (u) => invokeUeliCommandMock(u) };
-
-        const ueliCommandActionHandler = new UeliCommandActionHandler(ueliCommandInvoker);
-
-        expect(ueliCommandActionHandler.id).toBe("UeliCommand");
-
-        await ueliCommandActionHandler.invokeAction(searchResultItemAction);
-
-        expect(invokeUeliCommandMock).toHaveBeenCalledWith(expectedUeliCommand);
-    };
-
-    it("should invoke an ueli command", async () => {
-        await testInvokeAction({
-            searchResultItemAction: <SearchResultItemAction>{ argument: "quit" },
-            expectedUeliCommand: "quit",
-        });
-
-        await testInvokeAction({
-            searchResultItemAction: <SearchResultItemAction>{ argument: "settings" },
-            expectedUeliCommand: "openSettings",
-        });
-
-        await testInvokeAction({
-            searchResultItemAction: <SearchResultItemAction>{ argument: "extensions" },
-            expectedUeliCommand: "openExtensions",
-        });
-
-        await testInvokeAction({
-            searchResultItemAction: <SearchResultItemAction>{ argument: "centerWindow" },
-            expectedUeliCommand: "centerWindow",
-        });
-    });
-
-    it("should throw an error if the argument is an invalid ueli command", async () => {
-        const invokeUeliCommandMock = vi.fn().mockReturnValue(Promise.resolve());
-        const ueliCommandInvoker = <UeliCommandInvoker>{ invokeUeliCommand: (u) => invokeUeliCommandMock(u) };
-        const ueliCommandActionHandler = new UeliCommandActionHandler(ueliCommandInvoker);
-
-        await expect(
-            ueliCommandActionHandler.invokeAction(<SearchResultItemAction>{ argument: "unknown" }),
-        ).rejects.toThrowError("Unexpected argument: unknown");
-    });
-});