1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-05 04:58:30 -04:00

Compare commits

...

15 Commits

Author SHA1 Message Date
bbedward c2a7f767c2 bar: fix shadow from tinting goth corners
fixes #2975
port 1.5

(cherry picked from commit 6ad46cf2c2)
2026-08-05 02:15:57 +00:00
bbedward c9c11f8a27 powermenu: add soft reboot option for systemd systems
fixes #2986
port 1.5

(cherry picked from commit e089948225)
2026-08-04 22:05:11 +00:00
bbedward 8cffdab4d7 dank16: correct DPS lab lightness scale
fixes #2987
port 1.5

(cherry picked from commit 7f2ba56e06)
2026-08-04 21:56:09 +00:00
bbedward 0e8a4c0cc1 launcher: dispatch plugin-category items as plugin, not app
fixes #2988
port 1.5

(cherry picked from commit 365474b0d9)
2026-08-04 00:05:48 +00:00
bbedward 1db6381c78 evdev: resolve capslock state from any available keyboard
fixes #2991
port 1.5

(cherry picked from commit dc8a47644a)
2026-08-04 00:00:15 +00:00
bbedward d5c7efc861 dankinstall/gentoo: perform autounmask on install
fixes #2992
port 1.5

(cherry picked from commit 27483e68dc)
2026-08-03 23:40:50 +00:00
purian23 943ffb432c fix(battery): reliably apply configured power profiles at shell startup
Port 1.5

(cherry picked from commit 400a18a8ed)
2026-08-02 23:02:00 +00:00
purian23 7063448b80 refactor(media OSD): add previous/next UI controls
Port 1.5

(cherry picked from commit 32ddf614c3)
2026-08-02 22:10:33 +00:00
purian23 52afbc9801 refactor(mpris): consolidate equivalent-player resolution & update track matching
Port 1.5

