1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00

Compare commits

...

26 Commits

Author SHA1 Message Date
dms-ci[bot] 069ddab041 bump VERSION to v1.5.3 2026-07-27 00:17:35 +00:00
bbedward 45cf6ecefd sysupdate: fix gnome-terminal title
(cherry picked from commit c367153bac)
2026-07-26 19:51:46 -04:00
bbedward 7058b00091 cups: fix sub/unsub race
port 1.5

(cherry picked from commit bab2078dfd)
2026-07-26 23:43:53 +00:00
bbedward 6ba4b79039 port: record hand-ported master equivalents
(cherry picked from commit e9bc0169f6)
(cherry picked from commit 9b7d3c64fe)
(cherry picked from commit ea03fb2788)
(cherry picked from commit 0815e48465)
(cherry picked from commit 3938e60ce4)
(cherry picked from commit 3c688cfbd3)
(cherry picked from commit e63d210358)
(cherry picked from commit fa094db127)
2026-07-26 19:32:23 -04:00
bbedward b8e2ce1da8 dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926)
(cherry picked from commit fa629fbf99)
2026-07-26 19:15:44 -04:00
bbedward c58d55db7d bluetooth: add soft rfkill unblock, wait for adapter to become available
instead of forfeiting subscription

fixes #2922
fixes #1537

(cherry picked from commit 77a357109a)
2026-07-26 19:15:02 -04:00
bbedward e15db00714 cc: migrate some settings to cache
port 1.5

(cherry picked from commit e9bc0169f6)
2026-07-26 21:46:22 +00:00
bbedward 906eba5c17 battery: auto-apply power profile at startup when DMS managed
fixes #2935
port 1.5

(cherry picked from commit 59c83d19e4)
2026-07-26 19:27:56 +00:00
bbedward d2b61b91ef dbar: fix click-through mask of center section
fixes #2938

(cherry picked from commit 9b7d3c64fe)
2026-07-26 15:22:03 -04:00
bbedward 6c27b92445 dbar/notifications: fix right click context menu position and add middle
click to DnD

