Merge remote-tracking branch 'origin/ui-refactor' into ui-refactor

This commit is contained in:
Eduard Gert
2026-06-11 15:39:34 +02:00
4 changed files with 51 additions and 65 deletions

View File

@@ -825,3 +825,31 @@ func TestRouteSelector_ComplexScenarios(t *testing.T) {
})
}
}
// TestRouteSelector_EnableExitNodeKeepsOtherRoutes is a regression test for the
// tray exit-node toggle disabling every non-exit routed network. The tray used
// to Select an exit node with append=false, which the RouteSelector treats as
// "drop the whole current selection" (default-on semantics) — so enabling an
// exit node also turned off every LAN/route the user had on. The fix sends
// append=true and lets the daemon's SelectNetworks handler deselect only the
// sibling exit nodes. This test models that handler sequence against the
// selector: SelectRoutes(exit, append=true) followed by DeselectRoutes(other
// exit nodes) must leave non-exit routes untouched.
func TestRouteSelector_EnableExitNodeKeepsOtherRoutes(t *testing.T) {
rs := routeselector.NewRouteSelector()
all := []route.NetID{"exitA", "exitB", "lan1", "lan2"}
// User has two LAN routes on (default-on: nothing deselected => all selected).
require.True(t, rs.IsSelected("lan1"))
require.True(t, rs.IsSelected("lan2"))
// Tray enables exitA: SelectNetworks handler does SelectRoutes(append=true)
// then deselects sibling exit nodes (exitB), never the LAN routes.
require.NoError(t, rs.SelectRoutes([]route.NetID{"exitA"}, true, all))
require.NoError(t, rs.DeselectRoutes([]route.NetID{"exitB"}, all))
assert.True(t, rs.IsSelected("exitA"), "selected exit node stays on")
assert.False(t, rs.IsSelected("exitB"), "sibling exit node is deselected")
assert.True(t, rs.IsSelected("lan1"), "non-exit route must stay selected")
assert.True(t, rs.IsSelected("lan2"), "non-exit route must stay selected")
}

View File

@@ -2,57 +2,16 @@
package main
import (
"os"
"strings"
)
// recenterOnShowPredicate returns the predicate WindowManager uses to decide
// whether to re-center its Go-shown windows (main, Settings) on each show.
//
// Re-centering is needed only on bare WMs (fluxbox, IceWM, twm …) that neither
// place small windows for us nor restore their position across a hide -> show
// round-trip — the same environment the in-process XEmbed tray host serves.
// xembedTrayAvailable (a _NET_SYSTEM_TRAY_S0 selection-owner probe) detects
// that host, but it is NOT a clean proxy for "bare WM": full desktops like
// Cinnamon, MATE, XFCE and LXDE ship a legacy XEmbed systray AND a real
// compositing WM (Muffin/Marco/xfwm4) that places windows itself. On those,
// running the post-show X11 re-center makes the window visibly jump from the
// WM's placement to ours (reported on Cinnamon/Mint/X11). So we additionally
// require that the session does NOT advertise a known full desktop.
//
// Evaluated per show (not once at startup) because the XEmbed tray can appear
// after the UI starts — the panel and the autostarted app race at login — and
// xembedTrayAvailable is a cheap, side-effect-free selection-owner probe.
// On Linux this is xembedTrayAvailable: re-centering is needed only in the
// minimal-WM / in-process-XEmbed-tray environment, where the window manager
// neither centers small windows for us nor restores their position across a
// hide -> show round-trip. The predicate is evaluated per show (not once at
// startup) because the XEmbed tray can appear after the UI starts — the panel
// and the autostarted app race at login — and xembedTrayAvailable is a cheap,
// side-effect-free selection-owner probe, fine to call repeatedly.
func recenterOnShowPredicate() func() bool {
return func() bool {
return xembedTrayAvailable() && !inFullDesktopEnvironment()
}
}
// fullDesktopTokens are desktop-environment identifiers whose window manager
// places and restores windows for us. Several also expose a legacy XEmbed
// systray, so they must be excluded from the re-center path explicitly.
// Matched case-insensitively against the colon-separated XDG_CURRENT_DESKTOP /
// DESKTOP_SESSION tokens. Bare WMs leave these unset (or report their own name,
// e.g. "Fluxbox"), which is absent here — so they still re-center.
var fullDesktopTokens = []string{
"cinnamon", "mate", "xfce", "gnome", "kde", "plasma",
"lxde", "lxqt", "unity", "budgie", "deepin", "pantheon",
}
// inFullDesktopEnvironment reports whether the session advertises one of the
// known full desktop environments via XDG_CURRENT_DESKTOP or DESKTOP_SESSION.
func inFullDesktopEnvironment() bool {
for _, env := range []string{"XDG_CURRENT_DESKTOP", "DESKTOP_SESSION"} {
for _, tok := range strings.Split(os.Getenv(env), ":") {
tok = strings.ToLower(strings.TrimSpace(tok))
for _, full := range fullDesktopTokens {
if strings.Contains(tok, full) {
return true
}
}
}
}
return false
return xembedTrayAvailable
}

