mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-15 19:29:08 +02:00
[client] Add light mode with system, light, and dark theme options (#7344)
* desktop UI light mode * Theme review fixes plus macOS window outline fix * Windows runtime chrome re-theming plus apply serialization * Windows chrome threading and theme event ordering fixes * Darken toggle and setting sidebar text * resolve theme appearance, apply on UI thread * read theme once per window * Re-assert Windows dark opt-in after SetTheme * split app-wide GTK theming from per-window chrome * Update Wails dependency and checksums * KDE tray icon panel fix * Five review fixes: theme ordering, cgo dedup, KDE panel resolution * Path guard hardening, toggle contrast, windows comment * non-vacuous escape tests * Default view edits * Polish settings nav, controls, borders, and disc * Profiles settings boarder, modals, and buttons * Additional edits based on feedback * Switch colors away from slight blue hue * Update missing lang * Fix vertical tab active view
This commit is contained in:
+123
-24
@@ -17,6 +17,13 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// The two KDE files that decide the panel's appearance. Both live in the user
|
||||
// config dir, so one directory watch covers them (see watchKdeConfig).
|
||||
const (
|
||||
kdeglobalsFile = "kdeglobals"
|
||||
plasmarcFile = "plasmarc"
|
||||
)
|
||||
|
||||
// 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() {
|
||||
@@ -35,50 +42,142 @@ func isKDE() bool {
|
||||
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 {
|
||||
// kdeConfigPath locates one of KDE's user config files. We read only the user
|
||||
// file, not the full XDG_CONFIG_DIRS cascade: Plasma writes the active scheme
|
||||
// and style there, and anything missing falls back to the portal.
|
||||
func kdeConfigPath(name string) string {
|
||||
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
|
||||
return filepath.Join(dir, "kdeglobals")
|
||||
return filepath.Join(dir, name)
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".config", "kdeglobals")
|
||||
return filepath.Join(home, ".config", name)
|
||||
}
|
||||
|
||||
// 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 kdeglobalsPath() string { return kdeConfigPath(kdeglobalsFile) }
|
||||
func plasmarcPath() string { return kdeConfigPath(plasmarcFile) }
|
||||
|
||||
// kdePanelIsDark reports whether the KDE Plasma panel the tray icon sits on is
|
||||
// dark. ok is false when this isn't KDE or neither source was conclusive, 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
|
||||
// A pinned Plasma style paints the panel itself, so it outranks the
|
||||
// application colour scheme: a style with dark colours under a Light scheme
|
||||
// is still a dark panel and still needs the white silhouette.
|
||||
if dark, ok := plasmaStyleIsDark(); ok {
|
||||
return dark, true
|
||||
}
|
||||
rgb, ok := readKdeComplementaryBackground(path)
|
||||
// The default style follows the colour scheme, so decide by the luma of the
|
||||
// window background Plasma derives the panel from. Deliberately not
|
||||
// Complementary: that group is dark under Breeze *and* BreezeLight
|
||||
// (42,46,50 measured on Plasma 6.7.4), so reading it kept the white icon on
|
||||
// a light panel, where it is all but invisible.
|
||||
rgb, ok := readKdeColour(kdeglobalsPath(), "[Colors:Window]")
|
||||
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) {
|
||||
// plasmaStyleIsDark reports the appearance the pinned Plasma style paints the
|
||||
// panel with, decided by the style's own colours rather than by its name. A
|
||||
// style that ships a colours file overrides the colour scheme for the shell, and
|
||||
// nothing requires the name to admit it: breeze-dark happens to, but a style
|
||||
// named neutrally can carry dark colours just as well (measured on Plasma 6.7.4:
|
||||
// panel luma 38 while kdeglobals and the portal both reported light).
|
||||
//
|
||||
// ok is false when no style is pinned, when the pinned style ships no colours --
|
||||
// the "default" style, which is exactly the case that follows the colour scheme
|
||||
// -- and when its colours cannot be read; all three fall through to kdeglobals.
|
||||
func plasmaStyleIsDark() (dark, ok bool) {
|
||||
name, found := readIniValue(plasmarcPath(), "[Theme]", "name")
|
||||
if !found || name == "" {
|
||||
return false, false
|
||||
}
|
||||
if !isBareStyleName(name) {
|
||||
log.Debugf("tray theme: ignoring plasma style name %q, not a bare directory name", name)
|
||||
return false, false
|
||||
}
|
||||
for _, dir := range plasmaStyleDirs(name) {
|
||||
rgb, found := readKdeColour(filepath.Join(dir, "colors"), "[Colors:Window]")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
return isDarkRGB(rgb[0], rgb[1], rgb[2]), true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// isBareStyleName reports whether name is safe to index a directory with. The
|
||||
// name comes from a config file and is joined into a path, so it has to be a
|
||||
// single ordinary element: "../" in it would point the read anywhere, and ".",
|
||||
// ".." and "/" all survive filepath.Base(filepath.Clean(name)) unchanged, so
|
||||
// they need rejecting by name -- ".." alone resolves a level above desktoptheme.
|
||||
func isBareStyleName(name string) bool {
|
||||
if name == "" || name == "." || name == ".." || filepath.IsAbs(name) {
|
||||
return false
|
||||
}
|
||||
return name == filepath.Base(filepath.Clean(name))
|
||||
}
|
||||
|
||||
// plasmaStyleDirs lists where a Plasma style of that name may live, in the order
|
||||
// Plasma itself resolves them: the user data dir first, so a local style shadows
|
||||
// a system one of the same name.
|
||||
func plasmaStyleDirs(name string) []string {
|
||||
var dirs []string
|
||||
for _, base := range xdgDataDirs() {
|
||||
dirs = append(dirs, filepath.Join(base, "plasma", "desktoptheme", name))
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// xdgDataDirs returns XDG_DATA_HOME (or its default) followed by XDG_DATA_DIRS.
|
||||
func xdgDataDirs() []string {
|
||||
var dirs []string
|
||||
if home := os.Getenv("XDG_DATA_HOME"); home != "" {
|
||||
dirs = append(dirs, home)
|
||||
} else if h, err := os.UserHomeDir(); err == nil {
|
||||
dirs = append(dirs, filepath.Join(h, ".local", "share"))
|
||||
}
|
||||
system := os.Getenv("XDG_DATA_DIRS")
|
||||
if system == "" {
|
||||
system = "/usr/local/share:/usr/share"
|
||||
}
|
||||
for _, dir := range strings.Split(system, ":") {
|
||||
if dir != "" {
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// readKdeColour reads group's BackgroundNormal as an R,G,B triple.
|
||||
func readKdeColour(path, group string) (rgb [3]uint8, ok bool) {
|
||||
val, found := readIniValue(path, group, "BackgroundNormal")
|
||||
if !found {
|
||||
return rgb, false
|
||||
}
|
||||
return parseRGB(val)
|
||||
}
|
||||
|
||||
// readIniValue returns key's value inside group from a KDE-style INI file.
|
||||
// group carries its own brackets, e.g. "[Colors:Window]".
|
||||
func readIniValue(path, group, key string) (value string, ok bool) {
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Debugf("tray theme: kdeglobals open failed, using portal: %v", err)
|
||||
return rgb, false
|
||||
log.Debugf("tray theme: %s open failed, using portal: %v", filepath.Base(path), err)
|
||||
return "", false
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
const group = "[Colors:Complementary]"
|
||||
inGroup := false
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
@@ -90,13 +189,13 @@ func readKdeComplementaryBackground(path string) (rgb [3]uint8, ok bool) {
|
||||
if !inGroup {
|
||||
continue
|
||||
}
|
||||
key, val, found := strings.Cut(line, "=")
|
||||
if !found || strings.TrimSpace(key) != "BackgroundNormal" {
|
||||
k, v, found := strings.Cut(line, "=")
|
||||
if !found || strings.TrimSpace(k) != key {
|
||||
continue
|
||||
}
|
||||
return parseRGB(strings.TrimSpace(val))
|
||||
return strings.TrimSpace(v), true
|
||||
}
|
||||
return rgb, false
|
||||
return "", false
|
||||
}
|
||||
|
||||
// parseRGB parses KDE's "r,g,b" colour triple into bytes.
|
||||
|
||||
Reference in New Issue
Block a user