mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 00:34:01 -04:00
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:
@@ -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)" },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
+2
-2
@@ -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() {}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
Submodule dank-qml-common updated: 0de254e8dd...17e81ad63e
Generated
+3
-3
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -118,6 +118,7 @@ PanelWindow {
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: root
|
||||
surfaceColor: Theme.withAlpha(root.surfaceColor, osdContainer.popupSurfaceAlpha)
|
||||
blurX: shadowBuffer
|
||||
blurY: shadowBuffer
|
||||
blurWidth: presented ? alignedWidth : 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user