View File

@@ -163,12 +163,10 @@ type WindowManager struct {
hiddenForLogin []application.Window
mu sync.Mutex
// recenterOnShow reports whether Go should re-center the Go-shown
// windows (main, Settings) on each show. Only true on bare WMs (the
// in-process XEmbed-tray environment minus the full desktops — Cinnamon,
// MATE, XFCE … — that also expose an XEmbed systray), where the WM neither
// centers small windows for us nor restores their position across a
// hide -> show round-trip. On full desktops (GNOME/KDE/Cinnamon) the WM
// handles placement, so
// windows (main, Settings) on each show. Only true in the minimal-WM /
// in-process XEmbed-tray environment, where the WM neither centers small
// windows for us nor restores their position across a hide -> show
// round-trip. On full desktops (GNOME/KDE) the WM handles placement, so
// re-centering is unnecessary and would fight a window the user moved —
// there this stays nil and centerWhenReady is a no-op. Set by the Linux
// startup path via SetRecenterOnShow; nil on macOS/Windows and in tests.
@@ -651,10 +649,9 @@ func (s *WindowManager) ShowMain() {
// SetRecenterOnShow installs the predicate that gates Go-side re-centering of
// the main and Settings windows (see the recenterOnShow field). The Linux
// startup path passes a predicate that is true only on bare WMs (XEmbed tray
// present and no full desktop advertised), so re-centering happens only where
// the WM won't place windows for us; macOS/Windows and tests leave it unset,
// making centerWhenReady a no-op.
// startup path passes xembedTrayAvailable so re-centering happens only in the
// minimal-WM / in-process-XEmbed-tray environment; macOS/Windows and tests
// leave it unset, making centerWhenReady a no-op.
func (s *WindowManager) SetRecenterOnShow(pred func() bool) {
s.recenterOnShow = pred
}

View File

@@ -97,17 +97,19 @@ func (t *Tray) refreshExitNodes() {
}
// toggleExitNode activates or deactivates one exit node by NetID. Exit nodes
// are mutually exclusive, so Select uses append=false to clear any other
// active node before turning this one on; deselecting an active node turns
// routing off entirely. Mirrors the frontend's toggleExitNode semantics. Runs
// the RPC off the menu-click goroutine and re-fetches so the ✓ moves to the
// new selection.
// are mutually exclusive, but enforcement of that lives daemon-side: the
// SelectNetworks handler deselects every other exit node when this Select
// activates one. So Select uses append=true — append=false would tell the
// RouteSelector to drop the whole current selection (default-on semantics),
// which also turns off every non-exit routed network the user had enabled.
// Mirrors the frontend's toggleExitNode semantics. Runs the RPC off the
// menu-click goroutine and re-fetches so the ✓ moves to the new selection.
func (t *Tray) toggleExitNode(id string, selected bool) {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
params := services.SelectNetworksParams{NetworkIDs: []string{id}, Append: false, All: false}
params := services.SelectNetworksParams{NetworkIDs: []string{id}, Append: true, All: false}
var err error
if selected {
err = t.svc.Networks.Deselect(ctx, params)