mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-06 21:48:30 -04:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0af52d5b | |||
| e1b6427929 | |||
| 47bf6a8dfd | |||
| 84ac36295f | |||
| ca839487de | |||
| dabbec1dce | |||
| bd4d35d215 | |||
| cfdacd3ea3 | |||
| 2e558f2489 | |||
| 8535e3a1d7 | |||
| c2a7f767c2 | |||
| c9c11f8a27 | |||
| 8cffdab4d7 | |||
| 0e8a4c0cc1 | |||
| 1db6381c78 | |||
| d5c7efc861 | |||
| 943ffb432c | |||
| 7063448b80 | |||
| 52afbc9801 | |||
| 6edb985847 | |||
| 41efe6ad15 | |||
| 4fb6a17e1b | |||
| e771e3b675 | |||
| b1b7aa7aa2 | |||
| c128793239 | |||
| 069ddab041 | |||
| 45cf6ecefd | |||
| 7058b00091 | |||
| 6ba4b79039 | |||
| b8e2ce1da8 | |||
| c58d55db7d |
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
|
|||||||
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
|
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
|
||||||
m.promptBroker = broker
|
m.promptBroker = broker
|
||||||
|
|
||||||
adapter, err := m.findAdapter()
|
adapter, err := findAdapter(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, fmt.Errorf("no bluetooth adapter found: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
m.adapterPath = adapter
|
m.adapterPath = adapter
|
||||||
|
|
||||||
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
|
func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
|
||||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
|
obj := conn.Object(bluezService, dbus.ObjectPath("/"))
|
||||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||||
|
|
||||||
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
|
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("%w: %v", ErrNoAdapter, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for path, interfaces := range objects {
|
for path, interfaces := range objects {
|
||||||
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("no adapter found")
|
return "", ErrNoAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) initialize() error {
|
func (m *Manager) initialize() error {
|
||||||
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) SetPowered(powered bool) error {
|
func (m *Manager) SetPowered(powered bool) error {
|
||||||
|
if powered {
|
||||||
|
if err := rfkillUnblockBluetooth(); err != nil {
|
||||||
|
log.Debugf("[BluezManager] rfkill unblock failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||||
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
|
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package bluez
|
||||||
|
|
||||||
|
func rfkillUnblockBluetooth() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package bluez
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; },
|
||||||
|
// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3
|
||||||
|
func rfkillUnblockBluetooth() error {
|
||||||
|
f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var event [8]byte
|
||||||
|
event[4] = 2
|
||||||
|
event[5] = 3
|
||||||
|
_, err = f.Write(event[:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -229,6 +229,9 @@ func (m *Manager) snapshotState() CUPSState {
|
|||||||
func (m *Manager) Subscribe(id string) chan CUPSState {
|
func (m *Manager) Subscribe(id string) chan CUPSState {
|
||||||
ch := make(chan CUPSState, 64)
|
ch := make(chan CUPSState, 64)
|
||||||
|
|
||||||
|
m.subLifecycleMu.Lock()
|
||||||
|
defer m.subLifecycleMu.Unlock()
|
||||||
|
|
||||||
wasEmpty := true
|
wasEmpty := true
|
||||||
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
||||||
wasEmpty = false
|
wasEmpty = false
|
||||||
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
|
|||||||
|
|
||||||
m.subscribers.Store(id, ch)
|
m.subscribers.Store(id, ch)
|
||||||
|
|
||||||
if wasEmpty && m.subscription != nil {
|
if !wasEmpty || m.subscription == nil {
|
||||||
if err := m.subscription.Start(); err != nil {
|
return ch
|
||||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
|
||||||
} else {
|
|
||||||
m.eventWG.Add(1)
|
|
||||||
go m.eventHandler()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := m.subscription.Start(); err != nil {
|
||||||
|
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
m.eventWG.Add(1)
|
||||||
|
go m.eventHandler()
|
||||||
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Unsubscribe(id string) {
|
func (m *Manager) Unsubscribe(id string) {
|
||||||
|
m.subLifecycleMu.Lock()
|
||||||
|
defer m.subLifecycleMu.Unlock()
|
||||||
|
|
||||||
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||||
close(val)
|
close(val)
|
||||||
}
|
}
|
||||||
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
if isEmpty && m.subscription != nil {
|
if !isEmpty || m.subscription == nil {
|
||||||
m.subscription.Stop()
|
return
|
||||||
m.eventWG.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.subscription.Stop()
|
||||||
|
m.eventWG.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Close() {
|
func (m *Manager) Close() {
|
||||||
close(m.stopChan)
|
close(m.stopChan)
|
||||||
|
|
||||||
|
m.subLifecycleMu.Lock()
|
||||||
if m.subscription != nil {
|
if m.subscription != nil {
|
||||||
m.subscription.Stop()
|
m.subscription.Stop()
|
||||||
}
|
}
|
||||||
|
m.subLifecycleMu.Unlock()
|
||||||
|
|
||||||
m.eventWG.Wait()
|
m.eventWG.Wait()
|
||||||
m.notifierWg.Wait()
|
m.notifierWg.Wait()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package cups
|
package cups
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
|
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
|
||||||
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
|
|||||||
assert.Equal(t, 0, count)
|
assert.Equal(t, 0, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
|
||||||
|
// unsynchronized so overlapping Start/Stop trips the race detector
|
||||||
|
type stubSubscription struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
events chan SubscriptionEvent
|
||||||
|
conn *int
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubSubscription) Start() error {
|
||||||
|
if s.running {
|
||||||
|
return errors.New("already running")
|
||||||
|
}
|
||||||
|
s.running = true
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.events = make(chan SubscriptionEvent)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
v := 0
|
||||||
|
s.conn = &v
|
||||||
|
*s.conn++
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubSubscription) Stop() {
|
||||||
|
if !s.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.running = false
|
||||||
|
s.conn = nil
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
close(s.events)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.events
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
|
||||||
|
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
|
||||||
|
m.subscription = &stubSubscription{}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := range 8 {
|
||||||
|
wg.Go(func() {
|
||||||
|
id := fmt.Sprintf("client-%d", i)
|
||||||
|
for range 50 {
|
||||||
|
m.Subscribe(id)
|
||||||
|
m.Unsubscribe(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func TestManager_Close(t *testing.T) {
|
func TestManager_Close(t *testing.T) {
|
||||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ type Manager struct {
|
|||||||
client CUPSClientInterface
|
client CUPSClientInterface
|
||||||
pkHelper PkHelper
|
pkHelper PkHelper
|
||||||
subscription SubscriptionManagerInterface
|
subscription SubscriptionManagerInterface
|
||||||
|
subLifecycleMu sync.Mutex
|
||||||
stateMutex sync.RWMutex
|
stateMutex sync.RWMutex
|
||||||
subscribers syncmap.Map[string, chan CUPSState]
|
subscribers syncmap.Map[string, chan CUPSState]
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
|
|||||||
@@ -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
|
}
|
||||||
|
|
||||||
|
ledStates, err := device.State(evLedType)
|
||||||
|
if err != nil || len(ledStates) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return ledStates[ledCapslockKey], true
|
||||||
}
|
}
|
||||||
|
|
||||||
return ledStates[ledCapslockKey]
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1731,10 +1731,21 @@ func Start(printDocs bool) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := InitializeBluezManager(); err != nil {
|
for {
|
||||||
|
err := InitializeBluezManager()
|
||||||
|
if err == nil {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Warnf("Bluez manager unavailable: %v", err)
|
log.Warnf("Bluez manager unavailable: %v", err)
|
||||||
} else {
|
if !errors.Is(err, bluez.ErrNoAdapter) {
|
||||||
notifyCapabilityChange()
|
return
|
||||||
|
}
|
||||||
|
if err := bluez.WaitForAdapter(); err != nil {
|
||||||
|
log.Warnf("Bluetooth adapter watch failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("Bluetooth adapter appeared, initializing bluez manager")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ func wrapInTerminal(term, title, shellCmd string, extraArgs []string) []string {
|
|||||||
case "konsole":
|
case "konsole":
|
||||||
argv = []string{term, "-p", "tabtitle=" + title}
|
argv = []string{term, "-p", "tabtitle=" + title}
|
||||||
case "gnome-terminal":
|
case "gnome-terminal":
|
||||||
argv = []string{term, "--title=" + title}
|
// --wait: the factory process otherwise returns immediately
|
||||||
|
argv = []string{term, "--wait", "--title=" + title}
|
||||||
execFlag = "--"
|
execFlag = "--"
|
||||||
default:
|
default:
|
||||||
argv = []string{term}
|
argv = []string{term}
|
||||||
|
|||||||
@@ -412,25 +412,30 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, opts UpgradeOptions) {
|
|||||||
onLine := func(line string) { m.appendLog(line) }
|
onLine := func(line string) { m.appendLog(line) }
|
||||||
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
|
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
|
||||||
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
|
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
|
||||||
code := ErrCodeBackendFailed
|
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
||||||
code = ErrCodeTimeout
|
m.failCustomUpgrade(ErrCodeTimeout, err)
|
||||||
|
return
|
||||||
case errors.Is(ctx.Err(), context.Canceled):
|
case errors.Is(ctx.Err(), context.Canceled):
|
||||||
code = ErrCodeCancelled
|
m.failCustomUpgrade(ErrCodeCancelled, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
m.mu.Lock()
|
// exit status reflects the trailing `read`, not the update command
|
||||||
m.state.Phase = PhaseError
|
m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
|
||||||
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
|
|
||||||
m.mu.Unlock()
|
|
||||||
m.markDirty()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.finishSuccessfulUpgrade(false)
|
m.finishSuccessfulUpgrade(false)
|
||||||
m.runRefresh(context.Background(), false)
|
m.runRefresh(context.Background(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) failCustomUpgrade(code ErrorCode, err error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.state.Phase = PhaseError
|
||||||
|
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
|
||||||
|
m.mu.Unlock()
|
||||||
|
m.markDirty()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
||||||
m.appendLog("Upgrade complete.")
|
m.appendLog("Upgrade complete.")
|
||||||
|
|
||||||
|
|||||||
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
|
|||||||
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
|
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWrapInTerminal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
term string
|
||||||
|
wantPrefix []string
|
||||||
|
}{
|
||||||
|
{"kitty", []string{"kitty", "--class", "com.danklinux.dms", "-T", "Title"}},
|
||||||
|
{"gnome-terminal", []string{"gnome-terminal", "--wait", "--title=Title"}},
|
||||||
|
{"foot", []string{"foot", "--app-id=com.danklinux.dms", "--title=Title"}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := wrapInTerminal(tt.term, "Title", "echo hi", nil)
|
||||||
|
if len(got) < len(tt.wantPrefix) || !reflect.DeepEqual(got[:len(tt.wantPrefix)], tt.wantPrefix) {
|
||||||
|
t.Errorf("wrapInTerminal(%q) = %#v, want prefix %#v", tt.term, got, tt.wantPrefix)
|
||||||
|
}
|
||||||
|
tail := got[len(got)-3:]
|
||||||
|
if tail[0] != "sh" || tail[1] != "-c" {
|
||||||
|
t.Errorf("wrapInTerminal(%q) tail = %#v, want [sh -c <cmd>]", tt.term, tail)
|
||||||
|
}
|
||||||
|
if !strings.Contains(tail[2], "echo hi") {
|
||||||
|
t.Errorf("wrapInTerminal(%q) command %q does not contain shell command", tt.term, tail[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -760,6 +760,9 @@ func (m *Manager) schedulerLoop() {
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
m.recalcSchedule(now)
|
m.recalcSchedule(now)
|
||||||
|
// publish independent of output readiness so night status never
|
||||||
|
// presents a stale schedule while applies are blocked (#2967)
|
||||||
|
m.updateStateFromSchedule()
|
||||||
|
|
||||||
var waitDur time.Duration
|
var waitDur time.Duration
|
||||||
if enabled {
|
if enabled {
|
||||||
@@ -1120,13 +1123,15 @@ func (m *Manager) SetTemperature(low, high int) error {
|
|||||||
m.configMutex.Unlock()
|
m.configMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.config.LowTemp = low
|
updated := m.config
|
||||||
m.config.HighTemp = high
|
updated.LowTemp = low
|
||||||
err := m.config.Validate()
|
updated.HighTemp = high
|
||||||
m.configMutex.Unlock()
|
if err := updated.Validate(); err != nil {
|
||||||
if err != nil {
|
m.configMutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
m.config = updated
|
||||||
|
m.configMutex.Unlock()
|
||||||
m.triggerUpdate()
|
m.triggerUpdate()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1138,14 +1143,16 @@ func (m *Manager) SetLocation(lat, lon float64) error {
|
|||||||
m.configMutex.Unlock()
|
m.configMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.config.Latitude = &lat
|
updated := m.config
|
||||||
m.config.Longitude = &lon
|
updated.Latitude = &lat
|
||||||
m.config.UseIPLocation = false
|
updated.Longitude = &lon
|
||||||
err := m.config.Validate()
|
updated.UseIPLocation = false
|
||||||
m.configMutex.Unlock()
|
if err := updated.Validate(); err != nil {
|
||||||
if err != nil {
|
m.configMutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
m.config = updated
|
||||||
|
m.configMutex.Unlock()
|
||||||
m.triggerUpdate()
|
m.triggerUpdate()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1180,13 +1187,15 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
|
|||||||
m.configMutex.Unlock()
|
m.configMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.config.ManualSunrise = &sunrise
|
updated := m.config
|
||||||
m.config.ManualSunset = &sunset
|
updated.ManualSunrise = &sunrise
|
||||||
err := m.config.Validate()
|
updated.ManualSunset = &sunset
|
||||||
m.configMutex.Unlock()
|
if err := updated.Validate(); err != nil {
|
||||||
if err != nil {
|
m.configMutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
m.config = updated
|
||||||
|
m.configMutex.Unlock()
|
||||||
m.triggerUpdate()
|
m.triggerUpdate()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1209,12 +1218,14 @@ func (m *Manager) SetGamma(gamma float64) error {
|
|||||||
m.configMutex.Unlock()
|
m.configMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.config.Gamma = gamma
|
updated := m.config
|
||||||
err := m.config.Validate()
|
updated.Gamma = gamma
|
||||||
m.configMutex.Unlock()
|
if err := updated.Validate(); err != nil {
|
||||||
if err != nil {
|
m.configMutex.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
m.config = updated
|
||||||
|
m.configMutex.Unlock()
|
||||||
m.triggerUpdate()
|
m.triggerUpdate()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
|
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
|
||||||
@@ -412,3 +413,75 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
|||||||
_, err := NewManager(mockDisplay, config)
|
_, err := NewManager(mockDisplay, config)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetters_RejectedValuesLeaveConfigUntouched(t *testing.T) {
|
||||||
|
newManager := func() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
config: DefaultConfig(),
|
||||||
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("SetTemperature", func(t *testing.T) {
|
||||||
|
m := newManager()
|
||||||
|
before := m.config
|
||||||
|
|
||||||
|
err := m.SetTemperature(3200, 2500)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, before, m.config)
|
||||||
|
assert.Empty(t, m.updateTrigger)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SetLocation", func(t *testing.T) {
|
||||||
|
m := newManager()
|
||||||
|
before := m.config
|
||||||
|
|
||||||
|
err := m.SetLocation(120.0, 10.0)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, before, m.config)
|
||||||
|
assert.Empty(t, m.updateTrigger)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SetGamma", func(t *testing.T) {
|
||||||
|
m := newManager()
|
||||||
|
before := m.config
|
||||||
|
|
||||||
|
err := m.SetGamma(-1.0)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, before, m.config)
|
||||||
|
assert.Empty(t, m.updateTrigger)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetters_ValidValuesCommitAndTrigger(t *testing.T) {
|
||||||
|
m := &Manager{
|
||||||
|
config: DefaultConfig(),
|
||||||
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.SetTemperature(3000, 6000)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 3000, m.config.LowTemp)
|
||||||
|
assert.Equal(t, 6000, m.config.HighTemp)
|
||||||
|
assert.Len(t, m.updateTrigger, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyGamma_SkipsUnchangedTempAndGamma(t *testing.T) {
|
||||||
|
m := &Manager{config: DefaultConfig()}
|
||||||
|
m.controlsInitialized = true
|
||||||
|
|
||||||
|
out := &outputState{
|
||||||
|
id: 1,
|
||||||
|
rampSize: 256,
|
||||||
|
gammaControl: &wlr_gamma_control.ZwlrGammaControlV1{},
|
||||||
|
lastTemp: 5000,
|
||||||
|
lastGamma: m.config.Gamma,
|
||||||
|
}
|
||||||
|
m.outputs.Store(out.id, out)
|
||||||
|
|
||||||
|
m.applyGamma(5000)
|
||||||
|
|
||||||
|
assert.False(t, out.failed, "unchanged temp must not reach the compositor write path")
|
||||||
|
assert.Equal(t, 5000, out.lastTemp)
|
||||||
|
assert.Equal(t, uint32(256), out.rampSize)
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ Singleton {
|
|||||||
function openModal(modal) {
|
function openModal(modal) {
|
||||||
PopoutManager.screenshotActive = false;
|
PopoutManager.screenshotActive = false;
|
||||||
const screenName = modal.effectiveScreen?.name ?? "unknown";
|
const screenName = modal.effectiveScreen?.name ?? "unknown";
|
||||||
currentModalsByScreen[screenName] = modal;
|
var next = {};
|
||||||
|
for (var k in currentModalsByScreen)
|
||||||
|
next[k] = currentModalsByScreen[k];
|
||||||
|
next[screenName] = modal;
|
||||||
|
currentModalsByScreen = next;
|
||||||
modalChanged();
|
modalChanged();
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (!modal.allowStacking)
|
if (!modal.allowStacking)
|
||||||
@@ -34,7 +38,12 @@ Singleton {
|
|||||||
function closeModal(modal) {
|
function closeModal(modal) {
|
||||||
const screenName = modal.effectiveScreen?.name ?? "unknown";
|
const screenName = modal.effectiveScreen?.name ?? "unknown";
|
||||||
if (currentModalsByScreen[screenName] === modal) {
|
if (currentModalsByScreen[screenName] === modal) {
|
||||||
delete currentModalsByScreen[screenName];
|
var next = {};
|
||||||
|
for (var k in currentModalsByScreen) {
|
||||||
|
if (k !== screenName)
|
||||||
|
next[k] = currentModalsByScreen[k];
|
||||||
|
}
|
||||||
|
currentModalsByScreen = next;
|
||||||
modalChanged();
|
modalChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ Singleton {
|
|||||||
activeTrayMenus = newMenus
|
activeTrayMenus = newMenus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeHoverMenus() {
|
||||||
|
for (const screenName in activeTrayMenus) {
|
||||||
|
const menu = activeTrayMenus[screenName]
|
||||||
|
if (!menu || menu.openedByHover !== true) continue
|
||||||
|
if (typeof menu.close === "function") {
|
||||||
|
menu.close()
|
||||||
|
} else if (menu.showMenu !== undefined) {
|
||||||
|
menu.showMenu = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeAllMenus() {
|
function closeAllMenus() {
|
||||||
for (const screenName in activeTrayMenus) {
|
for (const screenName in activeTrayMenus) {
|
||||||
const menu = activeTrayMenus[screenName]
|
const menu = activeTrayMenus[screenName]
|
||||||
|
|||||||
@@ -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,16 +819,24 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCategoryFiltered) {
|
if (isCategoryFiltered) {
|
||||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
|
||||||
for (var i = 0; i < rawApps.length; i++) {
|
if (categoryPluginId) {
|
||||||
allItems.push(getOrTransformApp(rawApps[i]));
|
var pluginCategoryItems = getPluginItems(categoryPluginId, "");
|
||||||
}
|
for (var i = 0; i < pluginCategoryItems.length; i++) {
|
||||||
// Also include core apps (DMS Settings etc.) that match this category
|
allItems.push(pluginCategoryItems[i]);
|
||||||
var allCoreApps = AppSearchService.getCoreApps("");
|
}
|
||||||
for (var i = 0; i < allCoreApps.length; i++) {
|
} else {
|
||||||
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
|
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||||
if (coreAppCats.indexOf(appCategory) !== -1)
|
for (var i = 0; i < rawApps.length; i++) {
|
||||||
allItems.push(transformCoreApp(allCoreApps[i]));
|
allItems.push(getOrTransformApp(rawApps[i]));
|
||||||
|
}
|
||||||
|
// Also include core apps (DMS Settings etc.) that match this category
|
||||||
|
var allCoreApps = AppSearchService.getCoreApps("");
|
||||||
|
for (var i = 0; i < allCoreApps.length; i++) {
|
||||||
|
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
|
||||||
|
if (coreAppCats.indexOf(appCategory) !== -1)
|
||||||
|
allItems.push(transformCoreApp(allCoreApps[i]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var apps = searchApps(searchQuery);
|
var apps = searchApps(searchQuery);
|
||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Item {
|
|||||||
|
|
||||||
property string source: ""
|
property string source: ""
|
||||||
property int glyphSize: 14
|
property int glyphSize: 14
|
||||||
|
property bool badgeVisible: true
|
||||||
|
|
||||||
readonly property var sourceAsset: ({
|
readonly property var sourceAsset: ({
|
||||||
"flatpak": "../../assets/package-sources/flatpak.svg",
|
"flatpak": "../../assets/package-sources/flatpak.svg",
|
||||||
@@ -17,7 +18,7 @@ Item {
|
|||||||
|
|
||||||
readonly property string assetPath: sourceAsset[source] || ""
|
readonly property string assetPath: sourceAsset[source] || ""
|
||||||
|
|
||||||
visible: SettingsData.dankLauncherV2ShowSourceBadges && assetPath.length > 0
|
visible: badgeVisible && SettingsData.dankLauncherV2ShowSourceBadges && assetPath.length > 0
|
||||||
implicitWidth: glyphSize
|
implicitWidth: glyphSize
|
||||||
implicitHeight: glyphSize
|
implicitHeight: glyphSize
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ Rectangle {
|
|||||||
anchors.margins: Theme.spacingXS
|
anchors.margins: Theme.spacingXS
|
||||||
source: root.item?.type === "app" ? (root.item.source || "") : ""
|
source: root.item?.type === "app" ? (root.item.source || "") : ""
|
||||||
glyphSize: 16
|
glyphSize: 16
|
||||||
visible: !root.isSelected && !!source
|
badgeVisible: !root.isSelected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ Item {
|
|||||||
_closeHoverNotepad();
|
_closeHoverNotepad();
|
||||||
activeHoverTrigger = "";
|
activeHoverTrigger = "";
|
||||||
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
|
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
|
||||||
TrayMenuManager.closeAllMenus();
|
TrayMenuManager.closeHoverMenus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _beginSupersededCloseForActive() {
|
function _beginSupersededCloseForActive() {
|
||||||
|
|||||||
@@ -1484,6 +1484,7 @@ BasePill {
|
|||||||
property bool isVertical: false
|
property bool isVertical: false
|
||||||
property var axis: null
|
property var axis: null
|
||||||
property bool showMenu: false
|
property bool showMenu: false
|
||||||
|
property bool openedByHover: false
|
||||||
property var menuHandle: null
|
property var menuHandle: null
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
@@ -1493,7 +1494,8 @@ BasePill {
|
|||||||
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
|
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
|
||||||
|
openedByHover = byHover === true;
|
||||||
trayItem = item;
|
trayItem = item;
|
||||||
anchorItem = anchor;
|
anchorItem = anchor;
|
||||||
parentScreen = screen;
|
parentScreen = screen;
|
||||||
@@ -2084,7 +2086,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
|
||||||
if (!screen)
|
if (!screen)
|
||||||
return;
|
return;
|
||||||
if (currentTrayMenu) {
|
if (currentTrayMenu) {
|
||||||
@@ -2099,7 +2101,7 @@ BasePill {
|
|||||||
currentTrayMenu = trayMenuComponent.createObject(null);
|
currentTrayMenu = trayMenuComponent.createObject(null);
|
||||||
if (!currentTrayMenu)
|
if (!currentTrayMenu)
|
||||||
return;
|
return;
|
||||||
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
|
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj, byHover === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _trayLayoutRoot() {
|
function _trayLayoutRoot() {
|
||||||
@@ -2147,7 +2149,7 @@ BasePill {
|
|||||||
if (!hit?.trayItem?.hasMenu)
|
if (!hit?.trayItem?.hasMenu)
|
||||||
return false;
|
return false;
|
||||||
const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
|
const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
|
||||||
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis);
|
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,37 +264,102 @@ DankOSD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Row {
|
||||||
width: Theme.iconSize
|
id: transportControls
|
||||||
height: Theme.iconSize
|
|
||||||
radius: Theme.iconSize / 2
|
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
x: parent.gap
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXXS
|
||||||
|
|
||||||
DankIcon {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
width: Theme.iconSize - 4
|
||||||
name: root._displayIcon
|
height: Theme.iconSize - 4
|
||||||
size: Theme.iconSize
|
radius: (Theme.iconSize - 4) / 2
|
||||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
Rectangle {
|
||||||
id: playPauseButton
|
width: Theme.iconSize
|
||||||
|
height: Theme.iconSize
|
||||||
|
radius: Theme.iconSize / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
anchors.fill: parent
|
DankIcon {
|
||||||
hoverEnabled: true
|
anchors.centerIn: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
name: root._displayIcon
|
||||||
onClicked: {
|
size: Theme.iconSize
|
||||||
togglePlaying();
|
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
root.hide();
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: playPauseButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
togglePlaying();
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -1088,15 +1088,50 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractNiriOutputBlocks(content) {
|
||||||
|
const blocks = [];
|
||||||
|
const headerRegex = /output\s+"([^"]+)"\s*\{/g;
|
||||||
|
let match;
|
||||||
|
while ((match = headerRegex.exec(content)) !== null) {
|
||||||
|
const start = headerRegex.lastIndex;
|
||||||
|
let depth = 1;
|
||||||
|
let i = start;
|
||||||
|
while (i < content.length && depth > 0) {
|
||||||
|
const ch = content[i];
|
||||||
|
if (ch === '{')
|
||||||
|
depth++;
|
||||||
|
else if (ch === '}')
|
||||||
|
depth--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
blocks.push({
|
||||||
|
"name": match[1],
|
||||||
|
"body": content.slice(start, i - 1)
|
||||||
|
});
|
||||||
|
headerRegex.lastIndex = i;
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripNestedBlocks(body) {
|
||||||
|
let stripped = body;
|
||||||
|
let prev;
|
||||||
|
do {
|
||||||
|
prev = stripped;
|
||||||
|
stripped = stripped.replace(/[\w-]+\s*\{[^{}]*\}/g, "");
|
||||||
|
} while (stripped !== prev)
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
|
||||||
function parseNiriOutputs(content) {
|
function parseNiriOutputs(content) {
|
||||||
const result = {};
|
const result = {};
|
||||||
const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g;
|
for (const block of extractNiriOutputBlocks(content)) {
|
||||||
let match;
|
const name = block.name;
|
||||||
while ((match = outputRegex.exec(content)) !== null) {
|
const body = block.body;
|
||||||
const name = match[1];
|
|
||||||
const body = match[2];
|
|
||||||
|
|
||||||
const disabled = /^\s*off\s*$/m.test(body);
|
// off marks the output disabled only at the top level of its block;
|
||||||
|
// nested sections like hot-corners { off } must not count (#2966)
|
||||||
|
const disabled = /^\s*off\s*$/m.test(stripNestedBlocks(body));
|
||||||
const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
|
const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
|
||||||
const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
|
const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
|
||||||
const scaleMatch = body.match(/scale\s+([\d.]+)/);
|
const scaleMatch = body.match(/scale\s+([\d.]+)/);
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ Column {
|
|||||||
id: sharedTooltip
|
id: sharedTooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: sharedTooltip.hide()
|
||||||
|
|
||||||
signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
|
signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
|
||||||
signal itemOrderChanged(string sectionId, var orderedIds)
|
signal itemOrderChanged(string sectionId, var orderedIds)
|
||||||
signal addWidget(string sectionId)
|
signal addWidget(string sectionId)
|
||||||
@@ -1215,6 +1217,7 @@ Column {
|
|||||||
iconSize: 18
|
iconSize: 18
|
||||||
iconColor: Theme.error
|
iconColor: Theme.error
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
sharedTooltip.hide();
|
||||||
root.removeWidget(root.sectionId, index);
|
root.removeWidget(root.sectionId, index);
|
||||||
}
|
}
|
||||||
onEntered: {
|
onEntered: {
|
||||||
|
|||||||
@@ -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,17 +811,11 @@ 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();
|
return getPluginItemsForPlugin(pluginId, query);
|
||||||
for (const pluginId in launchers) {
|
|
||||||
const plugin = launchers[pluginId];
|
|
||||||
if ((plugin.name || pluginId) === category) {
|
|
||||||
return getPluginItemsForPlugin(pluginId, query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginItemsForPlugin(pluginId, query) {
|
function getPluginItemsForPlugin(pluginId, query) {
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -136,7 +143,7 @@ Singleton {
|
|||||||
|
|
||||||
function sendAlert(title, message, isWarning, category, notificationType) {
|
function sendAlert(title, message, isWarning, category, notificationType) {
|
||||||
if (notificationType === 1) {
|
if (notificationType === 1) {
|
||||||
Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "battery-caution" : "battery-charging", title, message]);
|
Quickshell.execDetached(["notify-send", "-u", isWarning ? "critical" : "normal", "-a", "DMS", "-i", isWarning ? "material:battery_alert" : "material:battery_charging_full", title, message]);
|
||||||
} else {
|
} else {
|
||||||
if (isWarning) {
|
if (isWarning) {
|
||||||
ToastService.showWarning(title, message, "", category);
|
ToastService.showWarning(title, message, "", category);
|
||||||
@@ -150,7 +157,7 @@ Singleton {
|
|||||||
if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) {
|
if (isCharging && batteryLevel >= SettingsData.batteryChargeLimit) {
|
||||||
if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) {
|
if (!_hasNotifiedChargeLimit && SettingsData.batteryNotifyChargeLimit) {
|
||||||
_hasNotifiedChargeLimit = true;
|
_hasNotifiedChargeLimit = true;
|
||||||
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "battery-charge-limit", SettingsData.batteryChargeLimitNotificationType);
|
sendAlert(I18n.tr("Charge Limit Reached"), I18n.tr("Battery has charged to your set limit of %1%").arg(SettingsData.batteryChargeLimit), false, "material:battery_profile", SettingsData.batteryChargeLimitNotificationType);
|
||||||
}
|
}
|
||||||
} else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) {
|
} else if (!isCharging || batteryLevel < SettingsData.batteryChargeLimit - 2) {
|
||||||
_hasNotifiedChargeLimit = false;
|
_hasNotifiedChargeLimit = false;
|
||||||
@@ -166,7 +173,7 @@ Singleton {
|
|||||||
if (isCriticalBattery) {
|
if (isCriticalBattery) {
|
||||||
if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) {
|
if (!_hasNotifiedCriticalBattery && SettingsData.batteryNotifyCritical) {
|
||||||
_hasNotifiedCriticalBattery = true;
|
_hasNotifiedCriticalBattery = true;
|
||||||
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "battery-critical", SettingsData.batteryCriticalNotificationType);
|
sendAlert(I18n.tr("Critical Battery"), I18n.tr("Battery is at %1% - Connect charger immediately!").arg(batteryLevel), true, "material:battery_alert", SettingsData.batteryCriticalNotificationType);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,7 +186,7 @@ Singleton {
|
|||||||
if (isLowBattery) {
|
if (isLowBattery) {
|
||||||
if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) {
|
if (!_hasNotifiedLowBattery && SettingsData.batteryNotifyLow) {
|
||||||
_hasNotifiedLowBattery = true;
|
_hasNotifiedLowBattery = true;
|
||||||
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "battery-low", SettingsData.batteryLowNotificationType);
|
sendAlert(I18n.tr("Low Battery"), I18n.tr("Battery is at %1% - Consider charging soon").arg(batteryLevel), true, "material:battery_0_bar", SettingsData.batteryLowNotificationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) {
|
if (SettingsData.batteryAutoPowerSaver && PowerProfileWatcher.available) {
|
||||||
@@ -223,6 +230,27 @@ Singleton {
|
|||||||
|
|
||||||
applyPowerProfile();
|
applyPowerProfile();
|
||||||
|
|
||||||
|
if (isPluggedIn) {
|
||||||
|
const dismissLow = SettingsData.batteryLowNotificationType === 1 && SettingsData.notificationTimeoutNormal === 0;
|
||||||
|
const dismissCritical = SettingsData.batteryCriticalNotificationType === 1 && SettingsData.notificationTimeoutCritical === 0;
|
||||||
|
|
||||||
|
if (dismissLow || dismissCritical) {
|
||||||
|
const lowSummary = I18n.tr("Low Battery");
|
||||||
|
const criticalSummary = I18n.tr("Critical Battery");
|
||||||
|
|
||||||
|
for (const w of NotificationService.visibleNotifications) {
|
||||||
|
if (!w || !w.notification)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const summary = w.notification.summary;
|
||||||
|
|
||||||
|
if ((dismissLow && summary === lowSummary) || (dismissCritical && summary === criticalSummary)) {
|
||||||
|
NotificationService.dismissNotification(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previousPluggedState = isPluggedIn;
|
previousPluggedState = isPluggedIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
detectUpdateCommand();
|
return;
|
||||||
}
|
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,52 +135,27 @@ 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.isConnecting = false;
|
|
||||||
root.apiVersion = 0;
|
|
||||||
root.capabilities = [];
|
|
||||||
root.connectionStateChanged();
|
|
||||||
}
|
}
|
||||||
|
root.isConnected = false;
|
||||||
|
root.apiVersion = 0;
|
||||||
|
root.capabilities = [];
|
||||||
|
root.failPendingRequests();
|
||||||
|
root.connectionStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
parser: SplitParser {
|
parser: SplitParser {
|
||||||
@@ -219,10 +186,10 @@ Singleton {
|
|||||||
connected: false
|
connected: false
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
onConnectionStateChanged: {
|
||||||
root.subscribeConnected = connected;
|
root.subscribeConnected = linkUp;
|
||||||
if (connected) {
|
if (!linkUp)
|
||||||
sendSubscribeRequest();
|
return;
|
||||||
}
|
sendSubscribeRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
parser: SplitParser {
|
parser: SplitParser {
|
||||||
@@ -335,6 +302,7 @@ Singleton {
|
|||||||
const data = response.result.data;
|
const data = response.result.data;
|
||||||
|
|
||||||
if (service === "server") {
|
if (service === "server") {
|
||||||
|
const prevCapabilities = capabilities;
|
||||||
apiVersion = data.apiVersion || 0;
|
apiVersion = data.apiVersion || 0;
|
||||||
cliVersion = data.cliVersion || "";
|
cliVersion = data.cliVersion || "";
|
||||||
capabilities = data.capabilities || [];
|
capabilities = data.capabilities || [];
|
||||||
@@ -346,6 +314,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
capabilitiesReceived();
|
capabilitiesReceived();
|
||||||
|
|
||||||
|
const capabilitiesChanged = prevCapabilities.length !== capabilities.length || capabilities.some(c => !prevCapabilities.includes(c));
|
||||||
|
if (prevCapabilities.length > 0 && capabilitiesChanged) {
|
||||||
|
log.info("Capabilities changed, resubscribing");
|
||||||
|
subscribe(activeSubscriptions);
|
||||||
|
}
|
||||||
} else if (service === "network") {
|
} else if (service === "network") {
|
||||||
networkStateUpdate(data);
|
networkStateUpdate(data);
|
||||||
} else if (service === "network.credentials") {
|
} else if (service === "network.credentials") {
|
||||||
@@ -439,10 +413,20 @@ Singleton {
|
|||||||
|
|
||||||
function handleResponse(response) {
|
function handleResponse(response) {
|
||||||
const callback = pendingRequests[response.id];
|
const callback = pendingRequests[response.id];
|
||||||
|
if (!callback)
|
||||||
|
return;
|
||||||
|
delete pendingRequests[response.id];
|
||||||
|
callback(response);
|
||||||
|
}
|
||||||
|
|
||||||
if (callback) {
|
function failPendingRequests() {
|
||||||
delete pendingRequests[response.id];
|
const pending = pendingRequests;
|
||||||
callback(response);
|
pendingRequests = {};
|
||||||
|
clipboardRequestIds = {};
|
||||||
|
for (const id in pending) {
|
||||||
|
pending[id]({
|
||||||
|
"error": "not connected to DMS socket"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1155,6 +1155,8 @@ Singleton {
|
|||||||
return "Temperature must be between 2500K and 6000K";
|
return "Temperature must be between 2500K and 6000K";
|
||||||
|
|
||||||
const rounded = Math.round(temp / 500) * 500;
|
const rounded = Math.round(temp / 500) * 500;
|
||||||
|
if (rounded > SessionData.nightModeHighTemperature)
|
||||||
|
return "Night temperature must not exceed the day temperature (" + SessionData.nightModeHighTemperature + "K)";
|
||||||
SessionData.setNightModeTemperature(rounded);
|
SessionData.setNightModeTemperature(rounded);
|
||||||
|
|
||||||
if (root.nightModeEnabled) {
|
if (root.nightModeEnabled) {
|
||||||
@@ -1184,6 +1186,8 @@ Singleton {
|
|||||||
return "Temperature must be between 2500K and 6500K";
|
return "Temperature must be between 2500K and 6500K";
|
||||||
|
|
||||||
const rounded = Math.round(temp / 500) * 500;
|
const rounded = Math.round(temp / 500) * 500;
|
||||||
|
if (rounded < SessionData.nightModeTemperature)
|
||||||
|
return "Day temperature must be at least the night temperature (" + SessionData.nightModeTemperature + "K)";
|
||||||
SessionData.setNightModeHighTemperature(rounded);
|
SessionData.setNightModeHighTemperature(rounded);
|
||||||
|
|
||||||
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
|
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
|
||||||
|
|||||||
@@ -388,6 +388,8 @@ Singleton {
|
|||||||
const binds = bindsData[cat];
|
const binds = bindsData[cat];
|
||||||
for (var i = 0; i < binds.length; i++) {
|
for (var i = 0; i < binds.length; i++) {
|
||||||
const bind = binds[i];
|
const bind = binds[i];
|
||||||
|
if (currentProvider === "hyprland" && bind.action && bind.action.startsWith("exec "))
|
||||||
|
bind.action = "spawn " + bind.action.slice(5);
|
||||||
const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat;
|
const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat;
|
||||||
if (!processed[targetCat])
|
if (!processed[targetCat])
|
||||||
processed[targetCat] = [];
|
processed[targetCat] = [];
|
||||||
|
|||||||
@@ -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,20 +139,133 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIdle(player: MprisPlayer): bool {
|
function isIdle(player: MprisPlayer): bool {
|
||||||
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
return player && player.playbackState === MprisPlaybackState.Stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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,11 +182,25 @@ Singleton {
|
|||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: MprisController
|
||||||
ignoreUnknownSignals: true
|
function onAvailablePlayersChanged() {
|
||||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
root._updateArtUrl();
|
||||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
}
|
||||||
function onMetadataChanged() { root._updateArtUrl(); }
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: MprisController.availablePlayers
|
||||||
|
delegate: Connections {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
target: modelData
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onIsPlayingChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtistChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackAlbumChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||||
|
function onMetadataChanged() { root._updateArtUrl(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _trackKey() {
|
function _trackKey() {
|
||||||
@@ -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.
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
v1.5.2
|
v1.5.3
|
||||||
|
|||||||
@@ -105,9 +105,11 @@ PanelWindow {
|
|||||||
|
|
||||||
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
||||||
|
|
||||||
|
readonly property string _slideoutScreenName: modelData?.name ?? ""
|
||||||
|
|
||||||
WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
|
WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
|
||||||
WlrLayershell.exclusiveZone: 0
|
WlrLayershell.exclusiveZone: 0
|
||||||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: isVisible && !ModalManager.currentModalsByScreen[_slideoutScreenName] ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||||
|
|
||||||
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
||||||
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
|
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
|
||||||
|
|||||||
@@ -869,7 +869,7 @@ Item {
|
|||||||
|
|
||||||
readonly property var tooltipTexts: ({
|
readonly property var tooltipTexts: ({
|
||||||
"dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
|
"dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
|
||||||
"compositor": I18n.tr("Niri compositor actions (focus, move, etc.)"),
|
"compositor": I18n.tr("Compositor actions (focus, move, etc.)", "keybind action type tooltip"),
|
||||||
"spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
|
"spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
|
||||||
"shell": I18n.tr("Run a shell command (e.g., notify-send)")
|
"shell": I18n.tr("Run a shell command (e.g., notify-send)")
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user