Skip to content

feat(settings): add "Close to tray" option#2845

Open
BlueManCZ wants to merge 1 commit intogitify-app:mainfrom
BlueManCZ:feat/close-to-tray
Open

feat(settings): add "Close to tray" option#2845
BlueManCZ wants to merge 1 commit intogitify-app:mainfrom
BlueManCZ:feat/close-to-tray

Conversation

@BlueManCZ
Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in "Close to tray" checkbox to System settings (default off). When enabled, pressing your window manager's close shortcut (e.g. Super+Q on Hyprland, Alt+F4 on Windows, Cmd+W on macOS) hides the Gitify popup instead of quitting the app — the same way Slack, Signal, and most messaging apps behave.

Why

On Linux tiling WMs (Hyprland, i3, Sway, …) users typically bind a global "close window" shortcut. Today, hitting that shortcut on Gitify's popup terminates the entire app — you lose the tray icon and have to re-launch Gitify to get notifications back. Pressing Esc or clicking outside already hides the popup gracefully; this PR makes the WM close shortcut do the same when the user opts in.

The setting is off by default, so existing behavior is preserved. Real ways to quit Gitify (tray right-click → "Quit Gitify", Ctrl/Cmd+Q) are unchanged and always work regardless of the setting.

Where it lives

Settings → System → Close to tray (right after "Open at startup"), with a tooltip explaining the behavior and how to actually quit.

Behavior matrix

Setting $MOD+Q / $ALT+F4 Esc Tray "Quit Gitify" Ctrl/Cmd+Q
Off (default) quits app hides popup quits app quits app
On hides popup hides popup quits app quits app

Implementation notes for reviewers

The two non-obvious bits are documented inline in src/main/lifecycle/window.ts. Calling them out here so the diff doesn't look surprising:

  1. Capturing the BrowserWindow reference at config time. menubar registers its own close listener (windowClear) when the window is created. It nulls mb.window regardless of preventDefault, and listeners run in registration order — so reading mb.window inside our handler returns undefined. We capture the reference at the top of configureWindowEvents and, after hiding, restore menubar's internal field so the next tray click reuses the same window (keeping renderer state — keyboard listeners, notification cache, scroll, etc.). The private-field write is wrapped in a try/catch so a future menubar refactor degrades to "fresh window per show" rather than throwing.

  2. Deferring hide() via setImmediate. On Wayland, calling hide() synchronously after preventDefault on a frameless surface can leave the window mapped but without keyboard input routing. Deferring lets the close cancellation unwind first.

  3. window-all-closed safety net. Suppresses the default Electron quit so the tray icon stays put if the WM tears the window down despite preventDefault — a known Wayland edge case ([Bug]: On Wayland closing apps to the system tray, then reopening them makes the app forget which monitor it was previously opened on (Signal, ProtonVPN, MullvadVPN) electron/electron#34788, #35657). menubar then recreates the window on the next tray click.

Test plan

  • With the setting off (default) on Linux: $MOD+Q quits the app — same as main.
  • With the setting on on Linux: $MOD+Q hides the popup; tray icon stays; clicking the tray reopens the same window with state intact (no flicker, no notification re-fetch).
  • Tray right-click → "Quit Gitify" fully exits regardless of the setting.
  • Ctrl/Cmd+Q accelerator fully exits regardless of the setting.
  • Esc still hides the popup regardless of the setting.
  • Toggling the setting persists across app restarts.
  • pnpm test passes (11 tests for lifecycle/window.ts, Settings snapshot updated, no other test changes needed).

🤖 Generated with Claude Code

Adds an opt-in System setting (default off) that hides the menubar
window on a WM-initiated close request instead of quitting the app.
Useful on Linux tiling WMs where a global "close window" shortcut
otherwise terminates Gitify.

The implementation works around two quirks documented inline in
src/main/lifecycle/window.ts:

  1. menubar's own `close` listener nulls `mb.window` regardless of
     preventDefault, and listeners run in registration order, so we
     capture the BrowserWindow at config time and restore the reference
     after hiding so the next tray click reuses the same window
     (preserving renderer state).

  2. On Wayland, `hide()` is deferred via setImmediate to let the close
     cancellation unwind — a synchronous hide can leave the surface
     mapped but without keyboard input routing.

A `window-all-closed` handler acts as a safety net: if the WM tears
down the window despite preventDefault (a known Wayland edge case,
electron/electron#34788, #35657), it suppresses the default quit so
the tray icon stays put and menubar can recreate the window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant