Skip to content

Desktop: Fixes #11805: Add global shortcut to show/hide Joplin#15013

Open
Ashutoshx7 wants to merge 6 commits intolaurent22:devfrom
Ashutoshx7:fix-11805-global-hotkey
Open

Desktop: Fixes #11805: Add global shortcut to show/hide Joplin#15013
Ashutoshx7 wants to merge 6 commits intolaurent22:devfrom
Ashutoshx7:fix-11805-global-hotkey

Conversation

@Ashutoshx7
Copy link
Copy Markdown
Contributor

@Ashutoshx7 Ashutoshx7 commented Apr 4, 2026

AI disclosure
ai was used to help me in testiing the pr and writing the test

Fixes #11805

This adds a configurable global hotkey to show/hide the Joplin window from anywhere on the desktop. Users can set their preferred shortcut from Settings using a custom key recorder input which Uses Electron's globalShortcut module and includes unit tests.

Screencast.from.2026-04-05.00-15-30.mp4

Reviewe note
This feature works fine on windows /macos / linux ( x11) but on linux( wayland) it not cause wayland doesn't allow global shortcut so on wayland this feature will remain hidden ( wayland detection logic )

Screencast.from.2026-04-05.01-25-17.mp4

recording after switching to wayland )

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a desktop global-hotkey feature: new setting metadata, a UI control with tests and styles, bridge methods to register/unregister Electron global shortcuts, middleware to apply setting changes, and app shutdown cleanup to unregister the hotkey.

Changes

Cohort / File(s) Summary
Ignore Patterns
/.eslintignore, /.gitignore
Added ignores for build/test artifacts: packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.js, packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.js.
Settings Metadata
packages/lib/models/settings/builtInMetadata.ts
Added new desktop-only globalHotkey setting (string, default '', file storage, isGlobal: true, autoSave: true) with visibility logic that hides it for Wayland sessions on Linux.
Bridge & App Backend
packages/app-desktop/bridge.ts, packages/app-desktop/app.ts, packages/app-desktop/ElectronAppWrapper.ts
Bridge: added registeredGlobalHotkey_, updateGlobalHotkey(accelerator) and unregisterGlobalHotkey() using Electron globalShortcut and a toggle-window handler. Middleware: calls bridge().updateGlobalHotkey on relevant setting updates. ElectronAppWrapper: calls bridge().unregisterGlobalHotkey() in before-quit.
Settings UI & Tests
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx, packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.tsx
New GlobalHotkeyInput React component (display and recording modes) with accelerator normalisation (Cmd/Ctrl → CommandOrControl), clear/save/cancel flows; tests cover display, clear and recording interactions.
Settings Integration & Styles
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx, packages/app-desktop/gui/ConfigScreen/styles/global-hotkey-input.scss, packages/app-desktop/gui/ConfigScreen/styles/index.scss
Registered the custom control for globalHotkey (widened control map typing) and added/imported SCSS rules for .global-hotkey-input and its child elements.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as GlobalHotkeyInput
    participant Settings as SettingComponent
    participant Middleware as AppMiddleware
    participant Bridge as Bridge
    participant Electron as Electron API

    User->>UI: Click "Record" and press keys
    UI->>Settings: props.onChange({ value: accelerator })
    Settings->>Middleware: Dispatch SETTING_UPDATE_ONE / SETTING_UPDATE_ALL
    Middleware->>Bridge: bridge().updateGlobalHotkey(accelerator)
    Bridge->>Bridge: unregisterGlobalHotkey() (if set)
    Bridge->>Electron: globalShortcut.register(accelerator, handler)
    Electron-->>Bridge: registration result
    Bridge->>Bridge: store registeredGlobalHotkey_
Loading
sequenceDiagram
    participant ElectronProcess as Electron
    participant Wrapper as ElectronAppWrapper
    participant Bridge as Bridge
    participant ElectronAPI as Electron API

    ElectronProcess->>Wrapper: before-quit
    Wrapper->>Bridge: bridge().unregisterGlobalHotkey()
    Bridge->>ElectronAPI: globalShortcut.unregister(accelerator)
    ElectronAPI-->>Bridge: unregistered / error
