From 0bd3e85150df67bfa3e7dfd5e40a078281c54a70 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 21 Sep 2026 11:45:50 -0400 Subject: [PATCH] settings: fix window rules overflow, unify popout border settings cards: fix regression that created borders matugen: run on a low priority worker and seeds dms-colors before other templates --- core/internal/config/embedded/hyprland.lua | 1 - core/internal/config/embedded/niri.kdl | 1 - .../lowprio_linux.go} | 4 +- .../lowprio_other.go} | 4 +- core/internal/matugen/matugen.go | 161 ++++++++++++------ core/internal/matugen/matugen_test.go | 2 +- core/internal/matugen/queue.go | 4 + core/internal/matugen/seedcache.go | 102 +++++++++++ core/internal/matugen/seedcache_test.go | 34 ++++ core/internal/server/dgop/worker.go | 4 +- core/internal/utils/flatpak.go | 39 ++++- core/internal/utils/flatpak_test.go | 107 +++++------- dank-qml-common | 2 +- flake.lock | 6 +- quickshell/Common/SettingsData.qml | 2 + quickshell/Common/settings/SettingsSpec.js | 3 + quickshell/Common/settings/SettingsStore.js | 3 + quickshell/Modals/Common/DankModalHost.qml | 1 + .../DankLauncherV2ModalHost.qml | 2 + .../Modals/Settings/SettingsContent.qml | 5 +- quickshell/Modals/Settings/SettingsModal.qml | 1 + .../Modals/Settings/SettingsSidebar.qml | 2 + .../ControlCenter/Components/EditControls.qml | 1 + .../ControlCenter/Widgets/CcSliderRow.qml | 4 - .../Modules/ControlCenter/Widgets/CcTile.qml | 4 - quickshell/Modules/Dock/Dock.qml | 1 + .../Modules/Dock/DockContextMenuBase.qml | 1 + .../Notifications/Popup/NotificationPopup.qml | 1 + .../Modules/Settings/ThemeSurfacesTab.qml | 150 ++++++++-------- .../Modules/Settings/Widgets/SettingsRow.qml | 28 +-- .../Widgets/SettingsWallpaperThumb.qml | 5 +- .../Modules/Settings/WindowRulesTab.qml | 3 +- quickshell/Modules/Toast.qml | 1 + .../WorkspaceOverlays/NiriOverviewOverlay.qml | 1 + quickshell/Services/BlurService.qml | 4 +- quickshell/Widgets/DankContextMenu.qml | 1 + quickshell/Widgets/DankFloatingWindow.qml | 1 + quickshell/Widgets/DankOSD.qml | 1 + quickshell/Widgets/DankPopoutHost.qml | 2 + quickshell/Widgets/DankSlideout.qml | 1 + quickshell/Widgets/WindowBlur.qml | 7 +- quickshell/matugen/configs/pywalfox.toml | 2 +- .../translations/settings_search_index.json | 3 +- 43 files changed, 470 insertions(+), 242 deletions(-) rename core/internal/{server/dgop/priority_linux.go => lowprio/lowprio_linux.go} (71%) rename core/internal/{server/dgop/priority_other.go => lowprio/lowprio_other.go} (69%) create mode 100644 core/internal/matugen/seedcache.go create mode 100644 core/internal/matugen/seedcache_test.go diff --git a/core/internal/config/embedded/hyprland.lua b/core/internal/config/embedded/hyprland.lua index 2881ed3a9..7ecb814d2 100644 --- a/core/internal/config/embedded/hyprland.lua +++ b/core/internal/config/embedded/hyprland.lua @@ -66,7 +66,6 @@ hl.window_rule({ match = { class = "^(org\\.gnome\\.Calculator)$" }, float = tru hl.window_rule({ match = { class = "^(gnome-calculator)$" }, float = true }) hl.window_rule({ match = { class = "^(galculator)$" }, float = true }) hl.window_rule({ match = { class = "^(blueman-manager)$" }, float = true }) -hl.window_rule({ match = { class = "^(org\\.gnome\\.Nautilus)$" }, float = true }) hl.window_rule({ match = { class = "^(xdg-desktop-portal)$" }, float = true }) hl.window_rule({ match = { class = "^(steam)$", title = "^(notificationtoasts)" }, diff --git a/core/internal/config/embedded/niri.kdl b/core/internal/config/embedded/niri.kdl index e0124563f..3b982a6e4 100644 --- a/core/internal/config/embedded/niri.kdl +++ b/core/internal/config/embedded/niri.kdl @@ -204,7 +204,6 @@ window-rule { match app-id=r#"^gnome-calculator$"# match app-id=r#"^galculator$"# match app-id=r#"^blueman-manager$"# - match app-id=r#"^org\.gnome\.Nautilus$"# match app-id=r#"^xdg-desktop-portal$"# open-floating true } diff --git a/core/internal/server/dgop/priority_linux.go b/core/internal/lowprio/lowprio_linux.go similarity index 71% rename from core/internal/server/dgop/priority_linux.go rename to core/internal/lowprio/lowprio_linux.go index f915eccce..7fe26c5c5 100644 --- a/core/internal/server/dgop/priority_linux.go +++ b/core/internal/lowprio/lowprio_linux.go @@ -1,9 +1,9 @@ //go:build linux -package dgop +package lowprio import "golang.org/x/sys/unix" -func lowerThreadPriority() { +func LowerThreadPriority() { _ = unix.Setpriority(unix.PRIO_PROCESS, unix.Gettid(), 19) } diff --git a/core/internal/server/dgop/priority_other.go b/core/internal/lowprio/lowprio_other.go similarity index 69% rename from core/internal/server/dgop/priority_other.go rename to core/internal/lowprio/lowprio_other.go index 25cf898ad..6f277b634 100644 --- a/core/internal/server/dgop/priority_other.go +++ b/core/internal/lowprio/lowprio_other.go @@ -1,6 +1,6 @@ //go:build !linux -package dgop +package lowprio // Nice is per-process outside Linux, so lowering it here would slow the whole daemon. -func lowerThreadPriority() {} +func LowerThreadPriority() {} diff --git a/core/internal/matugen/matugen.go b/core/internal/matugen/matugen.go index 2e761b308..1bed3a2cb 100644 --- a/core/internal/matugen/matugen.go +++ b/core/internal/matugen/matugen.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strconv" "strings" "sync" @@ -97,6 +98,7 @@ var ( matugenIsV4 bool matugenIsV42 bool matugenSupportsPrefer bool + matugenVersionStr string ) type Options struct { @@ -364,16 +366,11 @@ func buildOnce(opts *Options) (bool, error) { return false, err } defer cleanup() - if err := resolveSmartMode(opts, flags); err != nil { + smartJSON, err := resolveSmartMode(opts, flags) + if err != nil { return false, err } - - cfgFile, err := os.CreateTemp("", "matugen-config-*.toml") - if err != nil { - return false, fmt.Errorf("failed to create temp config: %w", err) - } - defer os.Remove(cfgFile.Name()) - defer cfgFile.Close() + seeds := loadSeedCache(opts.StateDir) tmpDir, err := os.MkdirTemp("", "matugen-templates-*") if err != nil { @@ -381,23 +378,12 @@ func buildOnce(opts *Options) (bool, error) { } defer os.RemoveAll(tmpDir) - if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil { - return false, fmt.Errorf("failed to build config: %w", err) - } - cfgFile.Close() - oldColors, _ := os.ReadFile(opts.ColorsOutput()) var primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight string var dank16JSON string - var importArgs []string + var importArgs, args []string - // Colorful mode resolves the seed here, before matugen is invoked at all, - // by rewriting the source to the extracted hex. Both the dry-run and the - // real run below read opts.Kind/opts.Value, so one rewrite covers both and - // they cannot disagree about the seed. Extraction failure (a format - // image.Decode cannot read, an unreadable file) falls through to matugen's - // own extraction: this must never fail a theme build. if opts.StockColors == "" && opts.SeedColor != "" { seed, err := NormalizeHexColor(opts.SeedColor) if err != nil { @@ -410,7 +396,10 @@ func buildOnce(opts *Options) (bool, error) { } if opts.StockColors == "" && opts.Kind == "image" && opts.SourceMode == SourceModeColorful { - if seed, err := ExtractSourceColor(opts.Value); err != nil { + seed, err := seeds.resolve(sourceImage, opts.SourceMode, flags.version, func() (string, error) { + return ExtractSourceColor(opts.Value) + }) + if err != nil { log.Warnf("Colorful source extraction failed for %s, using matugen's own: %v", opts.Value, err) } else { log.Infof("Colorful source color: %s -> %s", opts.Value, seed) @@ -440,15 +429,30 @@ func buildOnce(opts *Options) (bool, error) { importArgs = []string{"--import-json-string", importData} log.Info("Running matugen color hex with stock color overrides") - args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()} - args = appendContrastArg(args, opts.Contrast) - args = append(args, importArgs...) - if err := runMatugen(args, opts.SourceMode); err != nil { - return false, err - } + args = []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType} } else { log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value) + if opts.Kind == "image" { + seed, err := seeds.resolve(sourceImage, opts.SourceMode, flags.version, func() (string, error) { + if seed, err := matugenSeed(smartJSON); err == nil { + return seed, nil + } + output, err := runMatugenDryRun(opts) + if err != nil { + return "", err + } + return matugenSeed(output) + }) + if err != nil { + log.Warnf("Seed resolution failed for %s, running matugen on the image: %v", sourceImage, err) + } else { + log.Infof("Seed color: %s -> %s", sourceImage, seed) + opts.Kind = "hex" + opts.Value = seed + } + } + matJSON, err := runMatugenDryRun(opts) if err != nil { return false, fmt.Errorf("matugen dry-run failed: %w", err) @@ -493,34 +497,29 @@ func buildOnce(opts *Options) (bool, error) { importArgs = []string{"--import-json-string", buildImportData(dank16JSON, sourceImage, specColors)} log.Infof("Running matugen %s with dank16 injection", opts.Kind) - var args []string switch opts.Kind { case "hex": args = []string{"color", "hex", opts.Value} default: args = []string{opts.Kind, opts.Value} } - args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()) - args = appendContrastArg(args, opts.Contrast) - args = append(args, importArgs...) - if err := runMatugen(args, opts.SourceMode); err != nil { - return false, err - } + args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType) } + args = appendContrastArg(args, opts.Contrast) + args = append(args, importArgs...) - newColors, err := os.ReadFile(opts.colorsStaging()) + changed, err := renderColors(opts, args, tmpDir, oldColors) if err != nil { - return false, fmt.Errorf("matugen did not produce colors output: %w", err) + return false, err } - if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 { - return false, nil - } - if err := os.Rename(opts.colorsStaging(), opts.ColorsOutput()); err != nil { - return false, fmt.Errorf("failed to commit colors output: %w", err) - } - if opts.ColorsOnly { - return true, nil + return changed, nil + } + if err := renderTemplates(opts, args, tmpDir); err != nil { + return false, err + } + if !changed { + return false, nil } if isDMSGTKActive(opts.ConfigDir) { @@ -559,6 +558,50 @@ func buildOnce(opts *Options) (bool, error) { return true, nil } +// The shell only needs dms-colors.json, so it is rendered and committed on its own before the template pass. +func renderColors(opts *Options, args []string, tmpDir string, oldColors []byte) (bool, error) { + colorsOpts := *opts + colorsOpts.ColorsOnly = true + cfgPath, err := writeMergedConfig(&colorsOpts, tmpDir, "colors.toml") + if err != nil { + return false, err + } + if err := runMatugen(append(slices.Clone(args), "-c", cfgPath), opts.SourceMode); err != nil { + return false, err + } + newColors, err := os.ReadFile(opts.colorsStaging()) + if err != nil { + return false, fmt.Errorf("matugen did not produce colors output: %w", err) + } + if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 { + return false, nil + } + if err := os.Rename(opts.colorsStaging(), opts.ColorsOutput()); err != nil { + return false, fmt.Errorf("failed to commit colors output: %w", err) + } + return true, nil +} + +func renderTemplates(opts *Options, args []string, tmpDir string) error { + cfgPath, err := writeMergedConfig(opts, tmpDir, "templates.toml") + if err != nil { + return err + } + return runMatugen(append(slices.Clone(args), "-c", cfgPath), opts.SourceMode) +} + +func writeMergedConfig(opts *Options, tmpDir, name string) (string, error) { + cfgFile, err := os.Create(filepath.Join(tmpDir, name)) + if err != nil { + return "", fmt.Errorf("failed to create temp config: %w", err) + } + defer cfgFile.Close() + if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil { + return "", fmt.Errorf("failed to build config: %w", err) + } + return cfgFile.Name(), nil +} + func appendContrastArg(args []string, contrast float64) []string { if contrast == 0 { return args @@ -921,6 +964,7 @@ type matugenFlags struct { isV4 bool isV42 bool supportsPrefer bool + version string } func detectMatugenVersion() (matugenFlags, error) { @@ -928,7 +972,7 @@ func detectMatugenVersion() (matugenFlags, error) { defer matugenVersionMu.Unlock() if matugenVersionOK { - return matugenFlags{matugenSupportsCOE, matugenIsV4, matugenIsV42, matugenSupportsPrefer}, nil + return matugenFlags{matugenSupportsCOE, matugenIsV4, matugenIsV42, matugenSupportsPrefer, matugenVersionStr}, nil } return detectMatugenVersionLocked() @@ -985,6 +1029,7 @@ func detectMatugenVersionLocked() (matugenFlags, error) { // --prefer landed in 4.1; 4.0.x has --source-color-index but not --prefer, // and clap aborts on an unknown argument rather than ignoring it. matugenSupportsPrefer = major > 4 || (major == 4 && minor >= 1) + matugenVersionStr = versionStr matugenVersionOK = true if matugenSupportsCOE { @@ -996,7 +1041,7 @@ func detectMatugenVersionLocked() (matugenFlags, error) { if matugenIsV4 && !matugenSupportsPrefer { log.Debugf("Matugen %s detected: --prefer unavailable, source modes fall back to the dominant color", versionStr) } - return matugenFlags{matugenSupportsCOE, matugenIsV4, matugenIsV42, matugenSupportsPrefer}, nil + return matugenFlags{matugenSupportsCOE, matugenIsV4, matugenIsV42, matugenSupportsPrefer, matugenVersionStr}, nil } func buildMatugenArgs(baseArgs []string, flags matugenFlags, sourceMode string) []string { @@ -1157,31 +1202,39 @@ func extractTopLevelString(jsonStr, key string) string { return "" } -func resolveSmartMode(opts *Options, flags matugenFlags) error { +func resolveSmartMode(opts *Options, flags matugenFlags) (string, error) { if opts.MatugenType == "scheme-smart" && !flags.isV42 { - return fmt.Errorf("scheme-smart requires matugen 4.2+") + return "", fmt.Errorf("scheme-smart requires matugen 4.2+") } if opts.Mode != ColorModeSmart { - return nil + return "", nil } if !flags.isV42 { - return fmt.Errorf("smart mode requires matugen 4.2+") + return "", fmt.Errorf("smart mode requires matugen 4.2+") } if opts.Kind != "image" || opts.StockColors != "" { opts.Mode = ColorModeDark - return nil + return "", nil } output, err := runMatugenDryRun(opts) if err != nil { - return fmt.Errorf("smart mode resolution failed: %w", err) + return "", fmt.Errorf("smart mode resolution failed: %w", err) } resolved := extractTopLevelString(output, "mode") if resolved != string(ColorModeLight) && resolved != string(ColorModeDark) { - return fmt.Errorf("smart mode resolution returned unexpected mode %q", resolved) + return "", fmt.Errorf("smart mode resolution returned unexpected mode %q", resolved) } log.Infof("Smart mode resolved to %s", resolved) opts.Mode = ColorMode(resolved) - return nil + return output, nil +} + +func matugenSeed(dryRunJSON string) (string, error) { + seed := extractMatugenColor(dryRunJSON, "source_color", "dark") + if seed == "" { + return "", fmt.Errorf("matugen output has no source_color") + } + return seed, nil } func extractNestedColor(jsonStr, colorName, variant string) string { diff --git a/core/internal/matugen/matugen_test.go b/core/internal/matugen/matugen_test.go index 52f7ee5d0..51436e40a 100644 --- a/core/internal/matugen/matugen_test.go +++ b/core/internal/matugen/matugen_test.go @@ -1317,7 +1317,7 @@ func TestResolveSmartMode(t *testing.T) { opts.MatugenType = "scheme-tonal-spot" } - err := resolveSmartMode(opts, matugenFlags{isV42: tc.isV42}) + _, err := resolveSmartMode(opts, matugenFlags{isV42: tc.isV42}) if tc.wantErr != "" { assert.ErrorContains(t, err, tc.wantErr) return diff --git a/core/internal/matugen/queue.go b/core/internal/matugen/queue.go index 38cf72ac4..327a1024d 100644 --- a/core/internal/matugen/queue.go +++ b/core/internal/matugen/queue.go @@ -3,9 +3,11 @@ package matugen import ( "context" "errors" + "runtime" "sync" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/lowprio" ) type Result struct { @@ -74,6 +76,8 @@ func (q *Queue) Submit(opts Options) <-chan Result { } func (q *Queue) runWorker() { + runtime.LockOSThread() + lowprio.LowerThreadPriority() for { q.mu.Lock() job := q.current diff --git a/core/internal/matugen/seedcache.go b/core/internal/matugen/seedcache.go new file mode 100644 index 000000000..f6de24ce3 --- /dev/null +++ b/core/internal/matugen/seedcache.go @@ -0,0 +1,102 @@ +package matugen + +import ( + "encoding/json" + "os" + "path/filepath" + "time" +) + +const ( + seedCacheFile = "matugen-seeds.json" + seedCacheEntries = 8 +) + +type seedEntry struct { + Size int64 `json:"size"` + ModTime int64 `json:"modTime"` + Seed string `json:"seed"` + Used int64 `json:"used"` +} + +type seedCache struct { + path string + Entries map[string]seedEntry `json:"entries"` +} + +func loadSeedCache(stateDir string) *seedCache { + cache := &seedCache{ + path: filepath.Join(stateDir, seedCacheFile), + Entries: map[string]seedEntry{}, + } + data, err := os.ReadFile(cache.path) + if err != nil { + return cache + } + if json.Unmarshal(data, cache) != nil || cache.Entries == nil { + cache.Entries = map[string]seedEntry{} + } + return cache +} + +func seedCacheKey(imagePath, sourceMode, matugenVersion string) string { + if sourceMode != SourceModeColorful && !matugenPreferValues[sourceMode] { + sourceMode = SourceModeDominant + } + return imagePath + "|" + sourceMode + "|" + matugenVersion +} + +func (c *seedCache) lookup(imagePath, sourceMode, matugenVersion string) (string, bool) { + entry, ok := c.Entries[seedCacheKey(imagePath, sourceMode, matugenVersion)] + if !ok { + return "", false + } + info, err := os.Stat(imagePath) + if err != nil || info.Size() != entry.Size || info.ModTime().UnixNano() != entry.ModTime { + return "", false + } + return entry.Seed, true +} + +func (c *seedCache) resolve(imagePath, sourceMode, matugenVersion string, compute func() (string, error)) (string, error) { + if seed, ok := c.lookup(imagePath, sourceMode, matugenVersion); ok { + return seed, nil + } + seed, err := compute() + if err != nil { + return "", err + } + c.store(imagePath, sourceMode, matugenVersion, seed) + return seed, nil +} + +func (c *seedCache) store(imagePath, sourceMode, matugenVersion, seed string) { + info, err := os.Stat(imagePath) + if err != nil { + return + } + c.Entries[seedCacheKey(imagePath, sourceMode, matugenVersion)] = seedEntry{ + Size: info.Size(), + ModTime: info.ModTime().UnixNano(), + Seed: seed, + Used: time.Now().UnixNano(), + } + c.evict() + if data, err := json.Marshal(c); err == nil { + _ = os.WriteFile(c.path, data, 0o644) + } +} + +func (c *seedCache) evict() { + for len(c.Entries) > seedCacheEntries { + oldestKey := "" + oldest := int64(0) + for key, entry := range c.Entries { + if oldestKey == "" || entry.Used < oldest { + oldestKey = key + oldest = entry.Used + } + } + delete(c.Entries, oldestKey) + } +} diff --git a/core/internal/matugen/seedcache_test.go b/core/internal/matugen/seedcache_test.go new file mode 100644 index 000000000..095ea7bc3 --- /dev/null +++ b/core/internal/matugen/seedcache_test.go @@ -0,0 +1,34 @@ +package matugen + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSeedCacheLookupInvalidation(t *testing.T) { + dir := t.TempDir() + img := filepath.Join(dir, "wall.png") + assert.NoError(t, os.WriteFile(img, []byte("a"), 0o644)) + + loadSeedCache(dir).store(img, "dominant", "4.2.0", "#6c46dc") + cache := loadSeedCache(dir) + + seed, ok := cache.lookup(img, "dominant", "4.2.0") + assert.True(t, ok) + assert.Equal(t, "#6c46dc", seed) + + _, ok = cache.lookup(img, "colorful", "4.2.0") + assert.False(t, ok, "source mode is part of the key") + seed, ok = cache.lookup(img, "", "4.2.0") + assert.True(t, ok, "empty mode is dominant") + assert.Equal(t, "#6c46dc", seed) + _, ok = cache.lookup(img, "dominant", "4.3.0") + assert.False(t, ok, "matugen version is part of the key") + + assert.NoError(t, os.WriteFile(img, []byte("ab"), 0o644)) + _, ok = cache.lookup(img, "dominant", "4.2.0") + assert.False(t, ok, "rewritten file must miss") +} diff --git a/core/internal/server/dgop/worker.go b/core/internal/server/dgop/worker.go index 1bf841f67..769b2a391 100644 --- a/core/internal/server/dgop/worker.go +++ b/core/internal/server/dgop/worker.go @@ -3,6 +3,8 @@ package dgop import ( "runtime" "sync" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/lowprio" ) var ( @@ -26,7 +28,7 @@ func runLowPriority(job func()) { // Sampling must never preempt a foreground app, so it runs on one OS thread with lowered priority. func worker() { runtime.LockOSThread() - lowerThreadPriority() + lowprio.LowerThreadPriority() for job := range jobs { job() } diff --git a/core/internal/utils/flatpak.go b/core/internal/utils/flatpak.go index f1c9009d7..fcceeda91 100644 --- a/core/internal/utils/flatpak.go +++ b/core/internal/utils/flatpak.go @@ -2,12 +2,17 @@ package utils import ( "bytes" + "cmp" "errors" + "os" "os/exec" + "path/filepath" "slices" "strings" ) +var flatpakInstallationsDir = "/etc/flatpak/installations.d" + func FlatpakInPath() bool { _, err := exec.LookPath("flatpak") return err == nil @@ -17,10 +22,38 @@ func FlatpakExists(name string) bool { if !FlatpakInPath() { return false } + for _, dir := range flatpakInstallations() { + if info, err := os.Stat(filepath.Join(dir, "app", name)); err == nil && info.IsDir() { + return true + } + } + return false +} - cmd := exec.Command("flatpak", "info", name) - err := cmd.Run() - return err == nil +func flatpakInstallations() []string { + dirs := []string{ + cmp.Or(os.Getenv("FLATPAK_USER_DIR"), filepath.Join(XDGDataHome(), "flatpak")), + cmp.Or(os.Getenv("FLATPAK_SYSTEM_DIR"), "/var/lib/flatpak"), + } + entries, err := os.ReadDir(flatpakInstallationsDir) + if err != nil { + return dirs + } + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".conf") { + continue + } + data, err := os.ReadFile(filepath.Join(flatpakInstallationsDir, entry.Name())) + if err != nil { + continue + } + for line := range strings.Lines(string(data)) { + if path, ok := strings.CutPrefix(strings.TrimSpace(line), "Path="); ok { + dirs = append(dirs, strings.TrimSpace(path)) + } + } + } + return dirs } func FlatpakSearchBySubstring(substring string) bool { diff --git a/core/internal/utils/flatpak_test.go b/core/internal/utils/flatpak_test.go index 760254850..baa8f76d2 100644 --- a/core/internal/utils/flatpak_test.go +++ b/core/internal/utils/flatpak_test.go @@ -50,30 +50,6 @@ func TestFlatpakInstallationDirNoFlatpak(t *testing.T) { } } -func TestFlatpakExistsCommandFailure(t *testing.T) { - if !FlatpakInPath() { - t.Skip("flatpak not in PATH") - } - - // Mock a failing flatpak command through PATH interception - tempDir := t.TempDir() - fakeFlatpak := filepath.Join(tempDir, "flatpak") - - script := "#!/bin/sh\nexit 1\n" - err := os.WriteFile(fakeFlatpak, []byte(script), 0o755) - if err != nil { - t.Fatalf("failed to create fake flatpak: %v", err) - } - - originalPath := os.Getenv("PATH") - t.Setenv("PATH", tempDir+":"+originalPath) - - result := FlatpakExists("test.package") - if result { - t.Errorf("expected false when flatpak command fails, got true") - } -} - func TestFlatpakSearchBySubstringCommandFailure(t *testing.T) { if !FlatpakInPath() { t.Skip("flatpak not in PATH") @@ -125,50 +101,6 @@ func TestFlatpakInstallationDirCommandFailure(t *testing.T) { } } -func TestAnyFlatpakExistsSomeExist(t *testing.T) { - tempDir := t.TempDir() - fakeFlatpak := filepath.Join(tempDir, "flatpak") - - // Script that succeeds only for "app.exists.test" - script := `#!/bin/sh -if [ "$1" = "info" ] && [ "$2" = "app.exists.test" ]; then - exit 0 -fi -exit 1 -` - err := os.WriteFile(fakeFlatpak, []byte(script), 0o755) - if err != nil { - t.Fatalf("failed to create fake flatpak: %v", err) - } - - originalPath := os.Getenv("PATH") - t.Setenv("PATH", tempDir+":"+originalPath) - - result := AnyFlatpakExists("com.nonexistent.flatpak", "app.exists.test", "com.another.nonexistent") - if !result { - t.Errorf("expected true when at least one flatpak exists") - } -} - -func TestAnyFlatpakExistsNoneExist(t *testing.T) { - tempDir := t.TempDir() - fakeFlatpak := filepath.Join(tempDir, "flatpak") - - script := "#!/bin/sh\nexit 1\n" - err := os.WriteFile(fakeFlatpak, []byte(script), 0o755) - if err != nil { - t.Fatalf("failed to create fake flatpak: %v", err) - } - - originalPath := os.Getenv("PATH") - t.Setenv("PATH", tempDir+":"+originalPath) - - result := AnyFlatpakExists("com.nonexistent.flatpak1", "com.nonexistent.flatpak2") - if result { - t.Errorf("expected false when no flatpaks exist") - } -} - func TestAnyFlatpakExistsNoFlatpak(t *testing.T) { tempDir := t.TempDir() t.Setenv("PATH", tempDir) @@ -185,3 +117,42 @@ func TestAnyFlatpakExistsEmpty(t *testing.T) { t.Errorf("expected false when no flatpaks specified") } } + +func TestFlatpakExistsByInstallationDir(t *testing.T) { + if !FlatpakInPath() { + t.Skip("flatpak not in PATH") + } + user := t.TempDir() + system := t.TempDir() + extra := t.TempDir() + confDir := t.TempDir() + t.Setenv("FLATPAK_USER_DIR", user) + t.Setenv("FLATPAK_SYSTEM_DIR", system) + old := flatpakInstallationsDir + flatpakInstallationsDir = confDir + t.Cleanup(func() { flatpakInstallationsDir = old }) + + if err := os.MkdirAll(filepath.Join(user, "app", "app.user.test"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(extra, "app", "app.extra.test"), 0o755); err != nil { + t.Fatal(err) + } + conf := "[Installation \"extra\"]\nPath=" + extra + "\n" + if err := os.WriteFile(filepath.Join(confDir, "extra.conf"), []byte(conf), 0o644); err != nil { + t.Fatal(err) + } + + if !FlatpakExists("app.user.test") { + t.Errorf("expected app in the user installation to be found") + } + if !FlatpakExists("app.extra.test") { + t.Errorf("expected app in an installations.d path to be found") + } + if FlatpakExists("app.missing.test") { + t.Errorf("expected missing app to be reported absent") + } + if !AnyFlatpakExists("app.missing.test", "app.user.test") { + t.Errorf("expected AnyFlatpakExists to find the installed app") + } +} diff --git a/dank-qml-common b/dank-qml-common index 0de254e8d..17e81ad63 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 0de254e8dde48af44bd3411f625a930ce47cf136 +Subproject commit 17e81ad63eb2c36e64021e8dc5af51781b14127b diff --git a/flake.lock b/flake.lock index a1454a8ab..355bb6a8d 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1789958362, - "narHash": "sha256-cR1S8SdXzGGmnCRNIPacLXThEHsU1jk0yS+M7QCxylw=", + "lastModified": 1790003999, + "narHash": "sha256-1D3b45g4gmn4hczziKLykliW3oc1d8uqddzZ2poN6fQ=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "0de254e8dde48af44bd3411f625a930ce47cf136", + "rev": "17e81ad63eb2c36e64021e8dc5af51781b14127b", "type": "github" }, "original": { diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index ea9d2df59..c17a8525c 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -292,6 +292,8 @@ Singleton { property bool blurEnabled: Spec.SPEC.blurEnabled.def onBlurEnabledChanged: saveSettings() + property bool blurBorderSeeded: Spec.SPEC.blurBorderSeeded.def + onBlurBorderSeededChanged: saveSettings() property bool blurForegroundLayers: Spec.SPEC.blurForegroundLayers.def onBlurForegroundLayersChanged: saveSettings() property real foregroundLayerTransparency: Spec.SPEC.foregroundLayerTransparency.def diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 9a0247e38..9549225db 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -357,6 +357,9 @@ var LOCAL_SPEC = { blurEnabled: { def: false }, + blurBorderSeeded: { + def: false + }, blurForegroundLayers: { def: true }, diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index 0b26f81ce..f61c9ec0e 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -664,6 +664,9 @@ function migrateToVersion(obj, targetVersion) { } delete settings.frameColor; delete settings.frameOpacity; + if (settings.blurBorderEnabled === undefined) + settings.blurBorderEnabled = settings.blurEnabled === true; + settings.blurBorderSeeded = settings.blurEnabled === true; settings.configVersion = 28; } diff --git a/quickshell/Modals/Common/DankModalHost.qml b/quickshell/Modals/Common/DankModalHost.qml index 54dbacc14..cd3314cd9 100644 --- a/quickshell/Modals/Common/DankModalHost.qml +++ b/quickshell/Modals/Common/DankModalHost.qml @@ -543,6 +543,7 @@ Item { WindowBlur { targetWindow: contentWindow blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome + surfaceColor: root.effectiveBackgroundColor readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real op: Math.max(0, Math.min(1, (morph.value - 0.06) * 2)) readonly property real visibleScale: root.connected ? s : s * op diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalHost.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalHost.qml index c432c6101..4dc162ae9 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalHost.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2ModalHost.qml @@ -907,6 +907,7 @@ Item { WindowBlur { targetWindow: contentWindow blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome + surfaceColor: root.backgroundColor readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property bool clipDriven: root._fluidMotionActive && !root.connectedFluidMotion blurX: clipDriven ? (root._ccX + root.fluidBody.x) : root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) @@ -1185,6 +1186,7 @@ Item { WindowBlur { targetWindow: contentWindow + surfaceColor: root.backgroundColor readonly property real s: Math.min(1, modalContainer.publishedScale) readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2)) readonly property real visibleScale: s * op diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index 4a5fded7a..b624ecbd5 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -31,8 +31,9 @@ FocusScope { function _focusPage() { Qt.callLater(() => { - if (sessionVisible && currentPageItem) - currentPageItem.forceActiveFocus(); + if (!sessionVisible || !currentPageItem || parentModal?.searchFocused) + return; + currentPageItem.forceActiveFocus(); }); } diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index a37f788db..4b9a95226 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -41,6 +41,7 @@ DankFloatingWindow { property bool shouldHaveFocus: visible property bool allowFocusOverride: false property alias shouldBeVisible: settingsModal.visible + readonly property bool searchFocused: sidebar.searchFocused property bool isCompactMode: width < SettingsMetrics.compactBreakpoint property bool menuVisible: !isCompactMode property string keybindSearchQuery: "" diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index da671241e..d0488afb1 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -21,6 +21,7 @@ Item { signal pageRequested(string pageId) property bool searchActive: searchField.text.length > 0 + property bool searchFocused: false property int searchSelectedIndex: 0 property string keyboardHighlightId: "" readonly property var categoryStructure: SettingsTabs.structure @@ -187,6 +188,7 @@ Item { anchors.topMargin: Theme.spacingM height: Theme.iconButtonSize + Theme.spacingM placeholderText: I18n.tr("Search settings", "settings search field placeholder") + onFocusStateChanged: hasFocus => root.searchFocused = hasFocus onTextChanged: { SettingsSearchService.search(text); root.searchSelectedIndex = 0; diff --git a/quickshell/Modules/ControlCenter/Components/EditControls.qml b/quickshell/Modules/ControlCenter/Components/EditControls.qml index 0e3f22ebb..0ce4476a9 100644 --- a/quickshell/Modules/ControlCenter/Components/EditControls.qml +++ b/quickshell/Modules/ControlCenter/Components/EditControls.qml @@ -80,6 +80,7 @@ Row { WindowBlur { targetWindow: addWidgetWindow + surfaceColor: widgetLibraryPanel.color blurX: widgetLibraryPanel.x blurY: widgetLibraryPanel.y blurWidth: addWidgetWindow.visible ? widgetLibraryPanel.width : 0 diff --git a/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml b/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml index 4e1c59aa9..8e85b500a 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcSliderRow.qml @@ -48,8 +48,6 @@ Item { anchors.fill: parent radius: root.bodyRadius color: CcMetrics.tileInactiveColor - border.width: Theme.layerOutlineWidth - border.color: Theme.outlineMedium visible: root.tall } @@ -65,8 +63,6 @@ Item { y: root.vertical ? parent.height - height : root.tall ? 0 : (parent.height - height) / 2 buttonSize: root.actionSize backgroundColor: CcMetrics.tileInactiveColor - border.width: Theme.layerOutlineWidth - border.color: Theme.outlineMedium iconName: root.iconName iconSize: CcMetrics.iconBoxIconSize iconColor: CcMetrics.tileInactiveContent diff --git a/quickshell/Modules/ControlCenter/Widgets/CcTile.qml b/quickshell/Modules/ControlCenter/Widgets/CcTile.qml index 2979d2933..215735b23 100644 --- a/quickshell/Modules/ControlCenter/Widgets/CcTile.qml +++ b/quickshell/Modules/ControlCenter/Widgets/CcTile.qml @@ -151,8 +151,6 @@ Item { anchors.fill: parent radius: root.bodyRadius color: root.bodyColor - border.width: root.bodyActive ? 0 : Theme.layerOutlineWidth - border.color: Theme.outlineMedium Behavior on color { enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion @@ -223,8 +221,6 @@ Item { height: width radius: root.hasIconBox ? root.iconBoxRadius : 0 color: root.hasIconBox ? root.iconBoxColor : "transparent" - border.width: root.hasIconBox && !root.active ? Theme.layerOutlineWidth : 0 - border.color: Theme.outlineMedium activeFocusOnTab: root.showExpand && root.toggle && root.acceptsInput Accessible.role: root.toggle ? Accessible.CheckBox : Accessible.Button Accessible.name: root.title diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 2837237d1..f5812518e 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -68,6 +68,7 @@ Variants { WindowBlur { targetWindow: dock blurEnabled: body.effectiveBlurEnabled && !body.usesConnectedFrameChrome + surfaceColor: body.surfaceColor blurX: body.blurX blurY: body.blurY blurWidth: body.blurWidth diff --git a/quickshell/Modules/Dock/DockContextMenuBase.qml b/quickshell/Modules/Dock/DockContextMenuBase.qml index ab90ccb56..b5dc16260 100644 --- a/quickshell/Modules/Dock/DockContextMenuBase.qml +++ b/quickshell/Modules/Dock/DockContextMenuBase.qml @@ -74,6 +74,7 @@ PanelWindow { WindowBlur { targetWindow: root + surfaceColor: menuContainer.color blurX: menuContainer.x blurY: menuContainer.y blurWidth: root.visible ? menuContainer.width : 0 diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 808766b16..dae4fc9fb 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -16,6 +16,7 @@ PanelWindow { WindowBlur { targetWindow: win + surfaceColor: Theme.notificationFloatingSurface readonly property real s: Math.min(1, content.scale) * Math.max(0, content.opacity) readonly property real innerW: Math.max(0, content.width - content.cardInset * 2) readonly property real innerH: Math.max(0, content.height - content.cardInset * 2) diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index 8cb07fcb0..663ab4c80 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -9,7 +9,7 @@ Column { property var parentModal: null readonly property bool followsSurfaces: SettingsData.floatingWindowSyncGlobal ?? true - readonly property bool borderEnabled: SettingsData.blurBorderEnabled ?? true + readonly property bool borderEnabled: SettingsData.blurBorderEnabled ?? false readonly property string windowRadiusKey: CompositorService.supportsLayoutConfig ? CompositorService.configKey + "LayoutRadiusOverride" : "" width: parent?.width ?? 0 @@ -82,6 +82,76 @@ Column { onSliderValueChanged: newValue => SettingsData.set("popupTransparency", newValue / 100) } + SettingsToggleRow { + tab: "theme" + tags: ["surface", "popup", "modal", "border", "outline", "edge"] + settingKey: "blurBorderEnabled" + text: I18n.tr("Border") + checked: root.borderEnabled + onToggled: checked => SettingsData.set("blurBorderEnabled", checked) + } + + SettingsDropdownRow { + tab: "theme" + tags: ["surface", "popup", "modal", "border", "outline", "edge", "color"] + settingKey: "blurBorderColor" + visible: root.borderEnabled + resetKeys: ["blurBorderColor", "blurBorderCustomColor"] + text: I18n.tr("Border color") + options: [I18n.tr("Outline", "surface border color"), I18n.tr("Primary", "surface border color"), I18n.tr("Secondary", "surface border color"), I18n.tr("Text Color", "surface border color"), I18n.tr("Custom", "surface border color")] + optionColorMap: ({ + [I18n.tr("Outline", "surface border color")]: Theme.outline, + [I18n.tr("Primary", "surface border color")]: Theme.primary, + [I18n.tr("Secondary", "surface border color")]: Theme.secondary, + [I18n.tr("Text Color", "surface border color")]: Theme.surfaceText, + [I18n.tr("Custom", "surface border color")]: SettingsData.blurBorderCustomColor ?? "#ffffff" + }) + currentValue: { + switch (SettingsData.blurBorderColor) { + case "primary": + return I18n.tr("Primary", "surface border color"); + case "secondary": + return I18n.tr("Secondary", "surface border color"); + case "surfaceText": + return I18n.tr("Text Color", "surface border color"); + case "custom": + return I18n.tr("Custom", "surface border color"); + default: + return I18n.tr("Outline", "surface border color"); + } + } + onValueChanged: value => { + switch (value) { + case I18n.tr("Primary", "surface border color"): + SettingsData.set("blurBorderColor", "primary"); + return; + case I18n.tr("Secondary", "surface border color"): + SettingsData.set("blurBorderColor", "secondary"); + return; + case I18n.tr("Text Color", "surface border color"): + SettingsData.set("blurBorderColor", "surfaceText"); + return; + case I18n.tr("Custom", "surface border color"): + SettingsData.set("blurBorderColor", "custom"); + root.openSurfaceBorderColorPicker(); + return; + } + SettingsData.set("blurBorderColor", "outline"); + } + } + + SettingsSliderRow { + tab: "theme" + tags: ["surface", "popup", "modal", "border", "opacity"] + settingKey: "blurBorderOpacity" + visible: root.borderEnabled + text: I18n.tr("Border opacity") + value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100) + minimum: 0 + maximum: 100 + onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100) + } + SettingsToggleRow { tab: "theme" tags: ["foreground", "layers", "contrast", "surface", "blur", "glass", "frosted"] @@ -122,7 +192,14 @@ Column { description: BlurService.available ? "" : I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)") checked: SettingsData.blurEnabled ?? false enabled: BlurService.available - onToggled: checked => SettingsData.set("blurEnabled", checked) + onToggled: checked => { + SettingsData.set("blurEnabled", checked); + if (!checked || SettingsData.blurBorderSeeded) + return; + SettingsData.set("blurBorderSeeded", true); + SettingsData.set("blurBorderEnabled", true); + SettingsSearchService.navigateToSection("blurBorderEnabled"); + } } SettingsNavRow { @@ -373,75 +450,6 @@ Column { } } - SettingsToggleCard { - tab: "theme" - tags: ["surface", "popup", "modal", "border", "outline", "edge"] - settingKey: "blurBorderEnabled" - iconName: "border_style" - title: I18n.tr("Border") - checked: root.borderEnabled - onToggled: checked => SettingsData.set("blurBorderEnabled", checked) - - SettingsDropdownRow { - tab: "theme" - tags: ["surface", "popup", "modal", "border", "outline", "edge", "color"] - settingKey: "blurBorderColor" - resetKeys: ["blurBorderColor", "blurBorderCustomColor"] - text: I18n.tr("Border color") - options: [I18n.tr("Outline", "surface border color"), I18n.tr("Primary", "surface border color"), I18n.tr("Secondary", "surface border color"), I18n.tr("Text Color", "surface border color"), I18n.tr("Custom", "surface border color")] - optionColorMap: ({ - [I18n.tr("Outline", "surface border color")]: Theme.outline, - [I18n.tr("Primary", "surface border color")]: Theme.primary, - [I18n.tr("Secondary", "surface border color")]: Theme.secondary, - [I18n.tr("Text Color", "surface border color")]: Theme.surfaceText, - [I18n.tr("Custom", "surface border color")]: SettingsData.blurBorderCustomColor ?? "#ffffff" - }) - currentValue: { - switch (SettingsData.blurBorderColor) { - case "primary": - return I18n.tr("Primary", "surface border color"); - case "secondary": - return I18n.tr("Secondary", "surface border color"); - case "surfaceText": - return I18n.tr("Text Color", "surface border color"); - case "custom": - return I18n.tr("Custom", "surface border color"); - default: - return I18n.tr("Outline", "surface border color"); - } - } - onValueChanged: value => { - switch (value) { - case I18n.tr("Primary", "surface border color"): - SettingsData.set("blurBorderColor", "primary"); - return; - case I18n.tr("Secondary", "surface border color"): - SettingsData.set("blurBorderColor", "secondary"); - return; - case I18n.tr("Text Color", "surface border color"): - SettingsData.set("blurBorderColor", "surfaceText"); - return; - case I18n.tr("Custom", "surface border color"): - SettingsData.set("blurBorderColor", "custom"); - root.openSurfaceBorderColorPicker(); - return; - } - SettingsData.set("blurBorderColor", "outline"); - } - } - - SettingsSliderRow { - tab: "theme" - tags: ["surface", "popup", "modal", "border", "opacity"] - settingKey: "blurBorderOpacity" - text: I18n.tr("Border opacity") - value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100) - minimum: 0 - maximum: 100 - onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100) - } - } - SettingsToggleCard { tab: "theme" tags: ["focus", "ring", "keyboard", "outline", "highlight"] diff --git a/quickshell/Modules/Settings/Widgets/SettingsRow.qml b/quickshell/Modules/Settings/Widgets/SettingsRow.qml index 90e851519..29f797c41 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsRow.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsRow.qml @@ -240,8 +240,8 @@ T.Control { id: textColumn anchors.left: leadingArea.visible ? leadingArea.right : parent.left anchors.leftMargin: leadingArea.visible ? Theme.spacingL : 0 - anchors.right: resetButton.visible ? resetButton.left : (trailingArea.visible ? trailingArea.left : parent.right) - anchors.rightMargin: resetButton.visible || trailingArea.visible ? SettingsMetrics.rowContentSpacing : 0 + anchors.right: resetButton.active ? resetButton.left : (trailingArea.visible ? trailingArea.left : parent.right) + anchors.rightMargin: resetButton.active || trailingArea.visible ? SettingsMetrics.rowContentSpacing : 0 anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingXXS opacity: root.enabled ? 1 : SettingsMetrics.disabledOpacity @@ -270,20 +270,24 @@ T.Control { } } - DankActionButton { + Loader { id: resetButton anchors.right: trailingArea.visible ? trailingArea.left : parent.right anchors.rightMargin: trailingArea.visible ? Theme.spacingS : 0 anchors.verticalCenter: parent.verticalCenter - buttonSize: Theme.iconButtonSize - iconName: "restart_alt" - iconSize: Theme.iconSizeMedium - iconColor: Theme.surfaceVariantText - tooltipText: I18n.tr("Reset to default") - Accessible.name: I18n.tr("Reset to default") - visible: root.modified && root.resetInHeader - enabled: root.enabled - onClicked: root.resetRequested() + active: root.modified && root.resetInHeader + visible: active + + sourceComponent: DankActionButton { + buttonSize: Theme.iconButtonSize + iconName: "restart_alt" + iconSize: Theme.iconSizeMedium + iconColor: Theme.surfaceVariantText + tooltipText: I18n.tr("Reset to default") + Accessible.name: I18n.tr("Reset to default") + enabled: root.enabled + onClicked: root.resetRequested() + } } Row { diff --git a/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml b/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml index 624dba3db..de9a4bc90 100644 --- a/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml +++ b/quickshell/Modules/Settings/Widgets/SettingsWallpaperThumb.qml @@ -31,8 +31,7 @@ Rectangle { Loader { id: imageLoader anchors.fill: parent - active: root.visible && (root.Window.window?.visible ?? false) && root.isImage - asynchronous: true + active: root.isImage sourceComponent: CachingImage { imagePath: root.path @@ -47,7 +46,7 @@ Rectangle { name: root.placeholderIcon size: Theme.iconSizeLarge color: Theme.surfaceVariantText - visible: !root.isColor && imageLoader.item?.status !== Image.Ready + visible: !root.isColor && (!root.isImage || imageLoader.item?.status === Image.Error) } MouseArea { diff --git a/quickshell/Modules/Settings/WindowRulesTab.qml b/quickshell/Modules/Settings/WindowRulesTab.qml index a14098c22..11a982261 100644 --- a/quickshell/Modules/Settings/WindowRulesTab.qml +++ b/quickshell/Modules/Settings/WindowRulesTab.qml @@ -327,13 +327,14 @@ Item { } SettingsRow { + id: createRuleRow visible: root.activeWindows.length > 0 title: I18n.tr("Create rule for:") DankDropdown { id: windowSelector anchors.verticalCenter: parent.verticalCenter - dropdownWidth: 400 + dropdownWidth: Math.min(400, createRuleRow.width - SettingsMetrics.rowPaddingH * 2) compactMode: true emptyText: I18n.tr("Select a window...") options: root.activeWindows.map(w => { diff --git a/quickshell/Modules/Toast.qml b/quickshell/Modules/Toast.qml index e84f57798..1aa0895c6 100644 --- a/quickshell/Modules/Toast.qml +++ b/quickshell/Modules/Toast.qml @@ -480,6 +480,7 @@ PanelWindow { WindowBlur { targetWindow: root blurEnabled: root.presented + surfaceColor: toast.color blurX: toast.x blurY: toast.y blurWidth: root.presented ? toast.width : 0 diff --git a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml index 3258dfbd4..3c0f48ad6 100644 --- a/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml +++ b/quickshell/Modules/WorkspaceOverlays/NiriOverviewOverlay.qml @@ -162,6 +162,7 @@ Scope { WindowBlur { targetWindow: overlayWindow + surfaceColor: Theme.floatingWindowSurface readonly property real s: Math.min(1, spotlightContainer.scale) readonly property bool active: overlayWindow.shouldShowSpotlight && spotlightContainer.opacity > 0 blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5 diff --git a/quickshell/Services/BlurService.qml b/quickshell/Services/BlurService.qml index 41a37a4b1..5ca4efc0f 100644 --- a/quickshell/Services/BlurService.qml +++ b/quickshell/Services/BlurService.qml @@ -17,7 +17,7 @@ Singleton { // These settings predate non-blurred surface borders, so keep their keys for compatibility. readonly property color borderColor: { - if (!(SettingsData.blurBorderEnabled ?? true)) + if (!(SettingsData.blurBorderEnabled ?? false)) return "transparent"; const opacity = SettingsData.blurBorderOpacity ?? 0.35; switch (SettingsData.blurBorderColor ?? "outline") { @@ -33,7 +33,7 @@ Singleton { return Theme.withAlpha(Theme.outline, opacity); } } - readonly property int borderWidth: (SettingsData.blurBorderEnabled ?? true) ? 1 : 0 + readonly property int borderWidth: (SettingsData.blurBorderEnabled ?? false) ? 1 : 0 function hoverColor(baseColor, hoverAlpha) { if (!enabled) diff --git a/quickshell/Widgets/DankContextMenu.qml b/quickshell/Widgets/DankContextMenu.qml index 0497a9679..af291def5 100644 --- a/quickshell/Widgets/DankContextMenu.qml +++ b/quickshell/Widgets/DankContextMenu.qml @@ -198,6 +198,7 @@ Item { WindowBlur { targetWindow: menuWindow + surfaceColor: menuContainer.color blurX: root.blurActive ? menuContainer.x : 0 blurY: root.blurActive ? menuContainer.y : 0 blurWidth: root.blurActive ? menuContainer.width : 0 diff --git a/quickshell/Widgets/DankFloatingWindow.qml b/quickshell/Widgets/DankFloatingWindow.qml index 28b02cc1b..6e4ce43f4 100644 --- a/quickshell/Widgets/DankFloatingWindow.qml +++ b/quickshell/Widgets/DankFloatingWindow.qml @@ -22,6 +22,7 @@ FloatingWindow { visible: root.contentVisible } + // No surfaceColor gate: compositor window rules can make this window translucent. WindowBlur { targetWindow: root blurEnabled: root.contentVisible && Theme.connectedSurfaceBlurEnabled diff --git a/quickshell/Widgets/DankOSD.qml b/quickshell/Widgets/DankOSD.qml index b36a5550f..cd708ddfb 100644 --- a/quickshell/Widgets/DankOSD.qml +++ b/quickshell/Widgets/DankOSD.qml @@ -118,6 +118,7 @@ PanelWindow { WindowBlur { targetWindow: root + surfaceColor: Theme.withAlpha(root.surfaceColor, osdContainer.popupSurfaceAlpha) blurX: shadowBuffer blurY: shadowBuffer blurWidth: presented ? alignedWidth : 0 diff --git a/quickshell/Widgets/DankPopoutHost.qml b/quickshell/Widgets/DankPopoutHost.qml index c4fc52518..c92779434 100644 --- a/quickshell/Widgets/DankPopoutHost.qml +++ b/quickshell/Widgets/DankPopoutHost.qml @@ -1667,6 +1667,7 @@ Item { id: popoutBlur targetWindow: contentWindow blurEnabled: Theme.connectedSurfaceBlurEnabled && !root.frameOwnsConnectedChrome + surfaceColor: chrome.surfaceColor readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property bool trackBlurFromBarEdge: root.usesConnectedSurfaceChrome @@ -1909,6 +1910,7 @@ Item { WindowBlur { id: popoutBlur targetWindow: contentWindow + surfaceColor: Theme.readableSurface readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real op: Math.max(0, Math.min(1, (morph.value - 0.08) * 1.6)) readonly property real visibleScale: s * op diff --git a/quickshell/Widgets/DankSlideout.qml b/quickshell/Widgets/DankSlideout.qml index b00d23e7a..cd65af633 100644 --- a/quickshell/Widgets/DankSlideout.qml +++ b/quickshell/Widgets/DankSlideout.qml @@ -302,6 +302,7 @@ PanelWindow { WindowBlur { targetWindow: root + surfaceColor: contentRect.slideoutSurfaceColor blurX: root.slideoutBlurActive ? slideContainer.x + root.slideoutSlideSnapX : 0 blurY: root.slideoutBlurActive ? slideContainer.y : 0 blurWidth: root.slideoutBlurActive ? slideContainer.width : 0 diff --git a/quickshell/Widgets/WindowBlur.qml b/quickshell/Widgets/WindowBlur.qml index bf59e187f..196f86dcc 100644 --- a/quickshell/Widgets/WindowBlur.qml +++ b/quickshell/Widgets/WindowBlur.qml @@ -11,6 +11,7 @@ Item { required property var targetWindow property bool blurEnabled: Theme.connectedSurfaceBlurEnabled + property color surfaceColor: "transparent" property real blurX: 0 property real blurY: 0 property real blurWidth: 0 @@ -23,7 +24,7 @@ Item { property real clipWidth: blurWidth property real clipHeight: blurHeight - readonly property bool _active: blurEnabled && BlurService.enabled && !!targetWindow + readonly property bool _active: blurEnabled && surfaceColor.a < 1 && BlurService.enabled && !!targetWindow Region { id: blurRegion @@ -84,7 +85,7 @@ Item { onClipHeightChanged: settleKickAction.restart() function _runSettleKick() { - if (!BlurService.compositorSupported || !targetWindow?.visible) + if (!_active || !targetWindow?.visible) return; kick(); settleRepeatTimer.restart(); @@ -101,7 +102,7 @@ Item { interval: 96 repeat: false onTriggered: { - if (!root.targetWindow?.visible) + if (!root._active || !root.targetWindow?.visible) return; root.kick(); } diff --git a/quickshell/matugen/configs/pywalfox.toml b/quickshell/matugen/configs/pywalfox.toml index ef987a9a4..42082b057 100644 --- a/quickshell/matugen/configs/pywalfox.toml +++ b/quickshell/matugen/configs/pywalfox.toml @@ -5,4 +5,4 @@ output_path = 'CACHE_DIR/wal/dank-pywalfox.json' [templates.dmspywal] input_path = 'SHELL_DIR/matugen/templates/pywalfox-colors.json' output_path = 'CACHE_DIR/wal/colors.json' -post_hook = 'sh -c "command -v pywalfox >/dev/null 2>&1 && pywalfox {{ mode }} && pywalfox update"' +post_hook = 'sh -c "command -v pywalfox >/dev/null 2>&1 && (pywalfox {{ mode }} && pywalfox update) >/dev/null 2>&1 &"' diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 4ad4088a3..10e73b1c2 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -442,8 +442,7 @@ "style", "surface" ], - "page": "theme_surfaces", - "icon": "border_style" + "page": "theme_surfaces" }, { "section": "blurBorderColor",