Files
netbird/client/ui/tray_theme_linux.go
Maycon Santos 91acb8147c [management,client] 0.75.0 release with new desktop UI (#6473)
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow.
- **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend.
- **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher.
- **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements.
- **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms.
- **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows).
- **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow.

Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
Co-authored-by: braginini <bangvalo@gmail.com>
Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com>
Co-authored-by: riccardom <riccardomanfrin@gmail.com>
2026-07-06 13:47:16 +02:00

135 lines
3.8 KiB
Go

//go:build linux && !(linux && 386)
package main
// Wails v3's Linux SNI backend ignores SetDarkModeIcon (it just calls setIcon,
// last write wins) and SNI carries no panel dark/light hint, so we detect the
// desktop colour scheme ourselves and pick the silhouette in iconForState.
// The live watcher is in tray_theme_watcher_linux.go.
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
// startTrayTheme seeds t.panelDark and repaints on colour-scheme flips. Must
// run before the first applyIcon so the initial paint uses the right silhouette.
func (t *Tray) startTrayTheme() {
w := startThemeWatcher(func() { t.applyIcon() })
t.panelDark = w.IsDark
}
// isKDE reports whether the current desktop is KDE Plasma. XDG_CURRENT_DESKTOP
// is a colon-separated list (e.g. "ubuntu:KDE"), so match per token.
func isKDE() bool {
for _, d := range strings.Split(os.Getenv("XDG_CURRENT_DESKTOP"), ":") {
if strings.EqualFold(strings.TrimSpace(d), "KDE") {
return true
}
}
return false
}
// kdeglobalsPath returns the user kdeglobals path. We read only this file, not
// the full XDG_CONFIG_DIRS cascade: Plasma writes the active scheme here, and a
// missing Complementary group falls back to the portal.
func kdeglobalsPath() string {
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
return filepath.Join(dir, "kdeglobals")
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "kdeglobals")
}
// kdePanelIsDark reports whether the KDE Plasma panel is dark by the luma of
// its "Complementary" background (the colour Plasma paints the tray with). ok
// is false when this isn't KDE or the colour can't be read, so the caller falls
// through to the portal/GTK path.
func kdePanelIsDark() (dark, ok bool) {
if !isKDE() {
return false, false
}
path := kdeglobalsPath()
if path == "" {
return false, false
}
rgb, ok := readKdeComplementaryBackground(path)
if !ok {
return false, false
}
return isDarkRGB(rgb[0], rgb[1], rgb[2]), true
}
// readKdeComplementaryBackground parses kdeglobals for
// [Colors:Complementary] BackgroundNormal and returns its R,G,B (0-255).
func readKdeComplementaryBackground(path string) (rgb [3]uint8, ok bool) {
f, err := os.Open(path)
if err != nil {
log.Debugf("tray theme: kdeglobals open failed, using portal: %v", err)
return rgb, false
}
defer func() { _ = f.Close() }()
const group = "[Colors:Complementary]"
inGroup := false
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[") {
inGroup = line == group
continue
}
if !inGroup {
continue
}
key, val, found := strings.Cut(line, "=")
if !found || strings.TrimSpace(key) != "BackgroundNormal" {
continue
}
return parseRGB(strings.TrimSpace(val))
}
return rgb, false
}
// parseRGB parses KDE's "r,g,b" colour triple into bytes.
func parseRGB(s string) (rgb [3]uint8, ok bool) {
parts := strings.Split(s, ",")
if len(parts) != 3 {
return rgb, false
}
for i, p := range parts {
n, err := strconv.Atoi(strings.TrimSpace(p))
if err != nil || n < 0 || n > 255 {
return rgb, false
}
rgb[i] = uint8(n)
}
return rgb, true
}
// isDarkRGB reports whether a colour is dark via Rec. 601 luma, split at the
// 128 midpoint.
func isDarkRGB(r, g, b uint8) bool {
luma := (299*int(r) + 587*int(g) + 114*int(b)) / 1000
return luma < 128
}
// gtkThemeIsDark inspects the GTK_THEME env var. Empty (no override) is treated
// as dark to match the default-dark fallback used elsewhere.
func gtkThemeIsDark() bool {
theme := os.Getenv("GTK_THEME")
if theme == "" {
return true
}
// GTK_THEME is "Name[:variant]"; the dark variant is ":dark".
return strings.Contains(strings.ToLower(theme), ":dark")
}