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

Compare commits

..

18 Commits

Author SHA1 Message Date
bbedward 41efe6ad15 tailscale: add missing ID
related #2969
port 1.5

(cherry picked from commit ef191babb7)
2026-07-31 13:37:43 +00:00
bbedward 4fb6a17e1b launcher: fix regex escaping launch args
fixes #2961
port 1.5

(cherry picked from commit a710d6d7cc)
2026-07-30 18:35:04 +00:00
bbedward e771e3b675 greeter: lua for hypr fallback 2026-07-30 14:31:39 -04:00
bbedward b1b7aa7aa2 Revert "dock: use required properties in dock item delegate, fixes context injection on Qt 6.8 (#2926)"
This reverts commit b8e2ce1da8.
2026-07-30 14:30:51 -04:00
bbedward c128793239 qs/socket: improve resilience of socket detection and connection
related #2369
port 1.5

(cherry picked from commit 64461c534f)
2026-07-27 17:41:17 +00:00
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
48 changed files with 1520 additions and 215 deletions
+11 -6
View File
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
m.promptBroker = broker
adapter, err := m.findAdapter()
adapter, err := findAdapter(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("no bluetooth adapter found: %w", err)
return nil, err
}
m.adapterPath = adapter
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
return m, nil
}
func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
obj := conn.Object(bluezService, dbus.ObjectPath("/"))
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
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 {
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
}
}
return "", fmt.Errorf("no adapter found")
return "", ErrNoAdapter
}
func (m *Manager) initialize() error {
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() 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)
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 {
ch := make(chan CUPSState, 64)
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
wasEmpty := true
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
wasEmpty = false
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
m.subscribers.Store(id, ch)
if wasEmpty && m.subscription != nil {
if err := m.subscription.Start(); err != nil {
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
} else {
m.eventWG.Add(1)
go m.eventHandler()
}
if !wasEmpty || m.subscription == nil {
return ch
}
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
}
func (m *Manager) Unsubscribe(id string) {
m.subLifecycleMu.Lock()
defer m.subLifecycleMu.Unlock()
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
return false
})
if isEmpty && m.subscription != nil {
m.subscription.Stop()
m.eventWG.Wait()
if !isEmpty || m.subscription == nil {
return
}
m.subscription.Stop()
m.eventWG.Wait()
}
func (m *Manager) Close() {
close(m.stopChan)
m.subLifecycleMu.Lock()
if m.subscription != nil {
m.subscription.Stop()
}
m.subLifecycleMu.Unlock()
m.eventWG.Wait()
m.notifierWg.Wait()
+64
View File
@@ -1,6 +1,9 @@
package cups
import (
"errors"
"fmt"
"sync"
"testing"
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)
}
// 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) {
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
+1
View File
@@ -79,6 +79,7 @@ type Manager struct {
client CUPSClientInterface
pkHelper PkHelper
subscription SubscriptionManagerInterface
subLifecycleMu sync.Mutex
stateMutex sync.RWMutex
subscribers syncmap.Map[string, chan CUPSState]
stopChan chan struct{}
+14 -3
View File
@@ -1731,10 +1731,21 @@ func Start(printDocs bool) error {
}()
go func() {
if err := InitializeBluezManager(); err != nil {
for {
err := InitializeBluezManager()
if err == nil {
notifyCapabilityChange()
return
}
log.Warnf("Bluez manager unavailable: %v", err)
} else {
notifyCapabilityChange()
if !errors.Is(err, bluez.ErrNoAdapter) {
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":
argv = []string{term, "-p", "tabtitle=" + title}
case "gnome-terminal":
argv = []string{term, "--title=" + title}
// --wait: the factory process otherwise returns immediately
argv = []string{term, "--wait", "--title=" + title}
execFlag = "--"
default:
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) }
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
code := ErrCodeBackendFailed
switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded):
code = ErrCodeTimeout
m.failCustomUpgrade(ErrCodeTimeout, err)
return
case errors.Is(ctx.Err(), context.Canceled):
code = ErrCodeCancelled
m.failCustomUpgrade(ErrCodeCancelled, err)
return
}
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
return
// exit status reflects the trailing `read`, not the update command
m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
}
m.finishSuccessfulUpgrade(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) {
m.appendLog("Upgrade complete.")
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
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 {
lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity))
}
if a.OpenFloating != nil && *a.OpenFloating {
lines = append(lines, " open-floating true")
if a.OpenFloating != nil {
lines = append(lines, fmt.Sprintf(" open-floating %t", *a.OpenFloating))
}
if a.OpenMaximized != nil && *a.OpenMaximized {
lines = append(lines, " open-maximized true")
+64 -6
View File
@@ -11,7 +11,7 @@ Singleton {
id: root
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"
@@ -19,10 +19,21 @@ Singleton {
readonly property string _stateDir: Paths.strip(_stateUrl)
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 profileLastPath: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property var fileBrowserSettings: ({
"wallpaper": {
"lastPath": "",
@@ -82,8 +93,46 @@ Singleton {
function loadCache() {
_loading = true;
parseCache(cacheFile.text());
_loading = false;
try {
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) {
@@ -91,6 +140,7 @@ Singleton {
try {
if (content && content.trim()) {
const cache = JSON.parse(content);
_loadedCacheVersion = cache.configVersion || 0;
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "";
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) {
migrateFromUndefinedToV1(cache);
cleanupUnusedKeys();
@@ -142,12 +196,16 @@ Singleton {
function saveCache() {
if (_loading)
return;
cacheFile.setText(JSON.stringify({
const data = {
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"fileBrowserSettings": fileBrowserSettings,
"configVersion": cacheConfigVersion
}, null, 2));
};
for (const key of _pinKeys) {
data[key] = root[key];
}
cacheFile.setText(JSON.stringify(data, null, 2));
}
function migrateFromUndefinedToV1(cache) {
@@ -155,7 +213,7 @@ Singleton {
}
function cleanupUnusedKeys() {
const validKeys = ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "configVersion"];
const validKeys = _dataKeys.concat(["configVersion"]);
try {
const content = cacheFile.text();
+4 -6
View File
@@ -15,7 +15,7 @@ Singleton {
id: root
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"
@@ -773,11 +773,6 @@ Singleton {
property bool fadeToDpmsEnabled: true
property int fadeToDpmsGracePeriod: 5
property string launchPrefix: ""
property var brightnessDevicePins: ({})
property var wifiNetworkPins: ({})
property var bluetoothDevicePins: ({})
property var audioInputDevicePins: ({})
property var audioOutputDevicePins: ({})
property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false
@@ -1741,6 +1736,7 @@ Singleton {
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
const oldVersion = obj?.configVersion ?? 0;
const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null;
if (oldVersion < settingsConfigVersion) {
const migrated = Store.migrateToVersion(obj, settingsConfigVersion);
if (migrated) {
@@ -1748,6 +1744,8 @@ Singleton {
obj = migrated;
}
}
if (legacyPins)
Qt.callLater(() => CacheData.migratePins(legacyPins));
if (obj?.lockScreenActiveMonitor !== undefined) {
var oldVal = obj.lockScreenActiveMonitor;
@@ -354,11 +354,6 @@ var SPEC = {
fadeToDpmsEnabled: { def: true },
fadeToDpmsGracePeriod: { def: 5 },
launchPrefix: { def: "" },
brightnessDevicePins: { def: {} },
wifiNetworkPins: { def: {} },
bluetoothDevicePins: { def: {} },
audioInputDevicePins: { def: {} },
audioOutputDevicePins: { def: {} },
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
@@ -2,6 +2,21 @@
.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) {
var SPEC = SpecModule.SPEC;
@@ -263,6 +278,17 @@ function migrateToVersion(obj, targetVersion) {
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;
}
+17 -1
View File
@@ -52,6 +52,7 @@ FloatingWindow {
condInitialised.triState = 0;
opacityEnabled.checked = false;
opacitySlider.value = 100;
floatingCond.triState = 0;
floatingToggle.checked = false;
maximizedToggle.checked = false;
maximizedToEdgesToggle.checked = false;
@@ -164,6 +165,7 @@ FloatingWindow {
opacityEnabled.checked = hasOpacity;
opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100;
floatingCond.triState = triFromBool(actions.openFloating);
floatingToggle.checked = actions.openFloating || false;
maximizedToggle.checked = actions.openMaximized || false;
maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false;
@@ -318,7 +320,9 @@ FloatingWindow {
if (opacityEnabled.checked)
actions.opacity = opacitySlider.value / 100;
if (floatingToggle.checked)
if (isNiri)
applyCond(actions, "openFloating", floatingCond.triState);
else if (floatingToggle.checked)
actions.openFloating = true;
if (maximizedToggle.checked)
actions.openMaximized = true;
@@ -943,6 +947,17 @@ FloatingWindow {
title: I18n.tr("Window Opening")
}
Flow {
width: parent.width
spacing: Theme.spacingS
visible: isNiri
MatchCond {
id: floatingCond
label: I18n.tr("Float")
}
}
Flow {
width: parent.width
spacing: Theme.spacingL
@@ -950,6 +965,7 @@ FloatingWindow {
CheckboxRow {
id: floatingToggle
label: I18n.tr("Float")
visible: !isNiri
}
CheckboxRow {
id: maximizedToggle
@@ -469,6 +469,8 @@ PluginComponent {
}
MouseArea {
id: peerMouseArea
z: -1
anchors.fill: parent
hoverEnabled: true
@@ -159,7 +159,7 @@ Rectangle {
}
function getPinnedInputs() {
const pins = SettingsData.audioInputDevicePins || {};
const pins = CacheData.audioInputDevicePins || {};
return normalizePinList(pins["preferredInput"]);
}
@@ -315,7 +315,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
const pins = JSON.parse(JSON.stringify(CacheData.audioInputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredInput"]);
const pinIndex = pinnedList.indexOf(modelData.name);
@@ -332,7 +332,7 @@ Rectangle {
else
delete pins["preferredInput"];
SettingsData.set("audioInputDevicePins", pins);
CacheData.set("audioInputDevicePins", pins);
}
}
}
@@ -169,7 +169,7 @@ Rectangle {
}
function getPinnedOutputs() {
const pins = SettingsData.audioOutputDevicePins || {};
const pins = CacheData.audioOutputDevicePins || {};
return normalizePinList(pins["preferredOutput"]);
}
@@ -324,7 +324,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
const pins = JSON.parse(JSON.stringify(CacheData.audioOutputDevicePins || {}));
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
const pinIndex = pinnedList.indexOf(modelData.name);
@@ -341,7 +341,7 @@ Rectangle {
else
delete pins["preferredOutput"];
SettingsData.set("audioOutputDevicePins", pins);
CacheData.set("audioOutputDevicePins", pins);
}
}
}
@@ -112,7 +112,7 @@ Rectangle {
}
function getPinnedDevices() {
const pins = SettingsData.bluetoothDevicePins || {};
const pins = CacheData.bluetoothDevicePins || {};
return normalizePinList(pins["preferredDevice"]);
}
@@ -400,7 +400,7 @@ Rectangle {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}));
const pins = JSON.parse(JSON.stringify(CacheData.bluetoothDevicePins || {}));
let pinnedList = root.normalizePinList(pins["preferredDevice"]);
const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
@@ -418,7 +418,7 @@ Rectangle {
delete pins["preferredDevice"];
}
SettingsData.set("bluetoothDevicePins", pins);
CacheData.set("bluetoothDevicePins", pins);
}
}
}
@@ -37,7 +37,7 @@ Rectangle {
const pinKey = getScreenPinKey();
if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {};
const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) {
const found = devices.find(d => d.name === pinnedDevice);
@@ -85,12 +85,12 @@ Rectangle {
}
const pinKey = getScreenPinKey();
if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {};
const pins = CacheData.brightnessDevicePins || {};
const existing = pins[pinKey];
if (existing && existing !== deviceName) {
const next = JSON.parse(JSON.stringify(pins));
delete next[pinKey];
SettingsData.set("brightnessDevicePins", next);
CacheData.set("brightnessDevicePins", next);
}
}
root.currentDeviceName = deviceName;
@@ -106,7 +106,7 @@ Rectangle {
const pinKey = getScreenPinKey();
if (!pinKey || !deviceName)
return false;
const pins = SettingsData.brightnessDevicePins || {};
const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] === deviceName;
}
@@ -114,13 +114,13 @@ Rectangle {
const pinKey = getScreenPinKey();
if (!pinKey || !deviceName)
return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
if (pins[pinKey] === deviceName) {
delete pins[pinKey];
} else {
pins[pinKey] = deviceName;
}
SettingsData.set("brightnessDevicePins", pins);
CacheData.set("brightnessDevicePins", pins);
}
implicitHeight: {
@@ -269,7 +269,7 @@ Rectangle {
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
readonly property bool devicePinnedHere: {
SettingsData.brightnessDevicePins;
CacheData.brightnessDevicePins;
return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
}
@@ -49,7 +49,7 @@ Rectangle {
}
function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {};
const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]);
}
@@ -710,7 +710,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = root.normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(modelData.ssid);
@@ -727,7 +727,7 @@ Rectangle {
else
delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins);
CacheData.set("wifiNetworkPins", pins);
}
}
}
@@ -34,7 +34,7 @@ Row {
if (screenName && screenName.length > 0) {
const screen = Quickshell.screens.find(s => s.name === screenName);
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
const pins = SettingsData.brightnessDevicePins || {};
const pins = CacheData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
@@ -29,6 +29,8 @@ Item {
property var centerWidgets: []
property int totalWidgets: 0
property real totalSize: 0
property real contentStart: 0
property real contentSize: 0
function updateLayout() {
if (SettingsData.centeringMode === "geometric") {
@@ -36,6 +38,25 @@ Item {
} else {
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() {
+18 -4
View File
@@ -833,16 +833,30 @@ PanelWindow {
const pos = section.mapToItem(barWindow.contentItem, 0, 0);
const implW = section.implicitWidth || 0;
const implH = section.implicitHeight || 0;
const contentSize = isCenter ? (section.contentSize || 0) : 0;
const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
let offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 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;
return {
"x": pos.x + offsetX - edgePad,
"y": pos.y + offsetY - edgePad,
"w": implW + edgePad * 2,
"h": implH + edgePad * 2
"w": w + edgePad * 2,
"h": h + edgePad * 2
};
}
@@ -203,7 +203,7 @@ BasePill {
const pinKey = getScreenPinKey();
if (!pinKey)
return "";
const pins = SettingsData.brightnessDevicePins || {};
const pins = CacheData.brightnessDevicePins || {};
return pins[pinKey] || "";
}
@@ -35,44 +35,42 @@ BasePill {
}
}
onRightClicked: (rx, ry) => {
onRightClicked: {
const screen = root.parentScreen || Screen;
if (!screen)
return;
const globalPos = root.visualContent.mapToItem(null, 0, 0);
const isVertical = root.axis?.isVertical ?? false;
const edge = root.axis?.edge ?? "top";
const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
const barOffset = root.barThickness + root.barSpacing + gap;
const localPos = root.visualContent.mapToItem(null, root.visualContent.width / 2, root.visualContent.height / 2);
let anchorX;
let anchorY;
let anchorEdge;
if (isVertical) {
anchorY = globalPos.y - (screen.y || 0) + root.visualContent.height / 2;
if (edge === "left") {
anchorX = barOffset;
anchorEdge = "top";
} else {
anchorX = screen.width - barOffset;
anchorEdge = "top";
}
anchorX = edge === "left" ? barOffset : screen.width - barOffset;
anchorY = localPos.y;
} else {
anchorX = globalPos.x - (screen.x || 0) + root.visualContent.width / 2;
if (edge === "bottom") {
anchorY = screen.height - barOffset;
anchorEdge = "bottom";
} else {
anchorY = barOffset;
anchorEdge = "top";
}
anchorX = localPos.x;
anchorY = edge === "bottom" ? screen.height - barOffset : barOffset;
}
dndPopupLoader.active = true;
const popup = dndPopupLoader.item;
if (!popup)
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 {
+12 -8
View File
@@ -386,16 +386,20 @@ end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1
TEMP_CONFIG=$(mktemp --suffix=.lua)
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
hl.env("DMS_RUN_GREETER", "1")
misc {
disable_hyprland_logo = true
}
hl.config({
misc = {
disable_hyprland_logo = true,
},
})
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
hl.on("hyprland.start", function()
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
@@ -31,14 +31,16 @@ PanelWindow {
}
property point anchorPos: Qt.point(0, 0)
property string anchorEdge: "top"
property bool isVertical: false
property string edge: "top"
visible: false
function showAt(x, y, targetScreen, edge) {
function showAt(x, y, vertical, barEdge, targetScreen) {
if (targetScreen)
root.screen = targetScreen;
anchorPos = Qt.point(x, y);
anchorEdge = edge || "top";
isVertical = vertical ?? false;
edge = barEdge ?? "top";
visible = true;
}
@@ -66,21 +68,19 @@ PanelWindow {
visible: root.visible
x: {
const left = 10;
const right = root.width - width - 10;
const want = root.anchorPos.x - width / 2;
return Math.max(left, Math.min(right, want));
if (root.isVertical) {
if (root.edge === "left")
return Math.min(root.width - width - 10, root.anchorPos.x);
return Math.max(10, root.anchorPos.x - width);
}
return Math.max(10, Math.min(root.width - width - 10, root.anchorPos.x - width / 2));
}
y: {
switch (root.anchorEdge) {
case "bottom":
return Math.max(10, root.anchorPos.y - height);
case "left":
case "right":
if (root.isVertical)
return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2));
default:
return Math.min(root.height - height - 10, root.anchorPos.y);
}
if (root.edge === "bottom")
return Math.max(10, root.anchorPos.y - height);
return Math.min(root.height - height - 10, root.anchorPos.y);
}
onDismissed: root.closeMenu()
@@ -375,6 +375,8 @@ Item {
visible: DesktopService.autostartAvailable
SettingsCard {
settingKey: "autostartAddEntry"
tags: ["autostart", "add", "entry", "command", "startup"]
width: parent.width
iconName: "add_circle"
title: I18n.tr("Add Entry")
@@ -764,6 +766,8 @@ Item {
}
SettingsCard {
settingKey: "autostartTrayIconFix"
tags: ["tray", "icons", "fix", "systemd"]
width: parent.width
iconName: "handyman"
title: I18n.tr("Tray Icon Fix")
@@ -463,6 +463,7 @@ Item {
}
SettingsCard {
settingKey: "barEnable"
iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off"
title: I18n.tr("Enable Bar")
visible: !dankBarTab.appearanceOnly && selectedBarId !== "default"
@@ -651,6 +652,8 @@ Item {
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
SettingsToggleRow {
settingKey: "barAutoHide"
tags: ["autohide", "auto-hide", "reveal", "intellihide"]
text: I18n.tr("Auto-hide")
description: I18n.tr("Automatically hide the bar when the pointer moves away")
checked: selectedBarConfig?.autoHide ?? false
@@ -700,6 +703,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barAutoHideStrict"
tags: ["autohide", "strict", "popout"]
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")
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
@@ -713,6 +718,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barHideWhenWindowsOpen"
tags: ["hide", "windows", "empty", "workspace"]
width: parent.width - parent.leftPadding
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
text: I18n.tr("Hide When Windows Open")
@@ -734,6 +741,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barManualVisibility"
tags: ["manual", "show", "hide", "ipc", "toggle"]
text: I18n.tr("Manual Show/Hide")
description: I18n.tr("Toggle bar visibility manually via IPC")
checked: selectedBarConfig?.visible ?? true
@@ -753,6 +762,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barClickThrough"
tags: ["clickthrough", "click", "through", "mouse", "input", "mask", "passthrough"]
text: I18n.tr("Click Through")
description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
checked: selectedBarConfig?.clickThrough ?? false
@@ -910,6 +921,8 @@ Item {
SettingsSliderRow {
id: exclusiveZoneSlider
settingKey: "barExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
visible: !SettingsData.frameEnabled
text: I18n.tr("Exclusive Zone Offset")
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
@@ -933,6 +946,8 @@ Item {
SettingsSliderRow {
id: sizeSlider
settingKey: "barSize"
tags: ["size", "thickness", "height", "inner"]
visible: !SettingsData.frameEnabled
text: I18n.tr("Size")
description: I18n.tr("Adjust the bar height via inner padding")
@@ -1154,6 +1169,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barSquareCorners"
tags: ["square", "corners", "rounding"]
text: I18n.tr("Square Corners")
description: I18n.tr("Remove corner rounding from the bar")
visible: !SettingsData.frameEnabled
@@ -1164,6 +1181,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barNoBackground"
tags: ["transparent", "background", "invisible"]
text: I18n.tr("No Background")
description: I18n.tr("Make the bar background fully transparent")
visible: !SettingsData.frameEnabled
@@ -1174,6 +1193,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barMaximizeWidgetIcons"
tags: ["maximize", "icons", "stretch"]
text: I18n.tr("Maximize Widget Icons")
description: I18n.tr("Stretch widget icons to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetIcons ?? false
@@ -1183,6 +1204,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barMaximizeWidgetText"
tags: ["maximize", "text", "stretch"]
text: I18n.tr("Maximize Widget Text")
description: I18n.tr("Stretch widget text to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetText ?? false
@@ -1192,6 +1215,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barRemoveWidgetPadding"
tags: ["padding", "compact", "widgets"]
text: I18n.tr("Remove Widget Padding")
description: I18n.tr("Remove inner padding from all widgets")
checked: selectedBarConfig?.removeWidgetPadding ?? false
@@ -1208,6 +1233,8 @@ Item {
}
SettingsToggleRow {
settingKey: "barGothCorners"
tags: ["goth", "corners", "concave", "cutout"]
text: I18n.tr("Goth Corners")
description: I18n.tr("Apply inverse concave corner cutouts to the bar")
visible: !SettingsData.frameEnabled
@@ -1302,6 +1329,8 @@ Item {
}
SettingsToggleCard {
settingKey: "barMaximizeDetection"
tags: ["maximize", "gaps", "border", "fullscreen"]
iconName: "fit_screen"
title: I18n.tr("Maximize Detection")
description: I18n.tr("Remove gaps and border when windows are maximized")
@@ -1847,6 +1876,8 @@ Item {
SettingsToggleCard {
iconName: "mouse"
settingKey: "barScrollWheel"
tags: ["scroll", "wheel", "workspace", "axis"]
title: I18n.tr("Scroll Wheel")
description: I18n.tr("Control workspaces and columns by scrolling on the bar")
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
@@ -267,6 +267,8 @@ Item {
spacing: Theme.spacingXL
SettingsCard {
settingKey: "defaultAppsInternet"
tags: ["browser", "mail", "email", "web"]
title: I18n.tr("Internet", "Internet")
iconName: "public"
@@ -293,6 +295,8 @@ Item {
}
SettingsCard {
settingKey: "defaultAppsUtilities"
tags: ["file", "manager", "terminal", "editor"]
title: I18n.tr("Utilities", "Utilities")
iconName: "terminal"
@@ -317,6 +321,8 @@ Item {
}
SettingsCard {
settingKey: "defaultAppsDocuments"
tags: ["pdf", "text", "reader", "office"]
title: I18n.tr("Documents", "Documents")
iconName: "edit_document"
@@ -335,6 +341,8 @@ Item {
}
SettingsCard {
settingKey: "defaultAppsMultimedia"
tags: ["image", "video", "music", "viewer", "player"]
title: I18n.tr("Multimedia", "Multimedia")
iconName: "movie"
AppSelector {
@@ -166,6 +166,8 @@ Item {
spacing: Theme.spacingXL
SettingsCard {
settingKey: "desktopWidgetsManage"
tags: ["desktop", "widgets", "clock", "conky"]
width: parent.width
iconName: "widgets"
title: I18n.tr("Desktop Widgets")
@@ -203,6 +205,8 @@ Item {
}
SettingsCard {
settingKey: "desktopWidgetGroups"
tags: ["groups", "profiles", "layouts"]
width: parent.width
iconName: "folder"
title: I18n.tr("Groups")
@@ -17,6 +17,7 @@ Singleton {
readonly property var wlrOutputs: WlrOutputService.outputs
property var outputs: ({})
property var savedOutputs: ({})
property var savedParsedOutputs: ({})
property var allOutputs: buildAllOutputsMap()
property var includeStatus: ({
@@ -911,15 +912,18 @@ Singleton {
const paths = getConfigPaths();
if (!paths) {
savedOutputs = {};
savedParsedOutputs = {};
return;
}
Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => {
if (exitCode !== 0 || !content.trim()) {
savedOutputs = {};
savedParsedOutputs = {};
return;
}
const parsed = parseOutputsConfig(content);
savedParsedOutputs = parsed;
const filtered = filterDisconnectedOnly(parsed);
savedOutputs = filtered;
@@ -1092,20 +1096,7 @@ Singleton {
const name = match[1];
const body = match[2];
if (body.trim() === "off") {
result[name] = {
"name": name,
"disabled": true,
"logical": {
"x": 0,
"y": 0,
"scale": 1.0,
"transform": "Normal"
}
};
continue;
}
const disabled = /^\s*off\s*$/m.test(body);
const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
const scaleMatch = body.match(/scale\s+([\d.]+)/);
@@ -1115,6 +1106,7 @@ Singleton {
result[name] = {
"name": name,
"disabled": disabled,
"logical": {
"x": posMatch ? parseInt(posMatch[1]) : 0,
"y": posMatch ? parseInt(posMatch[2]) : 0,
@@ -1778,6 +1770,41 @@ Singleton {
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() {
const result = {};
@@ -1787,7 +1814,10 @@ Singleton {
}
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) {
@@ -1799,6 +1829,8 @@ Singleton {
result[outputName].logical.y = changes.position.y;
}
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++) {
if (formatMode(result[outputName].modes[i]) === changes.mode) {
result[outputName].current_mode = i;
+2
View File
@@ -618,6 +618,8 @@ Item {
}
SettingsSliderRow {
settingKey: "dockExclusiveZone"
tags: ["exclusive", "zone", "reserved", "offset"]
text: I18n.tr("Exclusive Zone Offset")
visible: !root.connectedFrameModeActive || root.connectedPersistentDockActive
value: SettingsData.dockBottomGap
@@ -38,6 +38,8 @@ Item {
settingKey: "mediaPlayer"
SettingsToggleRow {
settingKey: "mediaWaveProgress"
tags: ["wave", "progress", "animated"]
text: I18n.tr("Wave Progress Bars")
description: I18n.tr("Use animated wave progress bars for media playback")
checked: SettingsData.waveProgressEnabled
@@ -45,6 +47,8 @@ Item {
}
SettingsToggleRow {
settingKey: "mediaScrollTitle"
tags: ["scroll", "title", "marquee"]
text: I18n.tr("Scroll song title")
description: I18n.tr("Scroll title if it doesn't fit in widget")
checked: SettingsData.scrollTitleEnabled
@@ -52,6 +56,8 @@ Item {
}
SettingsToggleRow {
settingKey: "mediaVisualizer"
tags: ["visualizer", "cava", "spectrum"]
text: I18n.tr("Audio Visualizer")
description: I18n.tr("Show cava audio visualizer in media widget")
checked: SettingsData.audioVisualizerEnabled
@@ -59,6 +65,8 @@ Item {
}
SettingsToggleRow {
settingKey: "mediaAdaptiveWidth"
tags: ["adaptive", "width", "shrink"]
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")
checked: SettingsData.mediaAdaptiveWidthEnabled
@@ -66,6 +74,8 @@ Item {
}
SettingsToggleRow {
settingKey: "mediaAlbumArtAccent"
tags: ["album", "art", "accent", "colors"]
text: I18n.tr("Use album art accent")
description: I18n.tr("Use colors extracted from album art instead of system theme colors")
checked: SettingsData.mediaUseAlbumArtAccent
@@ -136,6 +146,8 @@ Item {
}
SettingsToggleRow {
settingKey: "mediaDeviceScrollVolume"
tags: ["device", "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")
checked: SettingsData.audioDeviceScrollVolumeEnabled
+1
View File
@@ -81,6 +81,7 @@ Item {
SettingsCard {
tab: "mux"
tags: ["mux", "session", "filter", "exclude", "hide"]
settingKey: "muxSessionFilter"
title: I18n.tr("Session Filter")
iconName: "filter_list"
@@ -55,12 +55,12 @@ Item {
}
function getPinnedWifiNetworks() {
const pins = SettingsData.wifiNetworkPins || {};
const pins = CacheData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]);
}
function toggleWifiPin(ssid) {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
let pinnedList = normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(ssid);
@@ -77,7 +77,7 @@ Item {
else
delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins);
CacheData.set("wifiNetworkPins", pins);
}
property var forgetNetworkConfirm: ConfirmModal {}
@@ -89,6 +89,8 @@ Item {
}
SettingsDropdownRow {
settingKey: "systemUpdaterCheckInterval"
tags: ["interval", "poll", "frequency"]
text: I18n.tr("Check interval")
description: I18n.tr("How often the server polls for new updates.")
options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel])
@@ -157,6 +159,8 @@ Item {
}
SettingsToggleRow {
settingKey: "systemUpdaterCheckOnStart"
tags: ["startup", "check", "boot"]
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.")
checked: SettingsData.updaterCheckOnStart
@@ -164,6 +168,8 @@ Item {
}
SettingsToggleRow {
settingKey: "systemUpdaterFlatpak"
tags: ["flatpak", "include"]
text: I18n.tr("Include Flatpak updates")
description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
@@ -172,6 +178,8 @@ Item {
}
SettingsToggleRow {
settingKey: "systemUpdaterAUR"
tags: ["aur", "paru", "yay"]
text: I18n.tr("Include AUR updates")
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")
@@ -328,6 +336,8 @@ Item {
settingKey: "systemUpdaterAdvanced"
SettingsToggleRow {
settingKey: "systemUpdaterCustomCommand"
tags: ["custom", "command", "terminal"]
text: I18n.tr("Use Custom Command")
description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.")
checked: SettingsData.updaterUseCustomCommand
@@ -167,7 +167,13 @@ FloatingWindow {
width: parent.width
height: parent.height - searchField.height - Theme.spacingM
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
delegate: Rectangle {
@@ -316,11 +322,21 @@ FloatingWindow {
function show() {
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;
Qt.callLater(() => searchField.forceActiveFocus());
}
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;
searchQuery = "";
filteredApps = [];
@@ -330,6 +346,9 @@ FloatingWindow {
onVisibleChanged: {
if (!visible) {
// Guard against visibility being cleared without going through hide()
// (e.g. window manager close, FloatingWindow.onClosed -> hide()).
appList.model = null;
searchQuery = "";
filteredApps = [];
selectedIndex = -1;
+17 -9
View File
@@ -17,7 +17,22 @@ Singleton {
interval: 500
repeat: false
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")
@@ -206,14 +221,7 @@ Singleton {
}
}
const profileValue = BatteryService.isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName;
if (profileValue !== "") {
const targetProfile = parseInt(profileValue);
if (!isNaN(targetProfile) && PowerProfiles.profile !== targetProfile) {
PowerProfiles.profile = targetProfile;
}
}
applyPowerProfile();
previousPluggedState = isPluggedIn;
}
+37 -53
View File
@@ -11,7 +11,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("DMSService")
property bool dmsAvailable: false
readonly property bool dmsAvailable: isConnected
property var capabilities: []
property int apiVersion: 0
property string cliVersion: ""
@@ -21,7 +21,7 @@ Singleton {
property var availableThemes: []
property var installedThemes: []
property bool isConnected: false
property bool isConnecting: false
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
property bool subscribeConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -72,9 +72,10 @@ Singleton {
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: {
if (socketPath && socketPath.length > 0) {
detectUpdateCommand();
}
if (!socketPath || socketPath.length === 0)
return;
detectUpdateCommand();
requestSocket.connected = true;
}
function detectUpdateCommand() {
@@ -82,12 +83,6 @@ Singleton {
checkAurHelper.running = true;
}
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true;
}
}
Process {
id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"]
@@ -105,7 +100,6 @@ Singleton {
} else {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
@@ -114,7 +108,6 @@ Singleton {
if (exitCode !== 0) {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
@@ -135,7 +128,6 @@ Singleton {
updateCommand = "dms update";
}
checkingUpdateCommand = false;
startSocketConnection();
}
}
@@ -143,52 +135,27 @@ Singleton {
if (exitCode !== 0) {
updateCommand = "dms update";
checkingUpdateCommand = false;
startSocketConnection();
}
}
}
Process {
id: testProcess
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
if (exitCode === 0) {
root.dmsAvailable = true;
connectSocket();
} else {
root.dmsAvailable = false;
}
}
}
function connectSocket() {
if (!dmsAvailable || isConnected || isConnecting) {
return;
}
isConnecting = true;
requestSocket.connected = true;
}
DankSocket {
id: requestSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (connected) {
if (linkUp) {
root.isConnected = true;
root.isConnecting = false;
root.connectionStateChanged();
subscribeSocket.connected = true;
} else {
root.isConnected = false;
root.isConnecting = false;
root.apiVersion = 0;
root.capabilities = [];
root.connectionStateChanged();
return;
}
root.isConnected = false;
root.apiVersion = 0;
root.capabilities = [];
root.failPendingRequests();
root.connectionStateChanged();
}
parser: SplitParser {
@@ -219,10 +186,10 @@ Singleton {
connected: false
onConnectionStateChanged: {
root.subscribeConnected = connected;
if (connected) {
sendSubscribeRequest();
}
root.subscribeConnected = linkUp;
if (!linkUp)
return;
sendSubscribeRequest();
}
parser: SplitParser {
@@ -335,6 +302,7 @@ Singleton {
const data = response.result.data;
if (service === "server") {
const prevCapabilities = capabilities;
apiVersion = data.apiVersion || 0;
cliVersion = data.cliVersion || "";
capabilities = data.capabilities || [];
@@ -346,6 +314,12 @@ Singleton {
}
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") {
networkStateUpdate(data);
} else if (service === "network.credentials") {
@@ -439,10 +413,20 @@ Singleton {
function handleResponse(response) {
const callback = pendingRequests[response.id];
if (!callback)
return;
delete pendingRequests[response.id];
callback(response);
}
if (callback) {
delete pendingRequests[response.id];
callback(response);
function failPendingRequests() {
const pending = pendingRequests;
pendingRequests = {};
clipboardRequestIds = {};
for (const id in pending) {
pending[id]({
"error": "not connected to DMS socket"
});
}
}
+1 -1
View File
@@ -386,7 +386,7 @@ Singleton {
if (!focusedScreen)
return "";
const pins = SettingsData.brightnessDevicePins || {};
const pins = CacheData.brightnessDevicePins || {};
const screenKey = SettingsData.getScreenDisplayName(focusedScreen);
if (!screenKey)
return "";
-2
View File
@@ -1487,8 +1487,6 @@ window-rule {
if (outputSettings.disabled) {
kdlContent += ` off\n`;
kdlContent += `}\n\n`;
continue;
}
if (output.configured_mode) {
+59 -2
View File
@@ -217,6 +217,64 @@ Singleton {
return envObj;
}
function splitShellArgs(str) {
const args = [];
let current = "";
let hasToken = false;
let quote = "";
let escaped = false;
for (const ch of str) {
if (escaped) {
current += ch;
escaped = false;
continue;
}
switch (quote) {
case "'":
if (ch === "'") {
quote = "";
continue;
}
current += ch;
continue;
case "\"":
switch (ch) {
case "\"":
quote = "";
continue;
case "\\":
escaped = true;
continue;
}
current += ch;
continue;
}
switch (ch) {
case "\\":
escaped = true;
continue;
case "'":
case "\"":
quote = ch;
hasToken = true;
continue;
case " ":
case "\t":
case "\n":
if (!hasToken && current.length === 0)
continue;
args.push(current);
current = "";
hasToken = false;
continue;
}
current += ch;
}
if (current.length > 0 || hasToken)
args.push(current);
return args;
}
function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command)
return;
@@ -230,8 +288,7 @@ Singleton {
cmd = [nvidiaCommand].concat(cmd);
if (override?.extraFlags) {
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
cmd = cmd.concat(extraArgs);
cmd = cmd.concat(splitShellArgs(override.extraFlags));
}
const userPrefix = SettingsData.launchPrefix?.trim() || "";
+1 -1
View File
@@ -1 +1 @@
v1.5.2
v1.5.3
File diff suppressed because it is too large Load Diff