(cherry picked from commit 1f94c3cbd4)
2026-07-26 14:52:39 -04:00
bbedward a64bd80cc9 window-rules/niri: make floating a tri-state
(cherry picked from commit 814871d982)
2026-07-26 14:34:50 -04:00
feng-yifan 661dc27d26 fix(settings): reset AppBrowserPopup model on close to avoid crash (#2925)
Reopening the autostart "Browse" picker crashed in
QQmlIncubatorPrivate::incubate. Drop the ListView model on hide()
and rebind it on show() so each open starts with a fresh
QQmlDelegateModel instead of reusing a stale incubation queue.

(cherry picked from commit 3182c70857)
2026-07-26 14:34:30 -04:00
bbedward 3d5a6fb9d7 display config: preserve config when turning off on niri
fixes #2939
port 1.5

(cherry picked from commit 24cb3d19a0)
2026-07-26 18:26:18 +00:00
bbedward c2a5397d8a notification: clip region on animation exit
related #2917
port 1.5

(cherry picked from commit f57f254855)
2026-07-24 18:36:07 +00:00
bbedward 9b5538323d lock: guard dpms window 2026-07-24 14:13:43 -04:00
bbedward 8321d28d18 greeter: don't override manually selected session with saved/defaul 2026-07-24 11:47:59 -04:00
bbedward 39278860c8 cups: fix initialization race
port 1.5

(cherry picked from commit ea03fb2788)
2026-07-24 10:58:41 -04:00
bbedward f455456ff1 dock: limit input mask to body, disable hover headroom unless enabled
fixes #2923

(cherry picked from commit 0815e48465)
2026-07-24 10:35:40 -04:00
bbedward 5a80a32951 network: fix networkPreference reverting from eth to wifi
fixes #2919
port 1.5

(cherry picked from commit 42060366cb)
2026-07-24 14:14:08 +00:00
Aldi Prasetyo 3cedd56f23 fix(minimal-install): add hyprland packages to minimal install preference (#2872)
(I guess we'll find out if this breaks any minimal script to desktop runs)

(cherry picked from commit ff71502083)
2026-07-24 09:31:46 -04:00
Legend-017 dc1ad5b329 Fix(spotlight): improve file search query restoration and mode switching (#2887)
* fix(spotlight): fix empty results on restoring last file search query

- When "remember last query" is enabled and the last query is file search, it shows "no results" until the query is manually modified in full launcher mode.

* fix(spotlight): properly restore last selected mode(tab) when enabled in full launcher mode

* fix(spotlight): allow file trigger in plugins mode(tab)

* fix(spotlight): restore previous mode when clearing '/' file search prefix

When the '/' prefix is removed from the query, the controller now correctly reverts to the previous mode instead of remaining stuck in "files" mode.

* fix(spotlight): code review fix

(cherry picked from commit 367ad5f69a)
2026-07-24 09:31:32 -04:00
Thomas Kroll 25c12a14a9 fix(lock): hold sleep inhibitor until compositor confirms lock (#2907)
lock-before-suspend released the logind sleep delay inhibitor as soon as Qt
reported the lock rendered (afterAnimating/afterRendering). That signal fires
before the compositor has committed/presented the ext-session-lock surface, so
the machine could freeze ~1 frame later with the desktop still the last
presented frame. On resume the desktop was briefly visible before the lock
appeared (both suspend and hibernate).

Gate lockerReady on WlSessionLock.secure (the ext-session-lock `locked` event),
so the inhibitor is held until the compositor confirms the session is locked and
the desktop is hidden. LockSurface passes the WlSessionLock down as sessionLock;
the readiness check returns early until secure, and re-fires on secureChanged.
The Go-side fallback timer in loginctl monitor remains as the backstop.

Reproduced on niri / s2idle with 3 outputs: debug log showed lockerReady sent
18 ms before `PM: suspend entry`; with this change the desktop no longer flashes
on resume.

Signed-off-by: Thomas Kroll <99196436+tkroll-ionos@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 237fb57d5e)
2026-07-24 09:31:23 -04:00
bbedward b72b276fb4 lock/pam: attempt to fix fprintd retry spam after resume
(cherry picked from commit 958672badd)
2026-07-24 09:30:57 -04:00
bbedward f89fb67316 settings: fix close IPC
fixes #2912
port 1.5

(cherry picked from commit 5854327c28)
2026-07-23 18:10:19 +00:00
bbedward f457e04cb1 calendar/dcal: fix flatpak socket detection
port 1.5

(cherry picked from commit d818067bee)
2026-07-23 14:14:49 +00:00
bbedward 23daca0b57 location: prevent calls when weather auto-locate is disabled
port 1.5

(cherry picked from commit fe561f2b5d)
2026-07-22 03:36:01 +00:00
64 changed files with 1669 additions and 233 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ type minimalInstallGroup struct {
func shouldPreferMinimalInstall(pkg string) bool { func shouldPreferMinimalInstall(pkg string) bool {
switch pkg { switch pkg {
case "niri", "niri-git": case "niri", "niri-git", "hyprland", "hyprland-git":
return true return true
default: default:
return false return false
+11 -6
View File
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
broker := NewSubscriptionBroker(m.broadcastPairingPrompt) broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
m.promptBroker = broker m.promptBroker = broker
adapter, err := m.findAdapter() adapter, err := findAdapter(conn)
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, fmt.Errorf("no bluetooth adapter found: %w", err) return nil, err
} }
m.adapterPath = adapter m.adapterPath = adapter
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func (m *Manager) findAdapter() (dbus.ObjectPath, error) { func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/")) obj := conn.Object(bluezService, dbus.ObjectPath("/"))
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
return "", err return "", fmt.Errorf("%w: %v", ErrNoAdapter, err)
} }
for path, interfaces := range objects { for path, interfaces := range objects {
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
} }
} }
return "", fmt.Errorf("no adapter found") return "", ErrNoAdapter
} }
func (m *Manager) initialize() error { func (m *Manager) initialize() error {
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error {
} }
func (m *Manager) SetPowered(powered bool) error { func (m *Manager) SetPowered(powered bool) error {
if powered {
if err := rfkillUnblockBluetooth(); err != nil {
log.Debugf("[BluezManager] rfkill unblock failed: %v", err)
}
}
obj := m.dbusConn.Object(bluezService, m.adapterPath) obj := m.dbusConn.Object(bluezService, m.adapterPath)
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
} }
@@ -0,0 +1,5 @@
package bluez
func rfkillUnblockBluetooth() error {
return nil
}
@@ -0,0 +1,19 @@
package bluez
import "os"
// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; },
// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3
func rfkillUnblockBluetooth() error {
f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()
var event [8]byte
event[4] = 2
event[5] = 3
_, err = f.Write(event[:])
return err
}
+46
View File
@@ -0,0 +1,46 @@
package bluez
import (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
)
var ErrNoAdapter = errors.New("no bluetooth adapter found")
func WaitForAdapter() error {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return err
}
defer conn.Close()
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(objectMgrIface),
dbus.WithMatchMember("InterfacesAdded"),
); err != nil {
return err
}
signals := make(chan *dbus.Signal, 64)
conn.Signal(signals)
if _, err := findAdapter(conn); err == nil {
return nil
}
for sig := range signals {
if sig == nil || sig.Name != objectMgrIface+".InterfacesAdded" || len(sig.Body) < 2 {
continue
}
ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant)
if !ok {
continue
}
if _, ok := ifaces[adapter1Iface]; ok {
return nil
}
}
return fmt.Errorf("dbus signal stream closed")
}
+23 -10
View File
@@ -229,6 +229,9 @@ func (m *Manager) snapshotState() CUPSState {
func (m *Manager) Subscribe(id string) chan CUPSState { func (m *Manager) Subscribe(id string) chan CUPSState {
ch := make(chan CUPSState, 64) ch := make(chan CUPSState, 64)
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
wasEmpty := true wasEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool { m.subscribers.Range(func(key string, ch chan CUPSState) bool {
wasEmpty = false wasEmpty = false
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
m.subscribers.Store(id, ch) m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil { if !wasEmpty || m.subscription == nil {
if err := m.subscription.Start(); err != nil { return ch
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
} else {
m.eventWG.Add(1)
go m.eventHandler()
}
} }
if err := m.subscription.Start(); err != nil {
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
return ch
}
m.eventWG.Add(1)
go m.eventHandler()
return ch return ch
} }
func (m *Manager) Unsubscribe(id string) { func (m *Manager) Unsubscribe(id string) {
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
if val, ok := m.subscribers.LoadAndDelete(id); ok { if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val) close(val)
} }
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
return false return false
}) })
if isEmpty && m.subscription != nil { if !isEmpty || m.subscription == nil {
m.subscription.Stop() return
m.eventWG.Wait()
} }
m.subscription.Stop()
m.eventWG.Wait()
} }
func (m *Manager) Close() { func (m *Manager) Close() {
close(m.stopChan) close(m.stopChan)
m.subLifecycleMu.Lock()
if m.subscription != nil { if m.subscription != nil {
m.subscription.Stop() m.subscription.Stop()
} }
m.subLifecycleMu.Unlock()
m.eventWG.Wait() m.eventWG.Wait()
m.notifierWg.Wait() m.notifierWg.Wait()
+64
View File
@@ -1,6 +1,9 @@
package cups package cups
import ( import (
"errors"
"fmt"
"sync"
"testing" "testing"
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
assert.Equal(t, 0, count) assert.Equal(t, 0, count)
} }
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
// unsynchronized so overlapping Start/Stop trips the race detector
type stubSubscription struct {
mu sync.Mutex
events chan SubscriptionEvent
conn *int
running bool
}
func (s *stubSubscription) Start() error {
if s.running {
return errors.New("already running")
}
s.running = true
s.mu.Lock()
s.events = make(chan SubscriptionEvent)
s.mu.Unlock()
v := 0
s.conn = &v
*s.conn++
return nil
}
func (s *stubSubscription) Stop() {
if !s.running {
return
}
s.running = false
s.conn = nil
s.mu.Lock()
close(s.events)
s.mu.Unlock()
}
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
s.mu.Lock()
defer s.mu.Unlock()
return s.events
}
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
m.subscription = &stubSubscription{}
var wg sync.WaitGroup
for i := range 8 {
wg.Go(func() {
id := fmt.Sprintf("client-%d", i)
for range 50 {
m.Subscribe(id)
m.Unsubscribe(id)
}
})
}
wg.Wait()
}
func TestManager_Close(t *testing.T) { func TestManager_Close(t *testing.T) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient := mocks_cups.NewMockCUPSClientInterface(t)
+1
View File
@@ -79,6 +79,7 @@ type Manager struct {
client CUPSClientInterface client CUPSClientInterface
pkHelper PkHelper pkHelper PkHelper
subscription SubscriptionManagerInterface subscription SubscriptionManagerInterface
subLifecycleMu sync.Mutex
stateMutex sync.RWMutex stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan CUPSState] subscribers syncmap.Map[string, chan CUPSState]
stopChan chan struct{} stopChan chan struct{}
+3 -2
View File
@@ -117,11 +117,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
} }
if strings.HasPrefix(req.Method, "cups.") { if strings.HasPrefix(req.Method, "cups.") {
if cupsManager == nil { mgr, err := ensureCupsManager()
if err != nil {
models.RespondError(conn, req.ID, "CUPS manager not initialized") models.RespondError(conn, req.ID, "CUPS manager not initialized")
return return
} }
cups.HandleRequest(conn, req, cupsManager) cups.HandleRequest(conn, req, mgr)
return return
} }
+80 -36
View File
@@ -12,7 +12,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@@ -85,8 +84,8 @@ var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
var capabilitySubscribers syncmap.Map[string, chan ServerInfo] var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
var cupsSubscribers syncmap.Map[string, bool] var cupsMu sync.Mutex
var cupsSubscriberCount atomic.Int32 var cupsSubscriberCount int
func getSocketDir() string { func getSocketDir() string {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
@@ -243,17 +242,55 @@ func InitializeAppPickerManager() error {
return nil return nil
} }
func InitializeCupsManager() error { func initializeCupsManagerLocked() (bool, error) {
if cupsManager != nil {
return false, nil
}
manager, err := cups.NewManager() manager, err := cups.NewManager()
if err != nil { if err != nil {
log.Warnf("Failed to initialize cups manager: %v", err) log.Warnf("Failed to initialize cups manager: %v", err)
return err return false, err
} }
cupsManager = manager cupsManager = manager
log.Info("CUPS manager initialized") log.Info("CUPS manager initialized")
return nil return true, nil
}
func ensureCupsManager() (*cups.Manager, error) {
cupsMu.Lock()
created, err := initializeCupsManagerLocked()
mgr := cupsManager
cupsMu.Unlock()
if err != nil {
return nil, err
}
if created {
notifyCapabilityChange()
}
return mgr, nil
}
func cupsAvailable() bool {
cupsMu.Lock()
defer cupsMu.Unlock()
return cupsManager != nil
}
func releaseCupsSubscriber() {
cupsMu.Lock()
cupsSubscriberCount--
var mgr *cups.Manager
if cupsSubscriberCount == 0 && cupsManager != nil {
mgr = cupsManager
cupsManager = nil
}
cupsMu.Unlock()
if mgr == nil {
return
}
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
mgr.Close()
notifyCapabilityChange()
} }
func InitializeBrightnessManager() error { func InitializeBrightnessManager() error {
@@ -463,7 +500,7 @@ func getCapabilities() Capabilities {
caps = append(caps, "browser") caps = append(caps, "browser")
} }
if cupsManager != nil { if cupsAvailable() {
caps = append(caps, "cups") caps = append(caps, "cups")
} }
@@ -533,7 +570,7 @@ func getServerInfo() ServerInfo {
caps = append(caps, "browser") caps = append(caps, "browser")
} }
if cupsManager != nil { if cupsAvailable() {
caps = append(caps, "cups") caps = append(caps, "cups")
} }
@@ -997,38 +1034,31 @@ func handleSubscribe(conn net.Conn, req models.Request) {
} }
if shouldSubscribe("cups") { if shouldSubscribe("cups") {
cupsSubscribers.Store(clientID+"-cups", true) cupsMu.Lock()
count := cupsSubscriberCount.Add(1) cupsSubscriberCount++
created, err := initializeCupsManagerLocked()
mgr := cupsManager
cupsMu.Unlock()
if count == 1 { if err != nil {
if err := InitializeCupsManager(); err != nil { log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) } else if created {
} else { notifyCapabilityChange()
notifyCapabilityChange()
}
} }
if cupsManager != nil { if mgr == nil {
releaseCupsSubscriber()
} else {
wg.Add(1) wg.Add(1)
cupsChan := cupsManager.Subscribe(clientID + "-cups") cupsChan := mgr.Subscribe(clientID + "-cups")
go func() { go func() {
defer wg.Done() defer wg.Done()
defer func() { defer func() {
cupsManager.Unsubscribe(clientID + "-cups") mgr.Unsubscribe(clientID + "-cups")
cupsSubscribers.Delete(clientID + "-cups") releaseCupsSubscriber()
count := cupsSubscriberCount.Add(-1)
if count == 0 {
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
if cupsManager != nil {
cupsManager.Close()
cupsManager = nil
notifyCapabilityChange()
}
}
}() }()
initialState := cupsManager.GetState() initialState := mgr.GetState()
select { select {
case eventChan <- ServiceEvent{Service: "cups", Data: initialState}: case eventChan <- ServiceEvent{Service: "cups", Data: initialState}:
case <-stopChan: case <-stopChan:
@@ -1338,9 +1368,12 @@ func cleanupManagers() {
if appPickerManager != nil { if appPickerManager != nil {
appPickerManager.Close() appPickerManager.Close()
} }
cupsMu.Lock()
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
cupsManager = nil
} }
cupsMu.Unlock()
if brightnessManager != nil { if brightnessManager != nil {
brightnessManager.Close() brightnessManager.Close()
} }
@@ -1698,10 +1731,21 @@ func Start(printDocs bool) error {
}() }()
go func() { go func() {
if err := InitializeBluezManager(); err != nil { for {
err := InitializeBluezManager()
if err == nil {
notifyCapabilityChange()
return
}
log.Warnf("Bluez manager unavailable: %v", err) log.Warnf("Bluez manager unavailable: %v", err)
} else { if !errors.Is(err, bluez.ErrNoAdapter) {
notifyCapabilityChange() return
}
if err := bluez.WaitForAdapter(); err != nil {
log.Warnf("Bluetooth adapter watch failed: %v", err)
return
}
log.Info("Bluetooth adapter appeared, initializing bluez manager")
} }
}() }()
+2 -1
View File
@@ -143,7 +143,8 @@ func wrapInTerminal(term, title, shellCmd string, extraArgs []string) []string {
case "konsole": case "konsole":
argv = []string{term, "-p", "tabtitle=" + title} argv = []string{term, "-p", "tabtitle=" + title}
case "gnome-terminal": case "gnome-terminal":
argv = []string{term, "--title=" + title} // --wait: the factory process otherwise returns immediately
argv = []string{term, "--wait", "--title=" + title}
execFlag = "--" execFlag = "--"
default: default:
argv = []string{term} argv = []string{term}
+14 -9
View File
@@ -412,25 +412,30 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, opts UpgradeOptions) {
onLine := func(line string) { m.appendLog(line) } onLine := func(line string) { m.appendLog(line) }
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs) argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil { if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
code := ErrCodeBackendFailed
switch { switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded): case errors.Is(ctx.Err(), context.DeadlineExceeded):
code = ErrCodeTimeout m.failCustomUpgrade(ErrCodeTimeout, err)
return
case errors.Is(ctx.Err(), context.Canceled): case errors.Is(ctx.Err(), context.Canceled):
code = ErrCodeCancelled m.failCustomUpgrade(ErrCodeCancelled, err)
return
} }
m.mu.Lock() // exit status reflects the trailing `read`, not the update command
m.state.Phase = PhaseError m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
return
} }
m.finishSuccessfulUpgrade(false) m.finishSuccessfulUpgrade(false)
m.runRefresh(context.Background(), false) m.runRefresh(context.Background(), false)
} }
func (m *Manager) failCustomUpgrade(code ErrorCode, err error) {
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) { func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
m.appendLog("Upgrade complete.") m.appendLog("Upgrade complete.")
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got) t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
} }
} }
func TestWrapInTerminal(t *testing.T) {
tests := []struct {
term string
wantPrefix []string
}{
{"kitty", []string{"kitty", "--class", "com.danklinux.dms", "-T", "Title"}},
{"gnome-terminal", []string{"gnome-terminal", "--wait", "--title=Title"}},
{"foot", []string{"foot", "--app-id=com.danklinux.dms", "--title=Title"}},
}
for _, tt := range tests {
got := wrapInTerminal(tt.term, "Title", "echo hi", nil)
if len(got) < len(tt.wantPrefix) || !reflect.DeepEqual(got[:len(tt.wantPrefix)], tt.wantPrefix) {
t.Errorf("wrapInTerminal(%q) = %#v, want prefix %#v", tt.term, got, tt.wantPrefix)
}
tail := got[len(got)-3:]
if tail[0] != "sh" || tail[1] != "-c" {
t.Errorf("wrapInTerminal(%q) tail = %#v, want [sh -c <cmd>]", tt.term, tail)
}
if !strings.Contains(tail[2], "echo hi") {
t.Errorf("wrapInTerminal(%q) command %q does not contain shell command", tt.term, tail[2])
}
}
}
@@ -928,8 +928,8 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
if a.Opacity != nil { if a.Opacity != nil {
lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity)) lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity))
} }
if a.OpenFloating != nil && *a.OpenFloating { if a.OpenFloating != nil {
lines = append(lines, " open-floating true") lines = append(lines, fmt.Sprintf(" open-floating %t", *a.OpenFloating))
} }
if a.OpenMaximized != nil && *a.OpenMaximized { if a.OpenMaximized != nil && *a.OpenMaximized {
lines = append(lines, " open-maximized true") lines = append(lines, " open-maximized true")
+64 -6
View File
@@ -11,7 +11,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("CacheData") readonly property var log: Log.scoped("CacheData")
readonly property int cacheConfigVersion: 1 readonly property int cacheConfigVersion: 2
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -19,10 +19,21 @@ Singleton {
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
property bool _loading: false property bool _loading: false
property bool _hasLoaded: false
property int _loadedCacheVersion: 0
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys)
property string wallpaperLastPath: "" property string wallpaperLastPath: ""
property string profileLastPath: "" property string profileLastPath: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property var fileBrowserSettings: ({ property var fileBrowserSettings: ({
"wallpaper": { "wallpaper": {
"lastPath": "", "lastPath": "",
@@ -82,8 +93,46 @@ Singleton {
function loadCache() { function loadCache() {
_loading = true; _loading = true;
parseCache(cacheFile.text()); try {
_loading = false; parseCache(cacheFile.text());
} finally {
_loading = false;
_hasLoaded = true;
}
}
function set(key, value) {
if (_dataKeys.indexOf(key) < 0) {
log.warn("Unknown cache key:", key);
return;
}
root[key] = value;
saveCache();
}
function migratePins(pins) {
if (!pins)
return;
if (!_hasLoaded)
loadCache();
if (_loadedCacheVersion >= cacheConfigVersion)
return;
let migrated = false;
for (const key of _pinKeys) {
const legacy = pins[key];
if (!legacy || Object.keys(legacy).length === 0)
continue;
if (Object.keys(root[key] || {}).length > 0)
continue;
root[key] = legacy;
migrated = true;
}
if (!migrated)
return;
log.info("Migrated device pins from settings.json");
saveCache();
} }
function parseCache(content) { function parseCache(content) {
@@ -91,6 +140,7 @@ Singleton {
try { try {
if (content && content.trim()) { if (content && content.trim()) {
const cache = JSON.parse(content); const cache = JSON.parse(content);
_loadedCacheVersion = cache.configVersion || 0;
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : ""; wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "";
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : ""; profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "";
@@ -126,6 +176,10 @@ Singleton {
}; };
} }
for (const key of _pinKeys) {
root[key] = cache[key] !== undefined ? cache[key] : {};
}
if (cache.configVersion === undefined) { if (cache.configVersion === undefined) {
migrateFromUndefinedToV1(cache); migrateFromUndefinedToV1(cache);
cleanupUnusedKeys(); cleanupUnusedKeys();
@@ -142,12 +196,16 @@ Singleton {
function saveCache() { function saveCache() {
if (_loading) if (_loading)
return; return;
cacheFile.setText(JSON.stringify({ const data = {
"wallpaperLastPath": wallpaperLastPath, "wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath, "profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings, "fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion "configVersion": cacheConfigVersion
}, null, 2)); };
for (const key of _pinKeys) {
data[key] = root[key];
}
cacheFile.setText(JSON.stringify(data, null, 2));
} }
function migrateFromUndefinedToV1(cache) { function migrateFromUndefinedToV1(cache) {
@@ -155,7 +213,7 @@ Singleton {
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
const validKeys = ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "configVersion"]; const validKeys = _dataKeys.concat(["configVersion"]);
try { try {
const content = cacheFile.text(); const content = cacheFile.text();
+4 -6
View File
@@ -15,7 +15,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("SettingsData") readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 12 readonly property int settingsConfigVersion: 13
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -773,11 +773,6 @@ Singleton {
property bool fadeToDpmsEnabled: true property bool fadeToDpmsEnabled: true
property int fadeToDpmsGracePeriod: 5 property int fadeToDpmsGracePeriod: 5
property string launchPrefix: "" property string launchPrefix: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property bool gtkThemingEnabled: false property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false property bool qtThemingEnabled: false
@@ -1741,6 +1736,7 @@ Singleton {
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null; let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
const oldVersion = obj?.configVersion ?? 0; const oldVersion = obj?.configVersion ?? 0;
const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null;
if (oldVersion < settingsConfigVersion) { if (oldVersion < settingsConfigVersion) {
const migrated = Store.migrateToVersion(obj, settingsConfigVersion); const migrated = Store.migrateToVersion(obj, settingsConfigVersion);
if (migrated) { if (migrated) {
@@ -1748,6 +1744,8 @@ Singleton {
obj = migrated; obj = migrated;
} }
} }
if (legacyPins)
Qt.callLater(() => CacheData.migratePins(legacyPins));
if (obj?.lockScreenActiveMonitor !== undefined) { if (obj?.lockScreenActiveMonitor !== undefined) {
var oldVal = obj.lockScreenActiveMonitor; var oldVal = obj.lockScreenActiveMonitor;
@@ -354,11 +354,6 @@ var SPEC = {
fadeToDpmsEnabled: { def: true }, fadeToDpmsEnabled: { def: true },
fadeToDpmsGracePeriod: { def: 5 }, fadeToDpmsGracePeriod: { def: 5 },
launchPrefix: { def: "" }, launchPrefix: { def: "" },
brightnessDevicePins: { def: {} },
wifiNetworkPins: { def: {} },
bluetoothDevicePins: { def: {} },
audioInputDevicePins: { def: {} },
audioOutputDevicePins: { def: {} },
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" }, gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" }, qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
@@ -2,6 +2,21 @@
.import "./SettingsSpec.js" as SpecModule .import "./SettingsSpec.js" as SpecModule
var PIN_KEYS = ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"];
function extractPins(obj) {
if (!obj) return null;
var pins = null;
for (var i = 0; i < PIN_KEYS.length; i++) {
var value = obj[PIN_KEYS[i]];
if (!value || Object.keys(value).length === 0) continue;
if (!pins) pins = {};
pins[PIN_KEYS[i]] = value;
}
return pins;
}
function parse(root, jsonObj) { function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC; var SPEC = SpecModule.SPEC;
@@ -263,6 +278,17 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 12; settings.configVersion = 12;
} }
if (currentVersion < 13) {
console.info("Migrating settings from version", currentVersion, "to version 13");
console.info("Moving device and network pins to cache.json");
for (var p = 0; p < PIN_KEYS.length; p++) {
delete settings[PIN_KEYS[p]];
}
settings.configVersion = 13;
}
return settings; return settings;
} }
@@ -449,7 +449,12 @@ Item {
searchQuery = query; searchQuery = query;
requestSearch(); requestSearch();
if (searchMode !== "plugins" && query.startsWith("/")) { if (autoSwitchedToFiles && !query.startsWith("/")) {
restorePreviousMode();
return;
}
if (query.startsWith("/")) {
var prefix = Utils.parseFileSearchPrefix(query); var prefix = Utils.parseFileSearchPrefix(query);
var explicitType = prefix && prefix.type !== null ? prefix.type : null; var explicitType = prefix && prefix.type !== null ? prefix.type : null;
var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all"); var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all");
@@ -174,9 +174,12 @@ Item {
spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null; spotlightContent.controller.selectedItem = null;
spotlightContent.controller.historyIndex = -1; spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.searchQuery = targetQuery; if (targetQuery) {
spotlightContent.controller.setSearchQuery(targetQuery);
spotlightContent.controller.performSearch(); } else {
spotlightContent.controller.searchQuery = "";
spotlightContent.controller.performSearch();
}
} }
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll(); spotlightContent.resetScroll();
@@ -271,7 +274,7 @@ Item {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode, userInitiated) { function onModeChanged(mode, userInitiated) {
if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps")) if (!userInitiated || !SettingsData.rememberLastMode)
return; return;
SessionData.setLauncherLastMode(mode); SessionData.setLauncherLastMode(mode);
} }
+11 -5
View File
@@ -41,6 +41,9 @@ FloatingWindow {
signal closingModal signal closingModal
function show() { function show() {
if (visible && !backingWindowVisible) {
visible = false;
}
visible = true; visible = true;
} }
@@ -49,7 +52,11 @@ FloatingWindow {
} }
function toggle() { function toggle() {
visible = !visible; if (visible && backingWindowVisible) {
hide();
return;
}
show();
} }
function setTabIndex(tabIndex: int) { function setTabIndex(tabIndex: int) {
@@ -61,13 +68,12 @@ FloatingWindow {
function showWithTab(tabIndex: int) { function showWithTab(tabIndex: int) {
setTabIndex(tabIndex); setTabIndex(tabIndex);
visible = true; show();
} }
function showWithTabName(tabName: string) { function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName); setTabIndex(sidebar.resolveTabIndex(tabName));
setTabIndex(idx); show();
visible = true;
} }
function resolveTabIndex(tabName: string): int { function resolveTabIndex(tabName: string): int {
+17 -1
View File
@@ -52,6 +52,7 @@ FloatingWindow {
condInitialised.triState = 0; condInitialised.triState = 0;
opacityEnabled.checked = false; opacityEnabled.checked = false;
opacitySlider.value = 100; opacitySlider.value = 100;
floatingCond.triState = 0;
floatingToggle.checked = false; floatingToggle.checked = false;
maximizedToggle.checked = false; maximizedToggle.checked = false;
maximizedToEdgesToggle.checked = false; maximizedToEdgesToggle.checked = false;
@@ -164,6 +165,7 @@ FloatingWindow {
opacityEnabled.checked = hasOpacity; opacityEnabled.checked = hasOpacity;
opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100; opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100;
floatingCond.triState = triFromBool(actions.openFloating);
floatingToggle.checked = actions.openFloating || false; floatingToggle.checked = actions.openFloating || false;
maximizedToggle.checked = actions.openMaximized || false; maximizedToggle.checked = actions.openMaximized || false;
maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false; maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false;
@@ -318,7 +320,9 @@ FloatingWindow {
if (opacityEnabled.checked) if (opacityEnabled.checked)
actions.opacity = opacitySlider.value / 100; actions.opacity = opacitySlider.value / 100;
if (floatingToggle.checked) if (isNiri)
applyCond(actions, "openFloating", floatingCond.triState);
else if (floatingToggle.checked)
actions.openFloating = true; actions.openFloating = true;
if (maximizedToggle.checked) if (maximizedToggle.checked)
actions.openMaximized = true; actions.openMaximized = true;
@@ -943,6 +947,17 @@ FloatingWindow {
title: I18n.tr("Window Opening") title: I18n.tr("Window Opening")
} }
Flow {
width: parent.width
spacing: Theme.spacingS
visible: isNiri
MatchCond {
id: floatingCond
label: I18n.tr("Float")
}
}
Flow { Flow {
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
@@ -950,6 +965,7 @@ FloatingWindow {
CheckboxRow { CheckboxRow {
id: floatingToggle id: floatingToggle
label: I18n.tr("Float") label: I18n.tr("Float")
visible: !isNiri
} }
CheckboxRow { CheckboxRow {
id: maximizedToggle id: maximizedToggle
@@ -159,7 +159,7 @@ Rectangle {
} }
function getPinnedInputs() { function getPinnedInputs() {
const pins = SettingsData.audioInputDevicePins || {}; const pins = CacheData.audioInputDevicePins || {};
return normalizePinList(pins["preferredInput"]); return normalizePinList(pins["preferredInput"]);
} }
@@ -315,7 +315,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.audioInputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredInput"]); let pinnedList = audioContent.normalizePinList(pins["preferredInput"]);
const pinIndex = pinnedList.indexOf(modelData.name); const pinIndex = pinnedList.indexOf(modelData.name);
@@ -332,7 +332,7 @@ Rectangle {
else else
delete pins["preferredInput"]; delete pins["preferredInput"];
SettingsData.set("audioInputDevicePins", pins); CacheData.set("audioInputDevicePins", pins);
} }
} }
} }
@@ -169,7 +169,7 @@ Rectangle {
} }
function getPinnedOutputs() { function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {}; const pins = CacheData.audioOutputDevicePins || {};
return normalizePinList(pins["preferredOutput"]); return normalizePinList(pins["preferredOutput"]);
} }
@@ -324,7 +324,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.audioOutputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]); let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
const pinIndex = pinnedList.indexOf(modelData.name); const pinIndex = pinnedList.indexOf(modelData.name);
@@ -341,7 +341,7 @@ Rectangle {
else else
delete pins["preferredOutput"]; delete pins["preferredOutput"];
SettingsData.set("audioOutputDevicePins", pins); CacheData.set("audioOutputDevicePins", pins);
} }
} }
} }
@@ -112,7 +112,7 @@ Rectangle {
} }
function getPinnedDevices() { function getPinnedDevices() {
const pins = SettingsData.bluetoothDevicePins || {}; const pins = CacheData.bluetoothDevicePins || {};
return normalizePinList(pins["preferredDevice"]); return normalizePinList(pins["preferredDevice"]);
} }
@@ -400,7 +400,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.bluetoothDevicePins || {}));
let pinnedList = root.normalizePinList(pins["preferredDevice"]); let pinnedList = root.normalizePinList(pins["preferredDevice"]);
const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address); const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
@@ -418,7 +418,7 @@ Rectangle {
delete pins["preferredDevice"]; delete pins["preferredDevice"];
} }
SettingsData.set("bluetoothDevicePins", pins); CacheData.set("bluetoothDevicePins", pins);
} }
} }
} }
@@ -37,7 +37,7 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = devices.find(d => d.name === pinnedDevice); const found = devices.find(d => d.name === pinnedDevice);
@@ -85,12 +85,12 @@ Rectangle {
} }
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const existing = pins[pinKey]; const existing = pins[pinKey];
if (existing && existing !== deviceName) { if (existing && existing !== deviceName) {
const next = JSON.parse(JSON.stringify(pins)); const next = JSON.parse(JSON.stringify(pins));
delete next[pinKey]; delete next[pinKey];
SettingsData.set("brightnessDevicePins", next); CacheData.set("brightnessDevicePins", next);
} }
} }
root.currentDeviceName = deviceName; root.currentDeviceName = deviceName;
@@ -106,7 +106,7 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || !deviceName) if (!pinKey || !deviceName)
return false; return false;
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] === deviceName; return pins[pinKey] === deviceName;
} }
@@ -114,13 +114,13 @@ Rectangle {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || !deviceName) if (!pinKey || !deviceName)
return; return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {})); const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
if (pins[pinKey] === deviceName) { if (pins[pinKey] === deviceName) {
delete pins[pinKey]; delete pins[pinKey];
} else { } else {
pins[pinKey] = deviceName; pins[pinKey] = deviceName;
} }
SettingsData.set("brightnessDevicePins", pins); CacheData.set("brightnessDevicePins", pins);
} }
implicitHeight: { implicitHeight: {
@@ -269,7 +269,7 @@ Rectangle {
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName) readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
readonly property bool devicePinnedHere: { readonly property bool devicePinnedHere: {
SettingsData.brightnessDevicePins; CacheData.brightnessDevicePins;
return root.isDevicePinnedToScreen(modelData ? modelData.name : ""); return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
} }
@@ -49,7 +49,7 @@ Rectangle {
} }
function getPinnedNetworks() { function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {}; const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]); return normalizePinList(pins["preferredWifi"]);
} }
@@ -710,7 +710,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = root.normalizePinList(pins["preferredWifi"]); let pinnedList = root.normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(modelData.ssid); const pinIndex = pinnedList.indexOf(modelData.ssid);
@@ -727,7 +727,7 @@ Rectangle {
else else
delete pins["preferredWifi"]; delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins); CacheData.set("wifiNetworkPins", pins);
} }
} }
} }
@@ -34,7 +34,7 @@ Row {
if (screenName && screenName.length > 0) { if (screenName && screenName.length > 0) {
const screen = Quickshell.screens.find(s => s.name === screenName); const screen = Quickshell.screens.find(s => s.name === screenName);
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName; const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice); const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
@@ -29,6 +29,8 @@ Item {
property var centerWidgets: [] property var centerWidgets: []
property int totalWidgets: 0 property int totalWidgets: 0
property real totalSize: 0 property real totalSize: 0
property real contentStart: 0
property real contentSize: 0
function updateLayout() { function updateLayout() {
if (SettingsData.centeringMode === "geometric") { if (SettingsData.centeringMode === "geometric") {
@@ -36,6 +38,25 @@ Item {
} else { } else {
applyIndexLayout(); applyIndexLayout();
} }
updateContentExtent();
}
function updateContentExtent() {
if (centerWidgets.length === 0) {
contentStart = 0;
contentSize = 0;
return;
}
let start = Infinity;
let end = -Infinity;
for (const widget of centerWidgets) {
const pos = isVertical ? widget.y : widget.x;
const size = isVertical ? widget.height : widget.width;
start = Math.min(start, pos);
end = Math.max(end, pos + size);
}
contentStart = start;
contentSize = end - start;
} }
function applyGeometricLayout() { function applyGeometricLayout() {
+18 -4
View File
@@ -833,16 +833,30 @@ PanelWindow {
const pos = section.mapToItem(barWindow.contentItem, 0, 0); const pos = section.mapToItem(barWindow.contentItem, 0, 0);
const implW = section.implicitWidth || 0; const implW = section.implicitWidth || 0;
const implH = section.implicitHeight || 0; const implH = section.implicitHeight || 0;
const contentSize = isCenter ? (section.contentSize || 0) : 0;
const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0; let offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0); let offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
let w = implW;
let h = implH;
// index centering lays content out asymmetrically; use the real extent
if (contentSize > 0) {
if (barWindow.isVertical) {
offsetY = section.contentStart;
h = contentSize;
} else {
offsetX = section.contentStart;
w = contentSize;
}
}
const edgePad = 2; const edgePad = 2;
return { return {
"x": pos.x + offsetX - edgePad, "x": pos.x + offsetX - edgePad,
"y": pos.y + offsetY - edgePad, "y": pos.y + offsetY - edgePad,
"w": implW + edgePad * 2, "w": w + edgePad * 2,
"h": implH + edgePad * 2 "h": h + edgePad * 2
}; };
} }
@@ -203,7 +203,7 @@ BasePill {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey) if (!pinKey)
return ""; return "";
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] || ""; return pins[pinKey] || "";
} }
@@ -35,44 +35,42 @@ BasePill {
} }
} }
onRightClicked: (rx, ry) => { onRightClicked: {
const screen = root.parentScreen || Screen; const screen = root.parentScreen || Screen;
if (!screen) if (!screen)
return; return;
const globalPos = root.visualContent.mapToItem(null, 0, 0);
const isVertical = root.axis?.isVertical ?? false; const isVertical = root.axis?.isVertical ?? false;
const edge = root.axis?.edge ?? "top"; const edge = root.axis?.edge ?? "top";
const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS); const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
const barOffset = root.barThickness + root.barSpacing + gap; const barOffset = root.barThickness + root.barSpacing + gap;
const localPos = root.visualContent.mapToItem(null, root.visualContent.width / 2, root.visualContent.height / 2);
let anchorX; let anchorX;
let anchorY; let anchorY;
let anchorEdge;
if (isVertical) { if (isVertical) {
anchorY = globalPos.y - (screen.y || 0) + root.visualContent.height / 2; anchorX = edge === "left" ? barOffset : screen.width - barOffset;
if (edge === "left") { anchorY = localPos.y;
anchorX = barOffset;
anchorEdge = "top";
} else {
anchorX = screen.width - barOffset;
anchorEdge = "top";
}
} else { } else {
anchorX = globalPos.x - (screen.x || 0) + root.visualContent.width / 2; anchorX = localPos.x;
if (edge === "bottom") { anchorY = edge === "bottom" ? screen.height - barOffset : barOffset;
anchorY = screen.height - barOffset;
anchorEdge = "bottom";
} else {
anchorY = barOffset;
anchorEdge = "top";
}
} }
dndPopupLoader.active = true; dndPopupLoader.active = true;
const popup = dndPopupLoader.item; const popup = dndPopupLoader.item;
if (!popup) if (!popup)
return; return;
popup.showAt(anchorX, anchorY, screen, anchorEdge);
popup.showAt(anchorX, anchorY, isVertical, edge, screen);
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton
onPressed: mouse => {
root.triggerRipple(this, mouse.x, mouse.y);
SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
}
} }
Loader { Loader {
+45 -10
View File
@@ -438,20 +438,55 @@ Variants {
parent: dock.contentItem parent: dock.contentItem
visible: false visible: false
readonly property bool expanded: dock.reveal readonly property bool expanded: dock.reveal
readonly property bool chrome: dock.usesConnectedFrameChrome
readonly property bool atEndEdge: SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right
readonly property real innerReach: borderThickness + (SettingsData.appsDockEnlargeOnHover ? animationHeadroom : 0)
readonly property real bodyX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
readonly property real bodyY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
x: { x: {
const baseX = dockCore.x + dockMouseArea.x; if (chrome) {
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) const baseX = dockCore.x + dockMouseArea.x;
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0); if (isVertical && atEndEdge)
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0); return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
}
if (!isVertical)
return dockCore.x + dockMouseArea.x - (expanded ? borderThickness : 0);
if (!atEndEdge)
return 0;
return expanded ? Math.min(bodyX - innerReach, dock.width - 1) : dock.width - 1;
} }
y: { y: {
const baseY = dockCore.y + dockMouseArea.y; if (chrome) {
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) const baseY = dockCore.y + dockMouseArea.y;
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0); if (!isVertical && atEndEdge)
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0); return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
}
if (isVertical)
return dockCore.y + dockMouseArea.y - (expanded ? borderThickness : 0);
if (!atEndEdge)
return 0;
return expanded ? Math.min(bodyY - innerReach, dock.height - 1) : dock.height - 1;
}
width: {
if (chrome)
return dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0);
if (!isVertical)
return dockMouseArea.width + (expanded ? borderThickness * 2 : 0);
if (!expanded)
return 1;
return atEndEdge ? dock.width - x : Math.max(bodyX + dockBackground.width + innerReach, 1);
}
height: {
if (chrome)
return dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0);
if (isVertical)
return dockMouseArea.height + (expanded ? borderThickness * 2 : 0);
if (!expanded)
return 1;
return atEndEdge ? dock.height - y : Math.max(bodyY + dockBackground.height + innerReach, 1);
} }
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
} }
mask: Region { mask: Region {
+4 -2
View File
@@ -479,6 +479,8 @@ Item {
delegate: Item { delegate: Item {
id: delegateItem id: delegateItem
required property var modelData
required property int index
property var dockButton: { property var dockButton: {
switch (itemData.type) { switch (itemData.type) {
@@ -599,7 +601,7 @@ Item {
height: delegateItem.height height: delegateItem.height
actualIconSize: root.iconSize actualIconSize: root.iconSize
dockApps: root dockApps: root
index: model.index index: delegateItem.index
} }
DockTrashButton { DockTrashButton {
@@ -624,7 +626,7 @@ Item {
appData: itemData appData: itemData
contextMenu: root.contextMenu contextMenu: root.contextMenu
dockApps: root dockApps: root
index: model.index index: delegateItem.index
parentDockScreen: root.dockScreen parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: { windowTitle: {
@@ -1773,6 +1773,7 @@ Item {
const idx = GreeterState.sessionList.indexOf(value); const idx = GreeterState.sessionList.indexOf(value);
if (idx < 0) if (idx < 0)
return; return;
GreeterState.sessionManuallySelected = true;
GreeterState.currentSessionIndex = idx; GreeterState.currentSessionIndex = idx;
GreeterState.selectedSession = GreeterState.sessionExecs[idx]; GreeterState.selectedSession = GreeterState.sessionExecs[idx];
GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx]; GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx];
@@ -1785,6 +1786,8 @@ Item {
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || "" property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
function finalizeSessionSelection() { function finalizeSessionSelection() {
if (GreeterState.sessionManuallySelected)
return;
if (GreeterState.sessionList.length === 0) if (GreeterState.sessionList.length === 0)
return; return;
if (!GreetdMemory.memoryReady) if (!GreetdMemory.memoryReady)
@@ -21,6 +21,7 @@ Singleton {
property var sessionPaths: [] property var sessionPaths: []
property var sessionDesktopIds: [] property var sessionDesktopIds: []
property int currentSessionIndex: 0 property int currentSessionIndex: 0
property bool sessionManuallySelected: false
property var availableUsers: [] property var availableUsers: []
property int selectedUserIndex: -1 property int selectedUserIndex: -1
+5 -1
View File
@@ -10,6 +10,7 @@ PanelWindow {
id: root id: root
property bool active: false property bool active: false
property bool _completed: false
signal fadeCompleted signal fadeCompleted
signal fadeCancelled signal fadeCancelled
@@ -36,7 +37,8 @@ PanelWindow {
opacity: 0 opacity: 0
onOpacityChanged: { onOpacityChanged: {
if (opacity >= 0.99 && root.active) { if (opacity >= 0.99 && root.active && !root._completed) {
root._completed = true;
root.fadeCompleted(); root.fadeCompleted();
} }
} }
@@ -59,6 +61,7 @@ PanelWindow {
function startFade() { function startFade() {
if (!SettingsData.fadeToDpmsEnabled) if (!SettingsData.fadeToDpmsEnabled)
return; return;
_completed = false;
active = true; active = true;
fadeOverlay.opacity = 0.0; fadeOverlay.opacity = 0.0;
fadeSeq.stop(); fadeSeq.stop();
@@ -74,6 +77,7 @@ PanelWindow {
fadeSeq.stop(); fadeSeq.stop();
fadeOverlay.opacity = 0.0; fadeOverlay.opacity = 0.0;
active = false; active = false;
_completed = false;
} }
Connections { Connections {
@@ -27,11 +27,14 @@ Item {
function hide() { function hide() {
if (isKeyboardActive && keyboard !== null) { if (isKeyboardActive && keyboard !== null) {
keyboard.destroy(); keyboard.destroy();
keyboard = null;
isKeyboardActive = false; isKeyboardActive = false;
} else } else
log.debug("The keyboard is already hidden"); log.debug("The keyboard is already hidden");
} }
Component.onDestruction: hide()
// private // private
property Item keyboard: null property Item keyboard: null
Component { Component {
+19 -1
View File
@@ -35,6 +35,7 @@ Item {
property int hyprlandLayoutCount: 0 property int hyprlandLayoutCount: 0
property bool lockerReadySent: false property bool lockerReadySent: false
property bool lockerReadyArmed: false property bool lockerReadyArmed: false
property var sessionLock: null
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== "" readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
readonly property string lockFontFamily: SettingsData.lockScreenFontFamily readonly property string lockFontFamily: SettingsData.lockScreenFontFamily
@@ -111,6 +112,8 @@ Item {
} }
function sendLockerReadyOnce() { function sendLockerReadyOnce() {
if (root.demoMode)
return;
if (lockerReadySent) if (lockerReadySent)
return; return;
if (root.unlocking) if (root.unlocking)
@@ -133,12 +136,27 @@ Item {
return; return;
if (!root.visible || root.opacity <= 0) if (!root.visible || root.opacity <= 0)
return; return;
// Don't report ready until the compositor has confirmed the session is
// locked (ext-session-lock `locked` event). Qt's afterRendering fires
// before the lock surface is committed/presented, so releasing the sleep
// inhibitor on it lets the machine freeze with the desktop still on screen,
// which then flashes on resume. secure=true guarantees the desktop is hidden.
if (root.sessionLock && !root.sessionLock.secure)
return;
Qt.callLater(() => { Qt.callLater(() => {
if (root.visible && root.opacity > 0 && !root.unlocking) if (root.visible && root.opacity > 0 && !root.unlocking)
sendLockerReadyOnce(); sendLockerReadyOnce();
}); });
} }
Connections {
target: root.sessionLock
enabled: target !== null
function onSecureChanged() {
root.maybeSend();
}
}
Connections { Connections {
target: root.Window.window target: root.Window.window
enabled: target !== null enabled: target !== null
@@ -778,7 +796,7 @@ Item {
return "passkey"; return "passkey";
if (pam.fprint.tries >= SettingsData.maxFprintTries) if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off"; return "fingerprint_off";
if (pam.fprint.active) if (pam.fprint.active || pam.fprint.retrying)
return "fingerprint"; return "fingerprint";
if (pam.u2f.active) if (pam.u2f.active)
return "passkey"; return "passkey";
+1
View File
@@ -33,6 +33,7 @@ FocusScope {
anchors.fill: parent anchors.fill: parent
demoMode: false demoMode: false
sessionLock: root.lock
pam: root.pam pam: root.pam
passwordBuffer: root.sharedPasswordBuffer passwordBuffer: root.sharedPasswordBuffer
screenName: root.screenName screenName: root.screenName
+8 -1
View File
@@ -262,9 +262,11 @@ Scope {
property bool available: SettingsData.lockFingerprintReady property bool available: SettingsData.lockFingerprintReady
property int tries property int tries
property int errorTries property int errorTries
property bool retrying: false
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) { if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
retrying = false;
abort(); abort();
return; return;
} }
@@ -273,6 +275,7 @@ Scope {
tries = 0; tries = 0;
errorTries = 0; errorTries = 0;
retrying = false;
start(); start();
} }
@@ -285,6 +288,7 @@ Scope {
switch (res) { switch (res) {
case PamResult.Success: case PamResult.Success:
retrying = false;
if (!root.unlockInProgress) { if (!root.unlockInProgress) {
passwd.abort(); passwd.abort();
root.proceedAfterPrimaryAuth(); root.proceedAfterPrimaryAuth();
@@ -293,13 +297,16 @@ Scope {
case PamResult.Error: case PamResult.Error:
errorTries++; errorTries++;
if (errorTries < 200) { if (errorTries < 200) {
retrying = true;
abort(); abort();
errorRetry.restart(); errorRetry.restart();
return; return;
} }
retrying = false;
abort(); abort();
return; return;
case PamResult.MaxTries: case PamResult.MaxTries:
retrying = false;
tries++; tries++;
if (tries < SettingsData.maxFprintTries) { if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail"; root.fprintState = "fail";
@@ -418,7 +425,7 @@ Scope {
Timer { Timer {
id: errorRetry id: errorRetry
interval: 1500 interval: Math.min(1500 * Math.pow(2, Math.max(0, fprint.errorTries - 1)), 30000)
onTriggered: fprint.start() onTriggered: fprint.start()
} }
@@ -31,14 +31,16 @@ PanelWindow {
} }
property point anchorPos: Qt.point(0, 0) property point anchorPos: Qt.point(0, 0)
property string anchorEdge: "top" property bool isVertical: false
property string edge: "top"
visible: false visible: false
function showAt(x, y, targetScreen, edge) { function showAt(x, y, vertical, barEdge, targetScreen) {
if (targetScreen) if (targetScreen)
root.screen = targetScreen; root.screen = targetScreen;
anchorPos = Qt.point(x, y); anchorPos = Qt.point(x, y);
anchorEdge = edge || "top"; isVertical = vertical ?? false;
edge = barEdge ?? "top";
visible = true; visible = true;
} }
@@ -66,21 +68,19 @@ PanelWindow {
visible: root.visible visible: root.visible
x: { x: {
const left = 10; if (root.isVertical) {
const right = root.width - width - 10; if (root.edge === "left")
const want = root.anchorPos.x - width / 2; return Math.min(root.width - width - 10, root.anchorPos.x);
return Math.max(left, Math.min(right, want)); return Math.max(10, root.anchorPos.x - width);
}
return Math.max(10, Math.min(root.width - width - 10, root.anchorPos.x - width / 2));
} }
y: { y: {
switch (root.anchorEdge) { if (root.isVertical)
case "bottom":
return Math.max(10, root.anchorPos.y - height);
case "left":
case "right":
return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2)); return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2));
default: if (root.edge === "bottom")
return Math.min(root.height - height - 10, root.anchorPos.y); return Math.max(10, root.anchorPos.y - height);
} return Math.min(root.height - height - 10, root.anchorPos.y);
} }
onDismissed: root.closeMenu() onDismissed: root.closeMenu()
@@ -45,6 +45,14 @@ PanelWindow {
blurWidth: !win._finalized && !win.connectedFrameMode ? innerW * s : 0 blurWidth: !win._finalized && !win.connectedFrameMode ? innerW * s : 0
blurHeight: !win._finalized && !win.connectedFrameMode ? innerH * s : 0 blurHeight: !win._finalized && !win.connectedFrameMode ? innerH * s : 0
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
// Slide-out translates the card past the surface edge; intersecting with the
// resting bounds keeps the committed region on the visible content instead of
// leaving a stray blur sliver at the edge (#2917).
clipEnabled: true
clipX: content.x + content.cardInset
clipY: content.y + content.cardInset
clipWidth: innerW
clipHeight: innerH
} }
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
@@ -375,6 +375,8 @@ Item {
visible: DesktopService.autostartAvailable visible: DesktopService.autostartAvailable
SettingsCard { SettingsCard {
settingKey: "autostartAddEntry"
tags: ["autostart", "add", "entry", "command", "startup"]
width: parent.width width: parent.width
iconName: "add_circle" iconName: "add_circle"
title: I18n.tr("Add Entry") title: I18n.tr("Add Entry")
@@ -764,6 +766,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "autostartTrayIconFix"
tags: ["tray", "icons", "fix", "systemd"]
width: parent.width width: parent.width
iconName: "handyman" iconName: "handyman"
title: I18n.tr("Tray Icon Fix") title: I18n.tr("Tray Icon Fix")
@@ -463,6 +463,7 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "barEnable"
iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off" iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off"
title: I18n.tr("Enable Bar") title: I18n.tr("Enable Bar")
visible: !dankBarTab.appearanceOnly && selectedBarId !== "default" visible: !dankBarTab.appearanceOnly && selectedBarId !== "default"
@@ -651,6 +652,8 @@ Item {
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsToggleRow { SettingsToggleRow {
settingKey: "barAutoHide"
tags: ["autohide", "auto-hide", "reveal", "intellihide"]
text: I18n.tr("Auto-hide") text: I18n.tr("Auto-hide")
description: I18n.tr("Automatically hide the bar when the pointer moves away") description: I18n.tr("Automatically hide the bar when the pointer moves away")
checked: selectedBarConfig?.autoHide ?? false checked: selectedBarConfig?.autoHide ?? false
@@ -700,6 +703,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barAutoHideStrict"
tags: ["autohide", "strict", "popout"]
width: parent.width - parent.leftPadding width: parent.width - parent.leftPadding
text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open") text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open")
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open") description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
@@ -713,6 +718,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barHideWhenWindowsOpen"
tags: ["hide", "windows", "empty", "workspace"]
width: parent.width - parent.leftPadding width: parent.width - parent.leftPadding
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
text: I18n.tr("Hide When Windows Open") text: I18n.tr("Hide When Windows Open")
@@ -734,6 +741,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barManualVisibility"
tags: ["manual", "show", "hide", "ipc", "toggle"]
text: I18n.tr("Manual Show/Hide") text: I18n.tr("Manual Show/Hide")
description: I18n.tr("Toggle bar visibility manually via IPC") description: I18n.tr("Toggle bar visibility manually via IPC")
checked: selectedBarConfig?.visible ?? true checked: selectedBarConfig?.visible ?? true
@@ -753,6 +762,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barClickThrough"
tags: ["clickthrough", "click", "through", "mouse", "input", "mask", "passthrough"]
text: I18n.tr("Click Through") text: I18n.tr("Click Through")
description: I18n.tr("Mouse clicks pass through the bar to windows behind it") description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
checked: selectedBarConfig?.clickThrough ?? false checked: selectedBarConfig?.clickThrough ?? false
@@ -910,6 +921,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: exclusiveZoneSlider id: exclusiveZoneSlider
settingKey: "barExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge") description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
@@ -933,6 +946,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: sizeSlider id: sizeSlider
settingKey: "barSize"
tags: ["size", "thickness", "height", "inner"]
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
text: I18n.tr("Size") text: I18n.tr("Size")
description: I18n.tr("Adjust the bar height via inner padding") description: I18n.tr("Adjust the bar height via inner padding")
@@ -1154,6 +1169,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barSquareCorners"
tags: ["square", "corners", "rounding"]
text: I18n.tr("Square Corners") text: I18n.tr("Square Corners")
description: I18n.tr("Remove corner rounding from the bar") description: I18n.tr("Remove corner rounding from the bar")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
@@ -1164,6 +1181,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barNoBackground"
tags: ["transparent", "background", "invisible"]
text: I18n.tr("No Background") text: I18n.tr("No Background")
description: I18n.tr("Make the bar background fully transparent") description: I18n.tr("Make the bar background fully transparent")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
@@ -1174,6 +1193,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barMaximizeWidgetIcons"
tags: ["maximize", "icons", "stretch"]
text: I18n.tr("Maximize Widget Icons") text: I18n.tr("Maximize Widget Icons")
description: I18n.tr("Stretch widget icons to fill the available bar height") description: I18n.tr("Stretch widget icons to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetIcons ?? false checked: selectedBarConfig?.maximizeWidgetIcons ?? false
@@ -1183,6 +1204,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barMaximizeWidgetText"
tags: ["maximize", "text", "stretch"]
text: I18n.tr("Maximize Widget Text") text: I18n.tr("Maximize Widget Text")
description: I18n.tr("Stretch widget text to fill the available bar height") description: I18n.tr("Stretch widget text to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetText ?? false checked: selectedBarConfig?.maximizeWidgetText ?? false
@@ -1192,6 +1215,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barRemoveWidgetPadding"
tags: ["padding", "compact", "widgets"]
text: I18n.tr("Remove Widget Padding") text: I18n.tr("Remove Widget Padding")
description: I18n.tr("Remove inner padding from all widgets") description: I18n.tr("Remove inner padding from all widgets")
checked: selectedBarConfig?.removeWidgetPadding ?? false checked: selectedBarConfig?.removeWidgetPadding ?? false
@@ -1208,6 +1233,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "barGothCorners"
tags: ["goth", "corners", "concave", "cutout"]
text: I18n.tr("Goth Corners") text: I18n.tr("Goth Corners")
description: I18n.tr("Apply inverse concave corner cutouts to the bar") description: I18n.tr("Apply inverse concave corner cutouts to the bar")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
@@ -1302,6 +1329,8 @@ Item {
} }
SettingsToggleCard { SettingsToggleCard {
settingKey: "barMaximizeDetection"
tags: ["maximize", "gaps", "border", "fullscreen"]
iconName: "fit_screen" iconName: "fit_screen"
title: I18n.tr("Maximize Detection") title: I18n.tr("Maximize Detection")
description: I18n.tr("Remove gaps and border when windows are maximized") description: I18n.tr("Remove gaps and border when windows are maximized")
@@ -1847,6 +1876,8 @@ Item {
SettingsToggleCard { SettingsToggleCard {
iconName: "mouse" iconName: "mouse"
settingKey: "barScrollWheel"
tags: ["scroll", "wheel", "workspace", "axis"]
title: I18n.tr("Scroll Wheel") title: I18n.tr("Scroll Wheel")
description: I18n.tr("Control workspaces and columns by scrolling on the bar") description: I18n.tr("Control workspaces and columns by scrolling on the bar")
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
@@ -267,6 +267,8 @@ Item {
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard { SettingsCard {
settingKey: "defaultAppsInternet"
tags: ["browser", "mail", "email", "web"]
title: I18n.tr("Internet", "Internet") title: I18n.tr("Internet", "Internet")
iconName: "public" iconName: "public"
@@ -293,6 +295,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "defaultAppsUtilities"
tags: ["file", "manager", "terminal", "editor"]
title: I18n.tr("Utilities", "Utilities") title: I18n.tr("Utilities", "Utilities")
iconName: "terminal" iconName: "terminal"
@@ -317,6 +321,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "defaultAppsDocuments"
tags: ["pdf", "text", "reader", "office"]
title: I18n.tr("Documents", "Documents") title: I18n.tr("Documents", "Documents")
iconName: "edit_document" iconName: "edit_document"
@@ -335,6 +341,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "defaultAppsMultimedia"
tags: ["image", "video", "music", "viewer", "player"]
title: I18n.tr("Multimedia", "Multimedia") title: I18n.tr("Multimedia", "Multimedia")
iconName: "movie" iconName: "movie"
AppSelector { AppSelector {
@@ -166,6 +166,8 @@ Item {
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard { SettingsCard {
settingKey: "desktopWidgetsManage"
tags: ["desktop", "widgets", "clock", "conky"]
width: parent.width width: parent.width
iconName: "widgets" iconName: "widgets"
title: I18n.tr("Desktop Widgets") title: I18n.tr("Desktop Widgets")
@@ -203,6 +205,8 @@ Item {
} }
SettingsCard { SettingsCard {
settingKey: "desktopWidgetGroups"
tags: ["groups", "profiles", "layouts"]
width: parent.width width: parent.width
iconName: "folder" iconName: "folder"
title: I18n.tr("Groups") title: I18n.tr("Groups")
@@ -17,6 +17,7 @@ Singleton {
readonly property var wlrOutputs: WlrOutputService.outputs readonly property var wlrOutputs: WlrOutputService.outputs
property var outputs: ({}) property var outputs: ({})
property var savedOutputs: ({}) property var savedOutputs: ({})
property var savedParsedOutputs: ({})
property var allOutputs: buildAllOutputsMap() property var allOutputs: buildAllOutputsMap()
property var includeStatus: ({ property var includeStatus: ({
@@ -911,15 +912,18 @@ Singleton {
const paths = getConfigPaths(); const paths = getConfigPaths();
if (!paths) { if (!paths) {
savedOutputs = {}; savedOutputs = {};
savedParsedOutputs = {};
return; return;
} }
Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => { Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => {
if (exitCode !== 0 || !content.trim()) { if (exitCode !== 0 || !content.trim()) {
savedOutputs = {}; savedOutputs = {};
savedParsedOutputs = {};
return; return;
} }
const parsed = parseOutputsConfig(content); const parsed = parseOutputsConfig(content);
savedParsedOutputs = parsed;
const filtered = filterDisconnectedOnly(parsed); const filtered = filterDisconnectedOnly(parsed);
savedOutputs = filtered; savedOutputs = filtered;
@@ -1092,20 +1096,7 @@ Singleton {
const name = match[1]; const name = match[1];
const body = match[2]; const body = match[2];
if (body.trim() === "off") { const disabled = /^\s*off\s*$/m.test(body);
result[name] = {
"name": name,
"disabled": true,
"logical": {
"x": 0,
"y": 0,
"scale": 1.0,
"transform": "Normal"
}
};
continue;
}
const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/); const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/); const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
const scaleMatch = body.match(/scale\s+([\d.]+)/); const scaleMatch = body.match(/scale\s+([\d.]+)/);
@@ -1115,6 +1106,7 @@ Singleton {
result[name] = { result[name] = {
"name": name, "name": name,
"disabled": disabled,
"logical": { "logical": {
"x": posMatch ? parseInt(posMatch[1]) : 0, "x": posMatch ? parseInt(posMatch[1]) : 0,
"y": posMatch ? parseInt(posMatch[2]) : 0, "y": posMatch ? parseInt(posMatch[2]) : 0,
@@ -1778,6 +1770,41 @@ Singleton {
return normalized; return normalized;
} }
function findSavedParsedOutput(outputName) {
const output = outputs[outputName];
const candidates = [outputName];
if (output?.make && output?.model) {
candidates.push(output.make + " " + output.model + " " + (output.serial || "Unknown"));
candidates.push(output.make + " " + output.model);
}
for (const savedName in savedParsedOutputs) {
if (candidates.includes(savedName.trim()))
return savedParsedOutputs[savedName];
}
return null;
}
// A disabled wlr head reports no current mode, scale 0 and position 0,0
// overlay the last written config so rewrites don't discard real settings.
function overlaySavedDisabledOutput(entry, outputName) {
const saved = findSavedParsedOutput(outputName);
if (!saved)
return;
const savedMode = saved.modes?.[saved.current_mode ?? 0];
if (savedMode && !entry.configured_mode)
entry.configured_mode = savedMode.width + "x" + savedMode.height + "@" + (savedMode.refresh_rate / 1000).toFixed(3);
if (saved.vrr_enabled !== undefined)
entry.vrr_enabled = saved.vrr_enabled;
if (!saved.logical || !entry.logical)
return;
entry.logical.x = saved.logical.x;
entry.logical.y = saved.logical.y;
entry.logical.scale = saved.logical.scale;
entry.logical.transform = saved.logical.transform;
}
function buildOutputsWithPendingChanges() { function buildOutputsWithPendingChanges() {
const result = {}; const result = {};
@@ -1787,7 +1814,10 @@ Singleton {
} }
for (const outputName in outputs) { for (const outputName in outputs) {
result[outputName] = JSON.parse(JSON.stringify(outputs[outputName])); const entry = JSON.parse(JSON.stringify(outputs[outputName]));
if (entry.enabled === false)
overlaySavedDisabledOutput(entry, outputName);
result[outputName] = entry;
} }
for (const outputName in pendingChanges) { for (const outputName in pendingChanges) {
@@ -1799,6 +1829,8 @@ Singleton {
result[outputName].logical.y = changes.position.y; result[outputName].logical.y = changes.position.y;
} }
if (changes.mode !== undefined && result[outputName].modes) { if (changes.mode !== undefined && result[outputName].modes) {
if (result[outputName].configured_mode)
result[outputName].configured_mode = changes.mode;
for (var i = 0; i < result[outputName].modes.length; i++) { for (var i = 0; i < result[outputName].modes.length; i++) {
if (formatMode(result[outputName].modes[i]) === changes.mode) { if (formatMode(result[outputName].modes[i]) === changes.mode) {
result[outputName].current_mode = i; result[outputName].current_mode = i;
+2
View File
@@ -618,6 +618,8 @@ Item {
} }
SettingsSliderRow { SettingsSliderRow {
settingKey: "dockExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
visible: !root.connectedFrameModeActive || root.connectedPersistentDockActive visible: !root.connectedFrameModeActive || root.connectedPersistentDockActive
value: SettingsData.dockBottomGap value: SettingsData.dockBottomGap
@@ -38,6 +38,8 @@ Item {
settingKey: "mediaPlayer" settingKey: "mediaPlayer"
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaWaveProgress"
tags: ["wave", "progress", "animated"]
text: I18n.tr("Wave Progress Bars") text: I18n.tr("Wave Progress Bars")
description: I18n.tr("Use animated wave progress bars for media playback") description: I18n.tr("Use animated wave progress bars for media playback")
checked: SettingsData.waveProgressEnabled checked: SettingsData.waveProgressEnabled
@@ -45,6 +47,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaScrollTitle"
tags: ["scroll", "title", "marquee"]
text: I18n.tr("Scroll song title") text: I18n.tr("Scroll song title")
description: I18n.tr("Scroll title if it doesn't fit in widget") description: I18n.tr("Scroll title if it doesn't fit in widget")
checked: SettingsData.scrollTitleEnabled checked: SettingsData.scrollTitleEnabled
@@ -52,6 +56,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaVisualizer"
tags: ["visualizer", "cava", "spectrum"]
text: I18n.tr("Audio Visualizer") text: I18n.tr("Audio Visualizer")
description: I18n.tr("Show cava audio visualizer in media widget") description: I18n.tr("Show cava audio visualizer in media widget")
checked: SettingsData.audioVisualizerEnabled checked: SettingsData.audioVisualizerEnabled
@@ -59,6 +65,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaAdaptiveWidth"
tags: ["adaptive", "width", "shrink"]
text: I18n.tr("Adaptive Media Width") text: I18n.tr("Adaptive Media Width")
description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size") description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size")
checked: SettingsData.mediaAdaptiveWidthEnabled checked: SettingsData.mediaAdaptiveWidthEnabled
@@ -66,6 +74,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaAlbumArtAccent"
tags: ["album", "art", "accent", "colors"]
text: I18n.tr("Use album art accent") text: I18n.tr("Use album art accent")
description: I18n.tr("Use colors extracted from album art instead of system theme colors") description: I18n.tr("Use colors extracted from album art instead of system theme colors")
checked: SettingsData.mediaUseAlbumArtAccent checked: SettingsData.mediaUseAlbumArtAccent
@@ -136,6 +146,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "mediaDeviceScrollVolume"
tags: ["device", "scroll", "volume"]
text: I18n.tr("Device list scroll volume") text: I18n.tr("Device list scroll volume")
description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list") description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list")
checked: SettingsData.audioDeviceScrollVolumeEnabled checked: SettingsData.audioDeviceScrollVolumeEnabled
+1
View File
@@ -81,6 +81,7 @@ Item {
SettingsCard { SettingsCard {
tab: "mux" tab: "mux"
tags: ["mux", "session", "filter", "exclude", "hide"] tags: ["mux", "session", "filter", "exclude", "hide"]
settingKey: "muxSessionFilter"
title: I18n.tr("Session Filter") title: I18n.tr("Session Filter")
iconName: "filter_list" iconName: "filter_list"
@@ -55,12 +55,12 @@ Item {
} }
function getPinnedWifiNetworks() { function getPinnedWifiNetworks() {
const pins = SettingsData.wifiNetworkPins || {}; const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]); return normalizePinList(pins["preferredWifi"]);
} }
function toggleWifiPin(ssid) { function toggleWifiPin(ssid) {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})); const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = normalizePinList(pins["preferredWifi"]); let pinnedList = normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(ssid); const pinIndex = pinnedList.indexOf(ssid);
@@ -77,7 +77,7 @@ Item {
else else
delete pins["preferredWifi"]; delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins); CacheData.set("wifiNetworkPins", pins);
} }
property var forgetNetworkConfirm: ConfirmModal {} property var forgetNetworkConfirm: ConfirmModal {}
@@ -89,6 +89,8 @@ Item {
} }
SettingsDropdownRow { SettingsDropdownRow {
settingKey: "systemUpdaterCheckInterval"
tags: ["interval", "poll", "frequency"]
text: I18n.tr("Check interval") text: I18n.tr("Check interval")
description: I18n.tr("How often the server polls for new updates.") description: I18n.tr("How often the server polls for new updates.")
options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel]) options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel])
@@ -157,6 +159,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterCheckOnStart"
tags: ["startup", "check", "boot"]
text: I18n.tr("Check on startup") text: I18n.tr("Check on startup")
description: I18n.tr("When enabled, checks updates on startup. When disabled, only the interval above or a manual refresh runs a check.") description: I18n.tr("When enabled, checks updates on startup. When disabled, only the interval above or a manual refresh runs a check.")
checked: SettingsData.updaterCheckOnStart checked: SettingsData.updaterCheckOnStart
@@ -164,6 +168,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterFlatpak"
tags: ["flatpak", "include"]
text: I18n.tr("Include Flatpak updates") text: I18n.tr("Include Flatpak updates")
description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.") description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak") visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
@@ -172,6 +178,8 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterAUR"
tags: ["aur", "paru", "yay"]
text: I18n.tr("Include AUR updates") text: I18n.tr("Include AUR updates")
description: I18n.tr("Run paru/yay with AUR enabled when 'Update All' is clicked.") description: I18n.tr("Run paru/yay with AUR enabled when 'Update All' is clicked.")
visible: (SystemUpdateService.backends || []).some(b => b.id === "paru" || b.id === "yay") visible: (SystemUpdateService.backends || []).some(b => b.id === "paru" || b.id === "yay")
@@ -328,6 +336,8 @@ Item {
settingKey: "systemUpdaterAdvanced" settingKey: "systemUpdaterAdvanced"
SettingsToggleRow { SettingsToggleRow {
settingKey: "systemUpdaterCustomCommand"
tags: ["custom", "command", "terminal"]
text: I18n.tr("Use Custom Command") text: I18n.tr("Use Custom Command")
description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.") description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.")
checked: SettingsData.updaterUseCustomCommand checked: SettingsData.updaterUseCustomCommand
@@ -167,7 +167,13 @@ FloatingWindow {
width: parent.width width: parent.width
height: parent.height - searchField.height - Theme.spacingM height: parent.height - searchField.height - Theme.spacingM
spacing: Theme.spacingS spacing: Theme.spacingS
model: root.filteredApps // Start with no model. It is (re)bound in show() so that each open
// gets a fresh QQmlDelegateModel, avoiding a crash where a stale
// incubation queue from a previous open races with a background
// refreshApplications() replacing the underlying DesktopEntries
// QObjectModel (SIGSEGV in QQmlIncubatorPrivate::incubate via
// libQt6QmlModels). See hide()/onVisibleChanged() for the teardown.
model: null
clip: true clip: true
delegate: Rectangle { delegate: Rectangle {
@@ -316,11 +322,21 @@ FloatingWindow {
function show() { function show() {
updateFilteredApps(); updateFilteredApps();
// Rebind the model reactively (search relies on filteredApps changes flowing
// through), and do it after populating so the ListView starts incubating from
// a fully-formed array rather than [].
appList.model = Qt.binding(() => root.filteredApps);
visible = true; visible = true;
Qt.callLater(() => searchField.forceActiveFocus()); Qt.callLater(() => searchField.forceActiveFocus());
} }
function hide() { function hide() {
// Drop the model before clearing visible, so the ListView releases its
// QQmlDelegateModel (and any in-flight incubators) synchronously on this
// frame. This is the crux of the fix: a later show() will allocate a fresh
// DelegateModel instead of reusing one whose incubation queue may hold
// references invalidated by a concurrent refreshApplications().
appList.model = null;
visible = false; visible = false;
searchQuery = ""; searchQuery = "";
filteredApps = []; filteredApps = [];
@@ -330,6 +346,9 @@ FloatingWindow {
onVisibleChanged: { onVisibleChanged: {
if (!visible) { if (!visible) {
// Guard against visibility being cleared without going through hide()
// (e.g. window manager close, FloatingWindow.onClosed -> hide()).
appList.model = null;
searchQuery = ""; searchQuery = "";
filteredApps = []; filteredApps = [];
selectedIndex = -1; selectedIndex = -1;
+17 -9
View File
@@ -17,7 +17,22 @@ Singleton {
interval: 500 interval: 500
repeat: false repeat: false
running: true running: true
onTriggered: root.suppressSound = false onTriggered: {
root.suppressSound = false;
root.applyPowerProfile();
}
}
function applyPowerProfile() {
if (!batteryAvailable)
return;
const profileValue = isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName;
if (profileValue === "")
return;
const targetProfile = parseInt(profileValue);
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
return;
PowerProfiles.profile = targetProfile;
} }
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
@@ -206,14 +221,7 @@ Singleton {
} }
} }
const profileValue = BatteryService.isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName; applyPowerProfile();
if (profileValue !== "") {
const targetProfile = parseInt(profileValue);
if (!isNaN(targetProfile) && PowerProfiles.profile !== targetProfile) {
PowerProfiles.profile = targetProfile;
}
}
previousPluggedState = isPluggedIn; previousPluggedState = isPluggedIn;
} }
+19 -8
View File
@@ -15,8 +15,10 @@ Item {
property string socketPath: "" property string socketPath: ""
readonly property bool socketFound: socketPath.length > 0 readonly property bool socketFound: socketPath.length > 0
property bool connected: false property bool connected: false
property bool binaryExists: false readonly property bool binaryExists: launchKind.length > 0
property bool binaryChecked: false property bool binaryChecked: false
// "dcal" for a host install, "flatpak" for a sandboxed one, "" for neither.
property string launchKind: ""
property var calendars: [] property var calendars: []
property var events: [] property var events: []
@@ -55,18 +57,20 @@ Item {
Process { Process {
id: binaryCheck id: binaryCheck
command: ["sh", "-c", "command -v dcal"] command: ["sh", "-c", "if command -v dcal >/dev/null 2>&1; then echo dcal; elif command -v flatpak >/dev/null 2>&1 && flatpak info com.danklinux.dankcalendar >/dev/null 2>&1; then echo flatpak; fi"]
running: false running: false
onExited: code => { stdout: StdioCollector {
root.binaryExists = (code === 0); onStreamFinished: {
root.binaryChecked = true; root.launchKind = text.trim();
root.binaryChecked = true;
}
} }
} }
Process { Process {
id: discoverProcess id: discoverProcess
running: false running: false
command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"] command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/app/com.danklinux.dankcalendar/dankcal-*.sock; do if [ -S \"$f\" ]; then echo \"$f\"; exit 0; fi; done; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
@@ -98,9 +102,16 @@ Item {
} }
function launch() { function launch() {
if (!binaryExists) switch (launchKind) {
case "dcal":
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]);
break;
case "flatpak":
Quickshell.execDetached(["flatpak", "run", "com.danklinux.dankcalendar", "run", "-d", "--hidden"]);
break;
default:
return; return;
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]); }
if (enabled && !connected) if (enabled && !connected)
discoverProcess.running = true; discoverProcess.running = true;
} }
+1 -3
View File
@@ -56,7 +56,7 @@ Singleton {
return "wifi_1_bar"; return "wifi_1_bar";
} }
property string userPreference: "auto" readonly property string userPreference: SettingsData.networkPreference
property bool isConnecting: false property bool isConnecting: false
property string connectingSSID: "" property string connectingSSID: ""
property string connectionError: "" property string connectionError: ""
@@ -187,7 +187,6 @@ Singleton {
} }
Component.onCompleted: { Component.onCompleted: {
root.userPreference = SettingsData.networkPreference;
lastConnectedVpnUuid = SessionData.vpnLastConnected || ""; lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
checkDMSCapabilities(); checkDMSCapabilities();
@@ -672,7 +671,6 @@ Singleton {
function setNetworkPreference(preference) { function setNetworkPreference(preference) {
if (!networkAvailable) if (!networkAvailable)
return; return;
userPreference = preference;
changingPreference = true; changingPreference = true;
targetPreference = preference; targetPreference = preference;
SettingsData.set("networkPreference", preference); SettingsData.set("networkPreference", preference);
+8 -1
View File
@@ -69,7 +69,7 @@ Singleton {
property bool screensaverInhibited: false property bool screensaverInhibited: false
property var screensaverInhibitors: [] property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"] property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -335,6 +335,7 @@ Singleton {
const data = response.result.data; const data = response.result.data;
if (service === "server") { if (service === "server") {
const prevCapabilities = capabilities;
apiVersion = data.apiVersion || 0; apiVersion = data.apiVersion || 0;
cliVersion = data.cliVersion || ""; cliVersion = data.cliVersion || "";
capabilities = data.capabilities || []; capabilities = data.capabilities || [];
@@ -346,6 +347,12 @@ Singleton {
} }
capabilitiesReceived(); capabilitiesReceived();
const capabilitiesChanged = prevCapabilities.length !== capabilities.length || capabilities.some(c => !prevCapabilities.includes(c));
if (prevCapabilities.length > 0 && capabilitiesChanged) {
log.info("Capabilities changed, resubscribing");
subscribe(activeSubscriptions);
}
} else if (service === "network") { } else if (service === "network") {
networkStateUpdate(data); networkStateUpdate(data);
} else if (service === "network.credentials") { } else if (service === "network.credentials") {
+1 -1
View File
@@ -386,7 +386,7 @@ Singleton {
if (!focusedScreen) if (!focusedScreen)
return ""; return "";
const pins = SettingsData.brightnessDevicePins || {}; const pins = CacheData.brightnessDevicePins || {};
const screenKey = SettingsData.getScreenDisplayName(focusedScreen); const screenKey = SettingsData.getScreenDisplayName(focusedScreen);
if (!screenKey) if (!screenKey)
return ""; return "";
+39 -7
View File
@@ -3,10 +3,13 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property bool wantsLocation: SettingsData.weatherEnabled && SettingsData.useAutoLocation
readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location") readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location")
readonly property bool valid: latitude !== 0 || longitude !== 0 readonly property bool valid: latitude !== 0 || longitude !== 0
@@ -15,19 +18,46 @@ Singleton {
signal locationChanged(var data) signal locationChanged(var data)
onLocationAvailableChanged: { onWantsLocationChanged: {
if (locationAvailable && !valid) if (wantsLocation) {
getState(); ensureSubscription();
} else if (DMSService.activeSubscriptions.includes("location")) {
DMSService.removeSubscription("location");
}
} }
onLocationAvailableChanged: ensureSubscription()
Component.onCompleted: ensureSubscription()
Connections { Connections {
target: DMSService target: DMSService
function onLocationStateUpdate(data) { function onConnectionStateChanged() {
if (!locationAvailable) if (DMSService.isConnected)
return; root.ensureSubscription();
handleStateUpdate(data);
} }
function onLocationStateUpdate(data) {
if (!root.wantsLocation)
return;
root.handleStateUpdate(data);
}
}
function ensureSubscription() {
if (!wantsLocation)
return;
if (!locationAvailable)
return;
if (DMSService.activeSubscriptions.includes("location"))
return;
if (DMSService.activeSubscriptions.includes("all"))
return;
DMSService.addSubscription("location");
if (!valid)
getState();
} }
function handleStateUpdate(data) { function handleStateUpdate(data) {
@@ -42,6 +72,8 @@ Singleton {
} }
function getState() { function getState() {
if (!wantsLocation)
return;
if (!locationAvailable) if (!locationAvailable)
return; return;
-2
View File
@@ -1487,8 +1487,6 @@ window-rule {
if (outputSettings.disabled) { if (outputSettings.disabled) {
kdlContent += ` off\n`; kdlContent += ` off\n`;
kdlContent += `}\n\n`;
continue;
} }
if (output.configured_mode) { if (output.configured_mode) {
+1 -1
View File
@@ -425,7 +425,7 @@ Singleton {
} }
function closeSettings() { function closeSettings() {
settingsModal?.close(); settingsModal?.hide();
} }
function toggleSettings() { function toggleSettings() {
+1 -1
View File
@@ -1 +1 @@
v1.5.2 v1.5.3
File diff suppressed because it is too large Load Diff