Loading

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Pr Description Must Follow Guidelines ❌ Error PR description lacks clear problem statement, high-level solution overview, and detailed test plan. It reads as technical implementation details rather than following the required problem-solution-verification structure. Revise PR description to explicitly include: problem statement explaining user need for global hotkey, high-level solution overview, and detailed test plan with verification steps for reviewers.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding a global shortcut feature to show/hide Joplin, directly addressing issue #11805.
Description check ✅ Passed The description is related to the changeset, explaining the feature implementation, platform support, Wayland limitations, and including an AI disclosure.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #11805: creates a configurable global hotkey command, provides a custom UI component (GlobalHotkeyInput) using ShortcutRecorder, integrates with Electron's globalShortcut module, includes unit tests, and handles Wayland platform detection.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the global hotkey feature: new component files, settings metadata, bridge integration, middleware updates, styles, ignore file updates, and ShortcutRecorder enhancement. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added enhancement Feature requests and code enhancements desktop All desktop platforms labels Apr 4, 2026
@Ashutoshx7 Ashutoshx7 force-pushed the fix-11805-global-hotkey branch from f7b98d5 to d911778 Compare April 4, 2026 19:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx (1)

6-11: Use a concrete callback type instead of Function.

Function loses type safety for the settings event payload. Please type onChange explicitly (for example { value: string }) to keep this control contract strict.

♻️ Suggested typing refinement
 interface Props {
-	value: unknown;
+	value: string;
 	themeId: number;
-	// eslint-disable-next-line `@typescript-eslint/ban-types` -- Matches settingKeyToControl interface
-	onChange: Function;
+	onChange: (event: { value: string }) => void;
 }
@@
-	const value = (props.value as string) || '';
+	const value = props.value || '';
As per coding guidelines: `**/*.{ts,tsx}`: Use proper TypeScript types and avoid `any`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx` around
lines 6 - 11, The Props interface currently uses the unsafe Function type for
onChange; replace it with a concrete callback signature such as onChange:
(payload: { value: string }) => void (and update value: unknown to value: string
if appropriate) in GlobalHotkeyInput.tsx so the control contract is strictly
typed; update any call sites to match the new signature and adjust imports/types
if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx`:
- Around line 6-11: The Props interface currently uses the unsafe Function type
for onChange; replace it with a concrete callback signature such as onChange:
(payload: { value: string }) => void (and update value: unknown to value: string
if appropriate) in GlobalHotkeyInput.tsx so the control contract is strictly
typed; update any call sites to match the new signature and adjust imports/types
if needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff5db50e-639c-480f-a3b3-476860ac3d31

📥 Commits

Reviewing files that changed from the base of the PR and between f7b98d5 and d911778.

📒 Files selected for processing (10)
  • .eslintignore
  • .gitignore
  • packages/app-desktop/ElectronAppWrapper.ts
  • packages/app-desktop/app.ts
  • packages/app-desktop/bridge.ts
  • packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx
  • packages/app-desktop/gui/ConfigScreen/styles/global-hotkey-input.scss
  • packages/app-desktop/gui/ConfigScreen/styles/index.scss
✅ Files skipped from review due to trivial changes (4)
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx
  • .eslintignore
  • packages/app-desktop/gui/ConfigScreen/styles/global-hotkey-input.scss
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/app-desktop/app.ts
  • packages/app-desktop/ElectronAppWrapper.ts
  • packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.tsx

// Should exit recording mode
expect(screen.getByText('Record shortcut')).toBeTruthy();
expect(onChange).not.toHaveBeenCalled();
});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's too many tests, please reduce them to something more sensible

interface Props {
value: unknown;
themeId: number;
// eslint-disable-next-line @typescript-eslint/ban-types -- Matches settingKeyToControl interface
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Types of new component need to be strict, please give it a proper type

Comment on lines +78 to +84
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- props.onChange is typed as Function (required by settingKeyToControl interface)
}, [recording, props.onChange]);

const onClearClick = useCallback(() => {
props.onChange({ value: '' });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- props.onChange is typed as Function (required by settingKeyToControl interface)
}, [props.onChange]);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make it work without this

@JGCarroll
Copy link
Copy Markdown

How come this doesn't work on Wayland? Which distributions and desktop environments have you tried?

I'd expect there's some environments where this could work - https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html exists and the Electron documentation implies Electron would make use of it where available. Reasonably this could work on Gnome 48+ for example.

@Ashutoshx7
Copy link
Copy Markdown
Contributor Author

Ashutoshx7 commented Apr 6, 2026

How come this doesn't work on Wayland? Which distributions and desktop environments have you tried?