(cherry picked from commit 19d919ed5c)
2026-08-02 22:09:37 +00:00
Rubén García 6edb985847 refactor(mpris): enrich metadata across equivalent players (#2928)
* fix(mpris): enrich metadata across equivalent players

* fix(mpris): address review feedback on source stealing and title stripping

- _bestPlayingPlayer: when the active player is playing but not
  controllable, only hand ownership to a controllable peer that passes
  isSameTrack; unrelated players can no longer steal the active source.
- displayTrackTitle: keep the full trackTitle for display. Suffix
  stripping now only applies to the isSameTrack matching key, and only
  for a known app-name allowlist (YouTube, SoundCloud, browsers, etc.)
  so legitimate titles containing ' | ' are never mangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Port 1.5

(cherry picked from commit 594a2cde19)
2026-08-02 21:34:49 +00:00
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
21 changed files with 618 additions and 190 deletions
+22 -7
View File
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
Lf, af, bf := cf.Lab() Lf, af, bf := cf.Lab()
Lf *= 100.0
dir := 1.0 dir := 1.0
if isLightMode { if isLightMode {
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
origL, af, bf := cf.Lab() origL, af, bf := cf.Lab()
origL *= 100.0
var darkerResult, lighterResult string var darkerResult, lighterResult string
darkerL, lighterL := origL, origL darkerL, lighterL := origL, origL
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
return result return result
} }
// color8 sits a fixed L* offset from the background so it keeps its dim role
// regardless of primary brightness (conventional palettes put ANSI bright
// black ~2-2.5:1 from the background, e.g. catppuccin-mocha #585b70 on #1e1e2e)
func DeriveDim(bgHex string, hue, sat float64, isLight bool) string {
offset := 22.0
if isLight {
offset = -offset
}
bgL := getLstar(bgHex)
targetL := math.Max(0, math.Min(100, bgL+offset))
tint := HSVToRGB(HSV{H: hue, S: sat, V: 0.5})
c := colorful.Color{R: tint.R, G: tint.G, B: tint.B}
_, af, bf := c.Lab()
return labToHex(targetL, af, bf)
}
func DeriveContainer(primary string, isLight bool) string { func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary) rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb) hsv := RGBToHSV(rgb)
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := baseVal * 0.28 gray7V := baseVal * 0.28
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.05 palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
gray8V := baseVal * 0.85
dimTarget := secondaryTarget * 0.5
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
brightRedS := math.Min(baseSat*1.0, 1.0) brightRedS := math.Min(baseSat*1.0, 1.0)
brightRedV := math.Min(baseVal*1.2, 1.0) brightRedV := math.Min(baseVal*1.2, 1.0)
@@ -559,9 +576,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := math.Min(baseVal*1.05, 1.0) gray7V := math.Min(baseVal*1.05, 1.0)
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.15 palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
gray8V := baseVal * 0.65
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
brightRedS := math.Min(baseSat*0.75, 1.0) brightRedS := math.Min(baseSat*0.75, 1.0)
brightRedV := math.Min(baseVal*1.35, 1.0) brightRedV := math.Min(baseVal*1.35, 1.0)
+70
View File
@@ -679,3 +679,73 @@ func TestContrastAlgorithmComparison(t *testing.T) {
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount) t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
} }
func TestEnsureContrastDPSLightModeStaysLight(t *testing.T) {
tests := []struct {
name string
result string
bg string
minLstar float64
}{
{
name: "bidirectional adjustment",
result: EnsureContrastDPSBidirectional("#d0ccc6", "#f8f8f8", 17.5, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
{
name: "lstar adjustment",
result: EnsureContrastDPSLstar("#c0c0c0", "#f8f8f8", 30.0, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lstar := getLstar(tt.result)
if lstar < tt.minLstar {
t.Errorf("result %s has L* %.2f on light bg %s, expected >= %.2f (collapsed to near-black)",
tt.result, lstar, tt.bg, tt.minLstar)
}
})
}
}
func TestGeneratePaletteColor8Dim(t *testing.T) {
hues := []string{"#e91e63", "#f59e0b", "#22c55e", "#06b6d4", "#8b5cf6", "#ef4444"}
for _, base := range hues {
t.Run(base, func(t *testing.T) {
palette := GeneratePalette(base, PaletteOptions{IsLight: false, UseDPS: true})
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio < 1.5 || bgRatio > 3.0 {
t.Errorf("Color8 %s vs bg %s ratio %.2f, expected 1.5-3.0 (bright black stays near bg)",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
sepRatio := ContrastRatio(palette.Color4.Hex, palette.Color8.Hex)
if sepRatio < 2.0 {
t.Errorf("Color4 %s vs Color8 %s ratio %.2f, expected >= 2.0 (blue must not collide with bright black)",
palette.Color4.Hex, palette.Color8.Hex, sepRatio)
}
})
}
}
func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
palette := GeneratePalette("#f59e0b", PaletteOptions{IsLight: true, UseDPS: true})
lstar := getLstar(palette.Color8.Hex)
if lstar < 60.0 {
t.Errorf("light mode Color8 %s has L* %.2f, expected >= 60 (dim grey, not near-black)",
palette.Color8.Hex, lstar)
}
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio > 3.0 {
t.Errorf("light mode Color8 %s vs bg %s ratio %.2f, expected <= 3.0",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
}
+9 -7
View File
@@ -53,6 +53,11 @@ func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDi
} }
} }
func emergeInstallArgs(packages []string) []string {
args := []string{"emerge", "--ask=n", "--quiet", "--autounmask-continue=y", "--autounmask-keep-keywords=y", "--autounmask-keep-masks=y"}
return append(args, packages...)
}
func (g *GentooDistribution) getArchKeyword() string { func (g *GentooDistribution) getArchKeyword() string {
arch := runtime.GOARCH arch := runtime.GOARCH
switch arch { switch arch {
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
g.log("Portage tree synced successfully") g.log("Portage tree synced successfully")
args := emergeInstallArgs(missingPkgs)
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
} }
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...)
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
} }
} }
args := []string{"emerge", "--ask=n", "--quiet"} args := emergeInstallArgs(packageNames)
args = append(args, packageNames...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
guruPackages[i] = pkg + "::guru" guruPackages[i] = pkg + "::guru"
} }
args := []string{"emerge", "--ask=n", "--quiet"} args := emergeInstallArgs(guruPackages)
args = append(args, guruPackages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
+25 -21
View File
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to find keyboards: %w", err) return nil, fmt.Errorf("failed to find keyboards: %w", err)
} }
initialCapsLock := readInitialCapsLockState(devices[0]) initialCapsLock, _ := capsLockFromDevices(devices)
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func readInitialCapsLockState(device EvdevDevice) bool { func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
ledStates, err := device.State(evLedType) for _, device := range devices {
if err != nil { if device == nil {
log.Debugf("Could not read LED state: %v", err) continue
return false
} }
return ledStates[ledCapslockKey] ledStates, err := device.State(evLedType)
if err != nil || len(ledStates) == 0 {
continue
}
return ledStates[ledCapslockKey], true
}
return false, false
} }
func findKeyboards() ([]EvdevDevice, error) { func findKeyboards() ([]EvdevDevice, error) {
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
m.devicesMutex.RUnlock() m.devicesMutex.RUnlock()
return return
} }
device := m.devices[deviceIndex] ordered := make([]EvdevDevice, 0, len(m.devices))
ordered = append(ordered, m.devices[deviceIndex])
for i, device := range m.devices {
if i == deviceIndex {
continue
}
ordered = append(ordered, device)
}
m.devicesMutex.RUnlock() m.devicesMutex.RUnlock()
ledStates, err := device.State(evLedType) capsLockState, ok := capsLockFromDevices(ordered)
if err != nil { if !ok {
log.Warnf("Failed to read LED state: %v", err) log.Debug("No LED-capable device available for caps lock state")
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }
+22 -4
View File
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
m.Close() m.Close()
} }
func TestReadInitialCapsLockState(t *testing.T) { func TestCapsLockFromDevices(t *testing.T) {
t.Run("caps lock is on", func(t *testing.T) { t.Run("caps lock is on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{ ledState := evdev.StateMap{
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
} }
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.True(t, ok)
assert.True(t, result) assert.True(t, result)
}) })
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
} }
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.True(t, ok)
assert.False(t, result) assert.False(t, result)
}) })
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.False(t, ok)
assert.False(t, result) assert.False(t, result)
}) })
t.Run("falls back past device without LED state", func(t *testing.T) {
noLedDevice := mocks.NewMockEvdevDevice(t)
noLedDevice.EXPECT().State(evdev.EvType(evLedType)).Return(evdev.StateMap{}, nil).Once()
ledDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: true,
}
ledDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result, ok := capsLockFromDevices([]EvdevDevice{noLedDevice, nil, ledDevice})
assert.True(t, ok)
assert.True(t, result)
})
} }
func TestHasInputGroupAccess(t *testing.T) { func TestHasInputGroupAccess(t *testing.T) {
+3
View File
@@ -982,6 +982,9 @@ Item {
case "reboot": case "reboot":
SessionService.reboot(); SessionService.reboot();
break; break;
case "softreboot":
SessionService.softReboot();
break;
case "poweroff": case "poweroff":
SessionService.poweroff(); SessionService.poweroff();
break; break;
@@ -819,6 +819,13 @@ Item {
} }
if (isCategoryFiltered) { if (isCategoryFiltered) {
var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
if (categoryPluginId) {
var pluginCategoryItems = getPluginItems(categoryPluginId, "");
for (var i = 0; i < pluginCategoryItems.length; i++) {
allItems.push(pluginCategoryItems[i]);
}
} else {
var rawApps = AppSearchService.getAppsInCategory(appCategory); var rawApps = AppSearchService.getAppsInCategory(appCategory);
for (var i = 0; i < rawApps.length; i++) { for (var i = 0; i < rawApps.length; i++) {
allItems.push(getOrTransformApp(rawApps[i])); allItems.push(getOrTransformApp(rawApps[i]));
@@ -830,6 +837,7 @@ Item {
if (coreAppCats.indexOf(appCategory) !== -1) if (coreAppCats.indexOf(appCategory) !== -1)
allItems.push(transformCoreApp(allCoreApps[i])); allItems.push(transformCoreApp(allCoreApps[i]));
} }
}
} else { } else {
var apps = searchApps(searchQuery); var apps = searchApps(searchQuery);
for (var i = 0; i < apps.length; i++) { for (var i = 0; i < apps.length; i++) {
@@ -839,7 +847,7 @@ Item {
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); var newSections = Scorer.groupBySection(scoredItems, buildDynamicSectionDefs(allItems), sortAlpha, searchQuery ? 50 : 500);
for (var sid in collapsedSections) { for (var sid in collapsedSections) {
for (var i = 0; i < newSections.length; i++) { for (var i = 0; i < newSections.length; i++) {
+26 -6
View File
@@ -175,6 +175,8 @@ DankModal {
visibleActions = allActions.filter(action => { visibleActions = allActions.filter(action => {
if (action === "hibernate" && !SessionService.hibernateSupported) if (action === "hibernate" && !SessionService.hibernateSupported)
return false; return false;
if (action === "softreboot" && !SessionService.softRebootSupported)
return false;
return true; return true;
}); });
@@ -223,6 +225,12 @@ DankModal {
"label": I18n.tr("Reboot"), "label": I18n.tr("Reboot"),
"key": "R" "key": "R"
}; };
case "softreboot":
return {
"icon": "autorenew",
"label": I18n.tr("Soft Reboot"),
"key": "B"
};
case "logout": case "logout":
return { return {
"icon": "logout", "icon": "logout",
@@ -370,7 +378,7 @@ DankModal {
function handleListNavigation(event, isPressed) { function handleListNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold(); cancelHold();
event.accepted = true; event.accepted = true;
} }
@@ -429,6 +437,12 @@ DankModal {
event.accepted = true; event.accepted = true;
} }
break; break;
case Qt.Key_B:
if (visibleActions.includes("softreboot")) {
startHold("softreboot", visibleActions.indexOf("softreboot"));
event.accepted = true;
}
break;
case Qt.Key_X: case Qt.Key_X:
if (visibleActions.includes("logout")) { if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout")); startHold("logout", visibleActions.indexOf("logout"));
@@ -464,7 +478,7 @@ DankModal {
function handleGridNavigation(event, isPressed) { function handleGridNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold(); cancelHold();
event.accepted = true; event.accepted = true;
} }
@@ -539,6 +553,12 @@ DankModal {
event.accepted = true; event.accepted = true;
} }
break; break;
case Qt.Key_B:
if (visibleActions.includes("softreboot")) {
startHold("softreboot", visibleActions.indexOf("softreboot"));
event.accepted = true;
}
break;
case Qt.Key_X: case Qt.Key_X:
if (visibleActions.includes("logout")) { if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout")); startHold("logout", visibleActions.indexOf("logout"));
@@ -597,7 +617,7 @@ DankModal {
readonly property var actionData: root.getActionData(modelData) readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
@@ -627,7 +647,7 @@ DankModal {
color: { color: {
if (gridButtonRect.modelData === "poweroff") if (gridButtonRect.modelData === "poweroff")
return Theme.errorSelected; return Theme.errorSelected;
if (gridButtonRect.modelData === "reboot") if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
return Theme.withAlpha(Theme.warning, 0.3); return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected; return Theme.primarySelected;
} }
@@ -722,7 +742,7 @@ DankModal {
readonly property var actionData: root.getActionData(modelData) readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: parent.width width: parent.width
@@ -752,7 +772,7 @@ DankModal {
color: { color: {
if (listButtonRect.modelData === "poweroff") if (listButtonRect.modelData === "poweroff")
return Theme.errorSelected; return Theme.errorSelected;
if (listButtonRect.modelData === "reboot") if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
return Theme.withAlpha(Theme.warning, 0.3); return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected; return Theme.primarySelected;
} }
@@ -469,6 +469,8 @@ PluginComponent {
} }
MouseArea { MouseArea {
id: peerMouseArea
z: -1 z: -1
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
+10 -1
View File
@@ -201,7 +201,16 @@ Item {
direction: root.effectiveShadowDirection direction: root.effectiveShadowDirection
fallbackOffset: 4 fallbackOffset: 4
targetRadius: root.rt targetRadius: root.rt
targetColor: barWindow._bgColor // wing-side body corners are square where the gothic fillets attach;
// rounding the shadow there leaves a shadow-filled notch under the
// translucent bar fill (#2975)
topLeftRadius: root.gothEnabled && (root.isBottom || root.isRight) ? 0 : root.rt
topRightRadius: root.gothEnabled && (root.isBottom || root.isLeft) ? 0 : root.rt
bottomLeftRadius: root.gothEnabled && (root.isTop || root.isRight) ? 0 : root.rt
bottomRightRadius: root.gothEnabled && (root.isTop || root.isLeft) ? 0 : root.rt
// barShape below is the sole painter of the bar; a fill here doubles
// the alpha of translucent bars while the wings stay single-painted
targetColor: "transparent"
shadowBlurPx: root.shadowBlurPx shadowBlurPx: root.shadowBlurPx
shadowOffsetX: root.shadowOffsetX shadowOffsetX: root.shadowOffsetX
@@ -490,7 +490,7 @@ Item {
} }
StyledText { StyledText {
text: activePlayer?.trackAlbum || "" text: MprisController.stableAlbum
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary color: Theme.surfaceTextSecondary
width: parent.width width: parent.width
+2 -4
View File
@@ -479,8 +479,6 @@ 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) {
@@ -601,7 +599,7 @@ Item {
height: delegateItem.height height: delegateItem.height
actualIconSize: root.iconSize actualIconSize: root.iconSize
dockApps: root dockApps: root
index: delegateItem.index index: model.index
} }
DockTrashButton { DockTrashButton {
@@ -626,7 +624,7 @@ Item {
appData: itemData appData: itemData
contextMenu: root.contextMenu contextMenu: root.contextMenu
dockApps: root dockApps: root
index: delegateItem.index index: model.index
parentDockScreen: root.dockScreen parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: { windowTitle: {
+12 -8
View File
@@ -386,16 +386,20 @@ end)
HYPRLAND_LUA_EOF HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp --suffix=.lua)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
env = DMS_RUN_GREETER,1 hl.env("DMS_RUN_GREETER", "1")
misc { hl.config({
disable_hyprland_logo = true misc = {
} disable_hyprland_logo = true,
},
})
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" hl.on("hyprland.start", function()
HYPRLAND_EOF hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
else else
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
+73 -7
View File
@@ -156,9 +156,10 @@ DankOSD {
if (MprisController.isFirefoxYoutubeHoverPreview(player)) if (MprisController.isFirefoxYoutubeHoverPreview(player))
return; return;
const newTitle = player.trackTitle || ""; const metaPlayer = MprisController.bestMetadataPlayer(player);
const newArtist = player.trackArtist || ""; const newTitle = MprisController.displayTrackTitle(metaPlayer);
const newAlbum = player.trackAlbum || ""; const newArtist = metaPlayer.trackArtist || "";
const newAlbum = metaPlayer.trackAlbum || "";
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum; const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
root._displayTitle = newTitle; root._displayTitle = newTitle;
@@ -263,13 +264,48 @@ DankOSD {
} }
} }
Row {
id: transportControls
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
Rectangle {
width: Theme.iconSize - 4
height: Theme.iconSize - 4
radius: (Theme.iconSize - 4) / 2
anchors.verticalCenter: parent.verticalCenter
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: Theme.iconSize - 10
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: prevButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoPrevious ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.previousOrRewind();
root.hide();
}
}
}
Rectangle { Rectangle {
width: Theme.iconSize width: Theme.iconSize
height: Theme.iconSize height: Theme.iconSize
radius: Theme.iconSize / 2 radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -291,9 +327,39 @@ DankOSD {
} }
} }
Rectangle {
width: Theme.iconSize - 4
height: Theme.iconSize - 4
radius: (Theme.iconSize - 4) / 2
anchors.verticalCenter: parent.verticalCenter
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: Theme.iconSize - 10
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoNext ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.next();
root.hide();
}
}
}
}
Column { Column {
x: parent.gap * 2 + Theme.iconSize x: parent.gap * 2 + transportControls.width
width: parent.width - Theme.iconSize - parent.gap * 3 width: parent.width - transportControls.width - parent.gap * 3
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS spacing: Theme.spacingXXS
+15 -3
View File
@@ -400,8 +400,8 @@ Item {
settingKey: "powerMenuDefaultAction" settingKey: "powerMenuDefaultAction"
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"] tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
text: I18n.tr("Default selected action") text: I18n.tr("Default selected action")
options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate")] options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate"), I18n.tr("Soft Reboot")]
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"] property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate", "softreboot"]
Component.onCompleted: { Component.onCompleted: {
const currentAction = SettingsData.powerMenuDefaultAction || "logout"; const currentAction = SettingsData.powerMenuDefaultAction || "logout";
@@ -465,6 +465,12 @@ Item {
label: I18n.tr("Show Hibernate"), label: I18n.tr("Show Hibernate"),
desc: I18n.tr("Only visible if hibernate is supported by your system"), desc: I18n.tr("Only visible if hibernate is supported by your system"),
hibernate: true hibernate: true
},
{
key: "softreboot",
label: I18n.tr("Show Soft Reboot"),
desc: I18n.tr("Restart userspace without rebooting the kernel, requires systemd"),
softreboot: true
} }
] ]
@@ -474,7 +480,13 @@ Item {
tags: ["power", "menu", "action", "show", modelData.key] tags: ["power", "menu", "action", "show", modelData.key]
text: modelData.label text: modelData.label
description: modelData.desc || "" description: modelData.desc || ""
visible: !modelData.hibernate || SessionService.hibernateSupported visible: {
if (modelData.hibernate)
return SessionService.hibernateSupported;
if (modelData.softreboot)
return SessionService.softRebootSupported;
return true;
}
checked: SettingsData.powerMenuActions.includes(modelData.key) checked: SettingsData.powerMenuActions.includes(modelData.key)
onToggled: checked => { onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]; let actions = [...SettingsData.powerMenuActions];
+17 -21
View File
@@ -749,16 +749,24 @@ Singleton {
if (category === I18n.tr("All")) if (category === I18n.tr("All"))
return visibleApps; return visibleApps;
const pluginItems = getPluginItems(category, "");
if (pluginItems.length > 0)
return pluginItems;
return visibleApps.filter(app => { return visibleApps.filter(app => {
const appCategories = getCategoriesForApp(app); const appCategories = getCategoriesForApp(app);
return appCategories.includes(category); return appCategories.includes(category);
}); });
} }
function getPluginIdForCategory(category) {
if (typeof PluginService === "undefined")
return null;
const launchers = PluginService.getLauncherPlugins();
for (const pluginId in launchers) {
if ((launchers[pluginId].name || pluginId) === category)
return pluginId;
}
return null;
}
// Plugin launcher support functions // Plugin launcher support functions
function getPluginCategories() { function getPluginCategories() {
if (typeof PluginService === "undefined") { if (typeof PluginService === "undefined") {
@@ -778,17 +786,11 @@ Singleton {
} }
function getPluginCategoryIcon(category) { function getPluginCategoryIcon(category) {
if (typeof PluginService === "undefined") const pluginId = getPluginIdForCategory(category);
if (!pluginId)
return null; return null;
const launchers = PluginService.getLauncherPlugins(); return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
if ((plugin.name || pluginId) === category) {
return plugin.icon || "extension";
}
}
return null;
} }
function getAllPluginItems() { function getAllPluginItems() {
@@ -809,18 +811,12 @@ Singleton {
} }
function getPluginItems(category, query) { function getPluginItems(category, query) {
if (typeof PluginService === "undefined") const pluginId = getPluginIdForCategory(category);
if (!pluginId)
return []; return [];
const launchers = PluginService.getLauncherPlugins();
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
if ((plugin.name || pluginId) === category) {
return getPluginItemsForPlugin(pluginId, query); return getPluginItemsForPlugin(pluginId, query);
} }
}
return [];
}
function getPluginItemsForPlugin(pluginId, query) { function getPluginItemsForPlugin(pluginId, query) {
if (typeof PluginService === "undefined") { if (typeof PluginService === "undefined") {
+8 -1
View File
@@ -23,6 +23,13 @@ Singleton {
} }
} }
Connections {
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
function onHasPerformanceProfileChanged() {
root.applyPowerProfile();
}
}
function applyPowerProfile() { function applyPowerProfile() {
if (!batteryAvailable) if (!batteryAvailable)
return; return;
@@ -32,7 +39,7 @@ Singleton {
const targetProfile = parseInt(profileValue); const targetProfile = parseInt(profileValue);
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile) if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
return; return;
PowerProfiles.profile = targetProfile; PowerProfileWatcher.applyProfile(targetProfile);
} }
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
+24 -47
View File
@@ -11,7 +11,7 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("DMSService") readonly property var log: Log.scoped("DMSService")
property bool dmsAvailable: false readonly property bool dmsAvailable: isConnected
property var capabilities: [] property var capabilities: []
property int apiVersion: 0 property int apiVersion: 0
property string cliVersion: "" property string cliVersion: ""
@@ -21,7 +21,7 @@ Singleton {
property var availableThemes: [] property var availableThemes: []
property var installedThemes: [] property var installedThemes: []
property bool isConnected: false property bool isConnected: false
property bool isConnecting: false readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
property bool subscribeConnected: false property bool subscribeConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET") 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"] 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)
return;
detectUpdateCommand(); detectUpdateCommand();
} requestSocket.connected = true;
} }
function detectUpdateCommand() { function detectUpdateCommand() {
@@ -82,12 +83,6 @@ Singleton {
checkAurHelper.running = true; checkAurHelper.running = true;
} }
function startSocketConnection() {
if (socketPath && socketPath.length > 0) {
testProcess.running = true;
}
}
Process { Process {
id: checkAurHelper id: checkAurHelper
command: ["sh", "-c", "command -v paru || command -v yay"] command: ["sh", "-c", "command -v paru || command -v yay"]
@@ -105,7 +100,6 @@ Singleton {
} else { } else {
updateCommand = "dms update"; updateCommand = "dms update";
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
} }
@@ -114,7 +108,6 @@ Singleton {
if (exitCode !== 0) { if (exitCode !== 0) {
updateCommand = "dms update"; updateCommand = "dms update";
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
} }
@@ -135,7 +128,6 @@ Singleton {
updateCommand = "dms update"; updateCommand = "dms update";
} }
checkingUpdateCommand = false; checkingUpdateCommand = false;
startSocketConnection();
} }
} }
@@ -143,53 +135,28 @@ Singleton {
if (exitCode !== 0) { if (exitCode !== 0) {
updateCommand = "dms update"; updateCommand = "dms update";
checkingUpdateCommand = false; 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 { DankSocket {
id: requestSocket id: requestSocket
path: root.socketPath path: root.socketPath
connected: false connected: false
onConnectionStateChanged: { onConnectionStateChanged: {
if (connected) { if (linkUp) {
root.isConnected = true; root.isConnected = true;
root.isConnecting = false;
root.connectionStateChanged(); root.connectionStateChanged();
subscribeSocket.connected = true; subscribeSocket.connected = true;
} else { return;
}
root.isConnected = false; root.isConnected = false;
root.isConnecting = false;
root.apiVersion = 0; root.apiVersion = 0;
root.capabilities = []; root.capabilities = [];
root.failPendingRequests();
root.connectionStateChanged(); root.connectionStateChanged();
} }
}
parser: SplitParser { parser: SplitParser {
onRead: line => { onRead: line => {
@@ -219,11 +186,11 @@ Singleton {
connected: false connected: false
onConnectionStateChanged: { onConnectionStateChanged: {
root.subscribeConnected = connected; root.subscribeConnected = linkUp;
if (connected) { if (!linkUp)
return;
sendSubscribeRequest(); sendSubscribeRequest();
} }
}
parser: SplitParser { parser: SplitParser {
onRead: line => { onRead: line => {
@@ -446,11 +413,21 @@ Singleton {
function handleResponse(response) { function handleResponse(response) {
const callback = pendingRequests[response.id]; const callback = pendingRequests[response.id];
if (!callback)
if (callback) { return;
delete pendingRequests[response.id]; delete pendingRequests[response.id];
callback(response); callback(response);
} }
function failPendingRequests() {
const pending = pendingRequests;
pendingRequests = {};
clipboardRequestIds = {};
for (const id in pending) {
pending[id]({
"error": "not connected to DMS socket"
});
}
} }
function ping(callback) { function ping(callback) {
+138 -8
View File
@@ -47,6 +47,7 @@ Singleton {
// Chromium can report blank metadata between tracks // Chromium can report blank metadata between tracks
property string stableTitle: "" property string stableTitle: ""
property string stableArtist: "" property string stableArtist: ""
property string stableAlbum: ""
Connections { Connections {
target: root.activePlayer target: root.activePlayer
@@ -59,6 +60,9 @@ Singleton {
root._syncStableMeta(); root._syncStableMeta();
root._checkIdle(); root._checkIdle();
} }
function onTrackAlbumChanged() {
root._syncStableMeta();
}
function onLengthChanged() { function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) { if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length; root.activePlayerStableLength = root.activePlayer.length;
@@ -72,8 +76,10 @@ Singleton {
onActivePlayerChanged: { onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0; activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
stableTitle = activePlayer?.trackTitle || ""; stableTitle = "";
stableArtist = activePlayer?.trackArtist || ""; stableArtist = "";
stableAlbum = "";
_syncStableMeta();
_checkIdle(); _checkIdle();
} }
@@ -82,14 +88,24 @@ Singleton {
if (!p) { if (!p) {
stableTitle = ""; stableTitle = "";
stableArtist = ""; stableArtist = "";
stableAlbum = "";
return; return;
} }
if (isFirefoxYoutubeHoverPreview(p)) if (isFirefoxYoutubeHoverPreview(p))
return; return;
if (p.trackTitle) const metadataPlayer = bestMetadataPlayer(p);
stableTitle = p.trackTitle; const nextTitle = displayTrackTitle(metadataPlayer);
if (p.trackArtist) const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
stableArtist = p.trackArtist; if (trackChanged) {
stableArtist = "";
stableAlbum = "";
}
if (nextTitle)
stableTitle = nextTitle;
if (metadataPlayer.trackArtist)
stableArtist = metadataPlayer.trackArtist;
if (metadataPlayer.trackAlbum)
stableAlbum = metadataPlayer.trackAlbum;
} }
// Chromium reports stopped media w/blank metadata, resolve by checking idle status // Chromium reports stopped media w/blank metadata, resolve by checking idle status
@@ -101,6 +117,7 @@ Singleton {
return; return;
root.stableTitle = ""; root.stableTitle = "";
root.stableArtist = ""; root.stableArtist = "";
root.stableAlbum = "";
root._resolveActivePlayer(); root._resolveActivePlayer();
} }
} }
@@ -122,9 +139,30 @@ Singleton {
delegate: Connections { delegate: Connections {
required property MprisPlayer modelData required property MprisPlayer modelData
target: modelData target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() { function onIsPlayingChanged() {
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackTitleChanged() {
if (modelData.isPlaying) if (modelData.isPlaying)
root._resolveActivePlayer(); root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackArtistChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackAlbumChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onMetadataChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
} }
} }
} }
@@ -133,9 +171,101 @@ Singleton {
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist; return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
} }
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
function _stripAppTitleSuffix(title: string): string {
const idx = title.lastIndexOf(" | ");
if (idx <= 0)
return title;
const suffix = title.substring(idx + 3).trim().toLowerCase();
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
}
function normalizedTrackTitle(player: MprisPlayer): string {
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
}
function displayTrackTitle(player: MprisPlayer): string {
return (player?.trackTitle || "").trim();
}
function normalizedTrackArtist(player: MprisPlayer): string {
return (player?.trackArtist || "").trim().toLowerCase();
}
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
const firstTitle = normalizedTrackTitle(first);
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
return false;
const firstArtist = normalizedTrackArtist(first);
const secondArtist = normalizedTrackArtist(second);
if (firstArtist && secondArtist)
return firstArtist === secondArtist;
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
if (firstUrl && secondUrl)
return firstUrl === secondUrl;
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
if (firstAlbum && secondAlbum)
return firstAlbum === secondAlbum;
return true;
}
function metadataQuality(player: MprisPlayer): int {
if (!player)
return -1;
let quality = player.trackArtist ? 100 : 0;
quality += player.trackTitle ? 40 : 0;
quality += player.trackAlbum ? 20 : 0;
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
quality += player.metadata?.["xesam:url"] ? 5 : 0;
return quality;
}
function equivalentPlayers(player: MprisPlayer): var {
if (!player)
return [];
return availablePlayers.filter(candidate => {
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
});
}
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
const equivalents = equivalentPlayers(player);
if (equivalents.length === 0)
return player;
return equivalents.reduce((best, candidate) => {
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
}, player);
}
function _bestPlayingPlayer(): MprisPlayer {
const playing = availablePlayers.filter(player => player.isPlaying);
if (playing.length === 0)
return null;
const controllable = playing.filter(player => player.canControl);
if (activePlayer?.isPlaying) {
if (activePlayer.canControl || controllable.length === 0)
return activePlayer;
// Playing but not controllable: only a same-track controllable peer may take over
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
return mirror || activePlayer;
}
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
if (onlyEquivalentMirrors)
return null;
}
return controllable[0] || playing[0];
}
function _resolveActivePlayer(): void { function _resolveActivePlayer(): void {
// A playing player always wins; otherwise keep the selection stable w/idle const playing = _bestPlayingPlayer();
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) { if (playing) {
if (activePlayer !== playing) { if (activePlayer !== playing) {
activePlayer = playing; activePlayer = playing;
+64 -2
View File
@@ -17,6 +17,7 @@ Singleton {
property bool loginctlCommandAvailable: false property bool loginctlCommandAvailable: false
property bool systemctlCommandAvailable: false property bool systemctlCommandAvailable: false
property bool hibernateSupported: false property bool hibernateSupported: false
readonly property bool softRebootSupported: systemctlCommandAvailable
property bool inhibitorAvailable: true property bool inhibitorAvailable: true
property bool idleInhibited: false property bool idleInhibited: false
property string inhibitReason: "Keep system awake" property string inhibitReason: "Keep system awake"
@@ -217,6 +218,64 @@ Singleton {
return envObj; 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) { function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command) if (!desktopEntry || !desktopEntry.command)
return; return;
@@ -230,8 +289,7 @@ Singleton {
cmd = [nvidiaCommand].concat(cmd); cmd = [nvidiaCommand].concat(cmd);
if (override?.extraFlags) { if (override?.extraFlags) {
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0); cmd = cmd.concat(splitShellArgs(override.extraFlags));
cmd = cmd.concat(extraArgs);
} }
const userPrefix = SettingsData.launchPrefix?.trim() || ""; const userPrefix = SettingsData.launchPrefix?.trim() || "";
@@ -395,6 +453,10 @@ Singleton {
} }
} }
function softReboot() {
Quickshell.execDetached(["systemctl", "soft-reboot"]);
}
function poweroff() { function poweroff() {
if (SettingsData.customPowerActionPowerOff.length === 0) { if (SettingsData.customPowerActionPowerOff.length === 0) {
Quickshell.execDetached(powerManagerCommand("poweroff")); Quickshell.execDetached(powerManagerCommand("poweroff"));
+29 -4
View File
@@ -26,7 +26,7 @@ Singleton {
return hash.toString(16).padStart(8, '0'); return hash.toString(16).padStart(8, '0');
} }
function getArtworkUrl(player) { function _directArtworkUrl(player) {
if (!player) return ""; if (!player) return "";
let artUrl = player.trackArtUrl || ""; let artUrl = player.trackArtUrl || "";
@@ -54,6 +54,17 @@ Singleton {
return ""; return "";
} }
function getArtworkUrl(player) {
const directUrl = _directArtworkUrl(player);
if (directUrl !== "")
return directUrl;
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
return candidate !== player && _directArtworkUrl(candidate) !== "";
});
return _directArtworkUrl(equivalent);
}
function _commit(u, artKey, srcUrl) { function _commit(u, artKey, srcUrl) {
resolvedArtUrl = u; resolvedArtUrl = u;
_committedArtKey = u !== "" ? artKey : ""; _committedArtKey = u !== "" ? artKey : "";
@@ -171,12 +182,26 @@ Singleton {
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
Connections { Connections {
target: root.activePlayer target: MprisController
function onAvailablePlayersChanged() {
root._updateArtUrl();
}
}
Instantiator {
model: MprisController.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
ignoreUnknownSignals: true ignoreUnknownSignals: true
function onIsPlayingChanged() { root._updateArtUrl(); }
function onTrackTitleChanged() { root._updateArtUrl(); } function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtistChanged() { root._updateArtUrl(); }
function onTrackAlbumChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); } function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); } function onMetadataChanged() { root._updateArtUrl(); }
} }
}
function _trackKey() { function _trackKey() {
const p = activePlayer; const p = activePlayer;
@@ -201,8 +226,8 @@ Singleton {
} }
_pendingArtKey = key; _pendingArtKey = key;
const url = getArtworkUrl(activePlayer); const url = getArtworkUrl(activePlayer);
// Ignore Chrome's same-track thumbnail size updates. // Ignore duplicate notifications, but let a richer peer replace same-track art
if (key !== "" && key === _committedArtKey) if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
return; return;
if (key !== "" && url !== "" && url === _committedSrcUrl) { if (key !== "" && url !== "" && url === _committedSrcUrl) {
// Chrome can publish track metadata before its new artwork URL. // Chrome can publish track metadata before its new artwork URL.