Skip to content

internal/tray: add profile switching submenu to tray menu#276

Open
alex-poor wants to merge 1 commit intoDeedleFake:masterfrom
alex-poor:feature/tray-profile-switching
Open

internal/tray: add profile switching submenu to tray menu#276
alex-poor wants to merge 1 commit intoDeedleFake:masterfrom
alex-poor:feature/tray-profile-switching

Conversation

@alex-poor
Copy link
Copy Markdown

@alex-poor alex-poor commented Apr 2, 2026

Summary

Adds a "Switch account" submenu to the system tray right-click menu, allowing the user to switch between Tailscale profiles without opening the main window. The currently active profile is shown with a bullet character (`●`) prefix, and the indicator updates whether the switch is initiated from the tray submenu or from the main window's profile dropdown.

Profiles are fetched once at tray startup via `tsutil.GetProfileStatus` and the submenu is populated in `Tray.Start`. The submenu only appears when there is more than one profile available.

Dependency on tray library fix

This PR depends on DeedleFake/tray#2, which fixes three bugs in `deedles.dev/tray` that were preventing this from working:

  1. `MenuItem.AddChild` was calling `emitPropertiesUpdated` on the parent instead of the child, causing a nil pointer dereference and dropping the child's property notifications.
  2. `MenuItem.setChildren` was mutating `children-display` without emitting it, so the desktop environment never learned that an item had become a submenu.
  3. `MenuItem.SetProps` had a lock-order inversion with `dbusmenu.GetLayout` that caused a hard deadlock whenever `SetProps` was called concurrently with the DE fetching the menu layout.

While that PR is in flight, the trayscale `go.mod` has a temporary `replace` directive pointing at my fork at the relevant commit:

```
replace deedles.dev/tray => github.com/alex-poor/tray v0.1.11-0.20260407004037-2451d98fc544
```

Once the upstream tray PR is merged, the `replace` directive should be dropped and the regular `deedles.dev/tray` requirement updated to a new pseudo-version.

Implementation notes

  • The "Switch account" submenu must be the first item added to the menu — there is some interaction between the submenu's children and sibling items added to the root menu after it that I wasn't able to fully chase down. The trayscale code includes a comment explaining this constraint.
  • `SetActiveProfile` updates the indicator labels using `SetProps`. It is called from a goroutine so that D-Bus emits don't run on the GTK main thread, and it serializes itself with `Tray.m` so concurrent indicator updates can't interleave and leave two items marked active.
  • `Tray.Start` now takes an optional `*tsutil.ProfileStatus` so the submenu can be built in the same code path as all the other menu items, avoiding the need to mutate the menu after `Start` returns.

Test plan

  • Tray menu shows "Switch account" submenu when more than one profile is configured
  • Tray menu does not show the submenu when only one profile is configured
  • Switching via the tray submenu actually switches profiles and the bullet indicator follows
  • Switching via the main window profile dropdown also updates the tray bullet indicator
  • No deadlocks or freezes during the disconnect/reconnect cycle that follows a profile switch
  • Respects `TRAYSCALE_PRIVATE` mode (profile names shown as `profile@example.com`)

🤖 Generated with Claude Code

@DeedleFake
Copy link
Copy Markdown
Owner

Thank you for the pull request. Overall I like it, but I'd prefer that the profile list go into a submenu. Could you update it to do that, please?

@DeedleFake DeedleFake self-assigned this Apr 6, 2026
Allow switching Tailscale profiles directly from the system tray
right-click menu, without needing to open the main window. The active
profile is shown with a bullet indicator that updates whether the
switch is initiated from the tray submenu or from the main window's
profile dropdown.

Depends on DeedleFake/tray#2, which fixes three bugs in the tray
library that were preventing submenu children from rendering and
causing a deadlock when SetProps is called concurrently with the
desktop environment's GetLayout requests. The temporary replace
directive in go.mod should be removed once that PR is merged and
deedles.dev/tray is updated to a new pseudo-version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alex-poor alex-poor force-pushed the feature/tray-profile-switching branch from 7186e56 to cd5bd4a Compare April 7, 2026 00:44
@alex-poor alex-poor changed the title internal/tray: add profile switching to tray menu internal/tray: add profile switching submenu to tray menu Apr 7, 2026
@alex-poor
Copy link
Copy Markdown
Author

Force-pushed an updated version of this PR that uses a submenu (per your earlier feedback) and includes the `SetActiveProfile` indicator-sync logic. Getting the submenu to render correctly required three upstream tray library fixes — see DeedleFake/tray#2 — and the trayscale `go.mod` currently has a temporary `replace` directive pointing at my fork while that lands.

I've updated the PR description with the full context.

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.

2 participants