I'd expect there's some environments where this could work - https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html exists and the Electron documentation implies Electron would make use of it where available. Reasonably this could work on Gnome 48+ for example.

i am on ubuntu 24 lts ( wayland genome 46)
yes wayland don’t support global shortcuts by security reasons in their protocols.
but there is workaround i could find don't know if that is good direction to go on wayland https://gitlab.com/AndrewShark/obs-scripts

@JGCarroll
Copy link
Copy Markdown

You'd possibly find this works fine on Ubuntu 25.10, Ubuntu 24.04 is using Gnome 46 which doesn't support the API above; by the time Joplin 3.6 releases the latest Ubuntu LTS will be 26.04 giving people on Ubuntu an avenue to upgrade to get the support. Other distributions like Fedora, Arch, and even the latest Debian would already have this in their most common setups.

@Ashutoshx7
Copy link
Copy Markdown
Contributor Author

You'd possibly find this works fine on Ubuntu 25.10, Ubuntu 24.04 is using Gnome 46 which doesn't support the API above; by the time Joplin 3.6 releases the latest Ubuntu LTS will be 26.04 giving people on Ubuntu an avenue to upgrade to get the support. Other distributions like Fedora, Arch, and even the latest Debian would already have this in their most common setups.

hy i shifted to Gnome50 ( ubuntu 26 ( beta) )
ok so gnome 50 does have org.freedesktop.portal.globalShortcuts available but electron 40 ( which joplin uses ) doesn't uses the portal ( globalshortcut.register() ) and still returns false on wayland.

…trim tests

Addressed review feedback:
- Reuse the existing ShortcutRecorder component instead of a custom recorder
- Use strict types throughout, removed all eslint-disable comments
- Reduced tests from 14 to 3 focused scenarios
- Restored Wayland detection to hide the setting since Electron's
  globalShortcut API does not work under Wayland yet
@coderabbitai coderabbitai bot added linux macOS windows and removed enhancement Feature requests and code enhancements desktop All desktop platforms labels Apr 6, 2026
@coderabbitai coderabbitai bot added enhancement Feature requests and code enhancements desktop All desktop platforms and removed linux macOS windows labels Apr 6, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx (1)

41-45: Comment and behaviour are out of sync in onError.

The comment says “log only”, but the handler is intentionally a no-op. Please either log the error or update the comment to reflect current behaviour.

Suggested fix (comment-only)
-	// Validation errors aren't critical for global shortcuts — log only.
+	// Validation errors aren't critical for global shortcuts.
 	const onError = useCallback((_event: { recorderError: Error }) => {
-		// No-op: ShortcutRecorder validates against the keymap (command
+		// No-op: ShortcutRecorder validates against the keymap (command
 		// conflicts), which doesn't apply to global hotkeys.
 	}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx` around
lines 41 - 45, The comment and implementation of onError in
GlobalHotkeyInput.tsx are inconsistent: the comment says “log only” but onError
is a no-op; update the implementation to match the comment by logging the
incoming error (e.g., include recorderError details via your app logger) or
change the comment to explicitly state that onError is a deliberate no-op
because ShortcutRecorder handles validation and global hotkeys are exempt;
modify the onError handler or its comment accordingly and ensure you reference
the ShortcutRecorder validation behavior in the comment if you keep the no-op.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx`:
- Around line 41-45: The comment and implementation of onError in
GlobalHotkeyInput.tsx are inconsistent: the comment says “log only” but onError
is a no-op; update the implementation to match the comment by logging the
incoming error (e.g., include recorderError details via your app logger) or
change the comment to explicitly state that onError is a deliberate no-op
because ShortcutRecorder handles validation and global hotkeys are exempt;
modify the onError handler or its comment accordingly and ensure you reference
the ShortcutRecorder validation behavior in the comment if you keep the no-op.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b2c3dad-211f-4608-b11b-3424a58da331

📥 Commits

Reviewing files that changed from the base of the PR and between 99d0d7f and 8cda073.

📒 Files selected for processing (2)
  • packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx (1)

22-30: Consider renaming the shadowed variable for clarity.

The accelerator constant shadows event.accelerator in the same scope. While this works correctly, a more descriptive name like normalizedAccelerator would improve readability.

♻️ Suggested change
 	const onSave = useCallback((event: { commandName: string; accelerator: string }) => {
 		// Normalize platform-specific modifiers to CommandOrControl for
 		// consistent cross-platform storage.
-		const accelerator = event.accelerator
+		const normalizedAccelerator = event.accelerator
 			.replace(/\bCmd\b/, 'CommandOrControl')
 			.replace(/\bCtrl\b/, 'CommandOrControl');
-		props.onChange({ value: accelerator });
+		props.onChange({ value: normalizedAccelerator });
 		setEditing(false);
 	}, [props.onChange]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx` around
lines 22 - 30, The onSave callback currently declares a local const accelerator
that shadows event.accelerator; rename it to a clearer identifier (e.g.,
normalizedAccelerator) to avoid shadowing and improve readability, then use
normalizedAccelerator when calling props.onChange({ value: ... }) and keep the
existing normalization logic and setEditing(false) behavior intact in the onSave
function.
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.tsx (1)

35-37: Consider adding skipKeymapValidation to the useEffect dependency array.

If skipKeymapValidation were ever to change during the component's lifecycle, the effect would not re-run and could leave the validation state inconsistent. While currently the prop is stable, adding it to the dependency array would be more robust and satisfy exhaustive-deps expectations.

♻️ Suggested change
 		// eslint-disable-next-line `@seiyab/react-hooks/exhaustive-deps` -- Old code before rule was applied
-	}, [accelerator]);
+	}, [accelerator, skipKeymapValidation, commandName]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.tsx` around lines 35 -
37, The effect in ShortcutRecorder.tsx calls keymapService.validateKeymap({
accelerator, command: commandName }) guarded by skipKeymapValidation but
skipKeymapValidation is not included in the useEffect dependency list; update
the useEffect hook to include skipKeymapValidation (alongside accelerator and
commandName) so the effect re-runs when that flag changes, keeping validation
state consistent and satisfying exhaustive-deps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx`:
- Around line 22-30: The onSave callback currently declares a local const
accelerator that shadows event.accelerator; rename it to a clearer identifier
(e.g., normalizedAccelerator) to avoid shadowing and improve readability, then
use normalizedAccelerator when calling props.onChange({ value: ... }) and keep
the existing normalization logic and setEditing(false) behavior intact in the
onSave function.

In `@packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.tsx`:
- Around line 35-37: The effect in ShortcutRecorder.tsx calls
keymapService.validateKeymap({ accelerator, command: commandName }) guarded by
skipKeymapValidation but skipKeymapValidation is not included in the useEffect
dependency list; update the useEffect hook to include skipKeymapValidation
(alongside accelerator and commandName) so the effect re-runs when that flag
changes, keeping validation state consistent and satisfying exhaustive-deps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66d1af16-714d-4034-b4ef-18e5f3cb8570

📥 Commits

Reviewing files that changed from the base of the PR and between 8cda073 and ee89241.

📒 Files selected for processing (6)
  • packages/app-desktop/app.ts
  • packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.tsx
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx
  • packages/app-desktop/gui/ConfigScreen/styles/global-hotkey-input.scss
  • packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.tsx
  • packages/lib/models/settings/builtInMetadata.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/app-desktop/gui/ConfigScreen/styles/global-hotkey-input.scss
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/app-desktop/app.ts
  • packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.tsx
  • packages/lib/models/settings/builtInMetadata.ts

@JGCarroll
Copy link
Copy Markdown

i shifted to Gnome50 ( ubuntu 26 ( beta) ) ok so gnome 50 does have org.freedesktop.portal.globalShortcuts available but electron 40 ( which joplin uses ) doesn't uses the portal ( globalshortcut.register() ) and still returns false on wayland.

Looks like it might be gated behind a feature flag for now
electron/electron#45171

Release Notes

Notes: support Portal's globalShortcuts. Electron must be run with --enable-features=GlobalShortcutsPortal in order to have the feature working.

So I guess for now it's worth leaving as is but in the future it sounds like we could expect an Electron version bump to sort out Wayland global shortcuts for people on modern systems

Comment on lines +62 to +84
return (
<div className="global-hotkey-input">
<span className="shortcut-display">
{value || _('Not set')}
</span>
<button
className="record-btn"
onClick={() => setEditing(true)}
type="button"
>
{_('Change')}
</button>
{value && (
<button
className="clear-btn"
onClick={() => props.onChange({ value: '' })}
type="button"
>
{_('Clear')}
</button>
)}
</div>
);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShortcutRecorded already has these features. Is it not possible to use it for this too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop All desktop platforms enhancement Feature requests and code enhancements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for global hotkey to show and hide Joplin

3 participants