Skip to content

feat: Allow toggling hosts directly from tray menu - #1052

Closed
sirbrillig wants to merge 3 commits into
oldj:masterfrom
sirbrillig:tray-menu-hosts-toggles
Closed

sirbrillig wants to merge 3 commits into
oldj:masterfrom
sirbrillig:tray-menu-hosts-toggles

Conversation

@sirbrillig

@sirbrillig sirbrillig commented Sep 17, 2026 •

Copy link
Copy Markdown

Proposed changes

This PR adds per-entry toggles to the tray menu, so a hosts configuration can be switched straight from the menu bar without opening a window.

Clicking an item runs the apply pipeline natively in Rust rather than delegating to the renderer. I also added a preference that lets anyone turn off the list if they like.

Screenshot 2026-09-17 at 4 05 13 PM Screenshot 2026-09-17 at 4 04 47 PM

Why are these changes being made?

Switching between host configurations is the main reason for this app but since version 5 that requires opening the main window which is a lot of extra clicks for something I need to do many times a day.

The interesting decision is doing the work in Rust instead of emitting toggle_item and letting the renderer's existing onToggleItem handle it, which is what http_api::api_toggle does. That would have been a much smaller change, but the menu bar is reachable in states where no renderer is: under lightweight_mode closing the main window destroys its webview, and hide_at_launch never creates one.

Testing instructions

Automated:

npm run typecheck
npm run test:unit
npm run test:rust
npm run test:e2e

New coverage: 10 Rust tests for the setOnStateOfItem port (single/multi choice mode, folder cascading, parent roll-up), 3 for the menu id/label helpers, and an e2e test for the new preference. The port was also cross-checked against the TypeScript original by running both over the same 10 scenarios.

Manual testing:

  • npm install && npm run tauri:dev
  • Open the tray menu (right-click the icon on macOS/Windows). Entries should be listed with check marks matching their state; folders should appear as submenus.
  • Click an entry. After the auth prompt, /etc/hosts should be updated and the main window's list, the tray title and the menu's check marks should all agree.
  • Click an entry and dismiss the auth prompt. Nothing should change, and the check mark should return to its previous state.
  • With Preferences → single-choice mode, toggling one entry should switch the others off, exactly as it does in the list.
  • Rename, add or delete an entry in the app, then reopen the menu — it should reflect the change.
  • Preferences → Advanced → uncheck "Switch Hosts from the Tray Menu". The entries should disappear from the menu immediately, without a restart; re-checking brings them back.
  • Optional: enable Lightweight mode, close the main window, then toggle from the menu. It should still apply.

Note for the maintainer

http_api::api_toggle (GET /api/toggle, used by the bundled Alfred workflow) still emits toggle_item for the renderer to handle, so it is a silent no-op under lightweight_mode / hide_at_launch. The documentation at the top of http_api.rs is not accurate. Now that hosts_toggle::toggle_item exists it could be changed to be a few lines and work even if the window is closed.

`apply_hosts_selection` mixed argument handling with the apply
pipeline itself — the privileged write, the history journal, the
`system_hosts_updated` broadcast, the tray title refresh and
`cmd_after_hosts_apply`. Only the renderer could reach any of it.

Extract everything after the argument check into
`apply_content_to_system`, leaving the command as the thin shell that
validates `args[0]` and delegates. No behaviour change: the helper
returns the same JSON envelope the renderer has always received.
The tray menu could only open the main window, so switching a
configuration always meant bringing up a window first. It now lists
every hosts entry as a check item — folders become submenus, with the
folder's own toggle at the top since a submenu parent isn't clickable.

Clicking an item runs the whole pipeline in Rust rather than emitting
`toggle_item` for the renderer the way `http_api::api_toggle` does.
The menu bar is reachable in states where no renderer is: under
`lightweight_mode` closing the main window destroys its webview, and
`hide_at_launch` never creates one. A menu-bar toggle that did nothing
in exactly the "app lives in the tray" setup it exists for would be
useless.

`hosts_toggle::set_on_state_of_item` ports `setOnStateOfItem` from
`src/common/hostsFn.ts`, so `choice_mode`, per-folder `folder_mode`
and `multi_chose_folder_switch_all` cascade the same from the menu as
from the list. The apply reuses `apply_content_to_system`, so both
paths journal history and broadcast identically. `manifest.json` is
written only after a successful apply — a dismissed auth prompt leaves
the stored selection alone — and the menu is rebuilt either way, since
the OS flips a check item's mark on click and a cancelled apply would
otherwise leave the menu claiming a state `/etc/hosts` doesn't have.

Two guards match the renderer: no hosts items during data-directory
recovery, and an unset `write_mode` opens the write-mode dialog
instead of guessing between append and overwrite.
With a long list of configurations the tray menu gets unwieldy, so
not everyone wants it carrying one. Advanced preferences gains
"Switch Hosts from the Tray Menu", on by default so current
behaviour is unchanged; turning it off leaves the menu exactly as it
was before the hosts items existed.

The check lives in `tray::hosts_nodes`, next to the data-directory
recovery case that already returns an empty list, so one place decides
whether the menu lists hosts entries at all. `apply_side_effects` now
treats `show_hosts_in_tray_menu` like `locale` and rebuilds the menu
when the preference flips, so it takes effect without a restart.
@sirbrillig

Copy link
Copy Markdown
Author

Oh, I just noticed that this should be based on the develop branch. Let me redo it there.

@sirbrillig sirbrillig closed this Sep 17, 2026
@sirbrillig

Copy link
Copy Markdown
Author

Moved to #1053

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant