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
This commit is contained in:
bbedward
2026-09-21 11:47:52 -04:00
parent 1c683e932f
commit 0bd3e85150
43 changed files with 470 additions and 242 deletions
@@ -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)" },
-1
View File
@@ -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
}
@@ -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)
}
@@ -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() {}
+107 -54
View File
@@ -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 {
+1 -1
View File
@@ -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
+4
View File
@@ -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
+102
View File
@@ -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)
}
}
+34
View File
@@ -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")
}
+3 -1
View File
@@ -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()
}
+36 -3
View File
@@ -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 {
+39 -68
View File
@@ -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")
}
}
Generated
+3 -3
View File
@@ -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": {
+2
View File
@@ -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
@@ -357,6 +357,9 @@ var LOCAL_SPEC = {
blurEnabled: {
def: false
},
blurBorderSeeded: {
def: false
},
blurForegroundLayers: {
def: true
},
@@ -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;
}
@@ -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
@@ -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
@@ -31,8 +31,9 @@ FocusScope {
function _focusPage() {
Qt.callLater(() => {
if (sessionVisible && currentPageItem)
currentPageItem.forceActiveFocus();
if (!sessionVisible || !currentPageItem || parentModal?.searchFocused)
return;
currentPageItem.forceActiveFocus();
});
}
@@ -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: ""
@@ -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;
@@ -80,6 +80,7 @@ Row {
WindowBlur {
targetWindow: addWidgetWindow
surfaceColor: widgetLibraryPanel.color
blurX: widgetLibraryPanel.x
blurY: widgetLibraryPanel.y
blurWidth: addWidgetWindow.visible ? widgetLibraryPanel.width : 0
@@ -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
@@ -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
+1
View File
@@ -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
@@ -74,6 +74,7 @@ PanelWindow {
WindowBlur {
targetWindow: root
surfaceColor: menuContainer.color
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.visible ? menuContainer.width : 0
@@ -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)
@@ -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"]
@@ -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 {
@@ -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 {
@@ -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 => {
+1
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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)
+1
View File
@@ -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
@@ -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
+1
View File
@@ -118,6 +118,7 @@ PanelWindow {
WindowBlur {
targetWindow: root
surfaceColor: Theme.withAlpha(root.surfaceColor, osdContainer.popupSurfaceAlpha)
blurX: shadowBuffer
blurY: shadowBuffer
blurWidth: presented ? alignedWidth : 0
+2
View File
@@ -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
+1
View File
@@ -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
+4 -3
View File
@@ -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();
}
+1 -1
View File
@@ -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 &"'
@@ -442,8 +442,7 @@
"style",
"surface"
],
"page": "theme_surfaces",
"icon": "border_style"
"page": "theme_surfaces"
},
{
"section": "blurBorderColor",