mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-06 05:28:29 -04:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e558f2489 | |||
| 8535e3a1d7 | |||
| c2a7f767c2 | |||
| c9c11f8a27 | |||
| 8cffdab4d7 | |||
| 0e8a4c0cc1 | |||
| 1db6381c78 | |||
| d5c7efc861 | |||
| 943ffb432c | |||
| 7063448b80 | |||
| 52afbc9801 | |||
| 6edb985847 | |||
| 41efe6ad15 | |||
| 4fb6a17e1b | |||
| e771e3b675 | |||
| b1b7aa7aa2 | |||
| c128793239 | |||
| 069ddab041 | |||
| 45cf6ecefd | |||
| 7058b00091 | |||
| 6ba4b79039 | |||
| b8e2ce1da8 | |||
| c58d55db7d | |||
| e15db00714 | |||
| 906eba5c17 | |||
| d2b61b91ef | |||
| 6c27b92445 | |||
| a64bd80cc9 | |||
| 661dc27d26 | |||
| 3d5a6fb9d7 | |||
| c2a5397d8a | |||
| 9b5538323d | |||
| 8321d28d18 | |||
| 39278860c8 | |||
| f455456ff1 | |||
| 5a80a32951 | |||
| 3cedd56f23 | |||
| dc1ad5b329 | |||
| 25c12a14a9 | |||
| b72b276fb4 | |||
| f89fb67316 | |||
| f457e04cb1 | |||
| 23daca0b57 |
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
Lf, af, bf := cf.Lab()
|
||||
Lf *= 100.0
|
||||
|
||||
dir := 1.0
|
||||
if isLightMode {
|
||||
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
origL, af, bf := cf.Lab()
|
||||
origL *= 100.0
|
||||
|
||||
var darkerResult, lighterResult string
|
||||
darkerL, lighterL := origL, origL
|
||||
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
|
||||
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 {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
gray7V := baseVal * 0.28
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.05
|
||||
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))
|
||||
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
|
||||
|
||||
brightRedS := math.Min(baseSat*1.0, 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)
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.15
|
||||
gray8V := baseVal * 0.65
|
||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
|
||||
|
||||
brightRedS := math.Min(baseSat*0.75, 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
}
|
||||
g.log("Portage tree synced successfully")
|
||||
|
||||
args := emergeInstallArgs(missingPkgs)
|
||||
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
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, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, packageNames...)
|
||||
args := emergeInstallArgs(packageNames)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
||||
guruPackages[i] = pkg + "::guru"
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, guruPackages...)
|
||||
args := emergeInstallArgs(guruPackages)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
|
||||
@@ -7,7 +7,7 @@ type minimalInstallGroup struct {
|
||||
|
||||
func shouldPreferMinimalInstall(pkg string) bool {
|
||||
switch pkg {
|
||||
case "niri", "niri-git":
|
||||
case "niri", "niri-git", "hyprland", "hyprland-git":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -43,10 +43,10 @@ func NewManager() (*Manager, error) {
|
||||
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
|
||||
m.promptBroker = broker
|
||||
|
||||
adapter, err := m.findAdapter()
|
||||
adapter, err := findAdapter(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("no bluetooth adapter found: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
m.adapterPath = adapter
|
||||
|
||||
@@ -74,12 +74,12 @@ func NewManager() (*Manager, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
|
||||
func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) {
|
||||
obj := conn.Object(bluezService, dbus.ObjectPath("/"))
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
|
||||
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("%w: %v", ErrNoAdapter, err)
|
||||
}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
@@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no adapter found")
|
||||
return "", ErrNoAdapter
|
||||
}
|
||||
|
||||
func (m *Manager) initialize() error {
|
||||
@@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error {
|
||||
}
|
||||
|
||||
func (m *Manager) SetPowered(powered bool) error {
|
||||
if powered {
|
||||
if err := rfkillUnblockBluetooth(); err != nil {
|
||||
log.Debugf("[BluezManager] rfkill unblock failed: %v", err)
|
||||
}
|
||||
}
|
||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package bluez
|
||||
|
||||
func rfkillUnblockBluetooth() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package bluez
|
||||
|
||||
import "os"
|
||||
|
||||
// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; },
|
||||
// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3
|
||||
func rfkillUnblockBluetooth() error {
|
||||
f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var event [8]byte
|
||||
event[4] = 2
|
||||
event[5] = 3
|
||||
_, err = f.Write(event[:])
|
||||
return err
|
||||
}
|
||||
@@ -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 {
|
||||
ch := make(chan CUPSState, 64)
|
||||
|
||||
m.subLifecycleMu.Lock()
|
||||
defer m.subLifecycleMu.Unlock()
|
||||
|
||||
wasEmpty := true
|
||||
m.subscribers.Range(func(key string, ch chan CUPSState) bool {
|
||||
wasEmpty = false
|
||||
@@ -237,19 +240,25 @@ func (m *Manager) Subscribe(id string) chan CUPSState {
|
||||
|
||||
m.subscribers.Store(id, ch)
|
||||
|
||||
if wasEmpty && m.subscription != nil {
|
||||
if err := m.subscription.Start(); err != nil {
|
||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||
} else {
|
||||
m.eventWG.Add(1)
|
||||
go m.eventHandler()
|
||||
}
|
||||
if !wasEmpty || m.subscription == nil {
|
||||
return ch
|
||||
}
|
||||
|
||||
if err := m.subscription.Start(); err != nil {
|
||||
log.Warnf("[CUPS] Failed to start subscription manager: %v", err)
|
||||
return ch
|
||||
}
|
||||
|
||||
m.eventWG.Add(1)
|
||||
go m.eventHandler()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subLifecycleMu.Lock()
|
||||
defer m.subLifecycleMu.Unlock()
|
||||
|
||||
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(val)
|
||||
}
|
||||
@@ -260,18 +269,22 @@ func (m *Manager) Unsubscribe(id string) {
|
||||
return false
|
||||
})
|
||||
|
||||
if isEmpty && m.subscription != nil {
|
||||
m.subscription.Stop()
|
||||
m.eventWG.Wait()
|
||||
if !isEmpty || m.subscription == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.subscription.Stop()
|
||||
m.eventWG.Wait()
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
|
||||
m.subLifecycleMu.Lock()
|
||||
if m.subscription != nil {
|
||||
m.subscription.Stop()
|
||||
}
|
||||
m.subLifecycleMu.Unlock()
|
||||
|
||||
m.eventWG.Wait()
|
||||
m.notifierWg.Wait()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups"
|
||||
@@ -75,6 +78,67 @@ func TestManager_Subscribe(t *testing.T) {
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
// mirrors the real managers: eventChan guarded by mu, conn/running deliberately
|
||||
// unsynchronized so overlapping Start/Stop trips the race detector
|
||||
type stubSubscription struct {
|
||||
mu sync.Mutex
|
||||
events chan SubscriptionEvent
|
||||
conn *int
|
||||
running bool
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Start() error {
|
||||
if s.running {
|
||||
return errors.New("already running")
|
||||
}
|
||||
s.running = true
|
||||
|
||||
s.mu.Lock()
|
||||
s.events = make(chan SubscriptionEvent)
|
||||
s.mu.Unlock()
|
||||
|
||||
v := 0
|
||||
s.conn = &v
|
||||
*s.conn++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Stop() {
|
||||
if !s.running {
|
||||
return
|
||||
}
|
||||
s.running = false
|
||||
s.conn = nil
|
||||
|
||||
s.mu.Lock()
|
||||
close(s.events)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *stubSubscription) Events() <-chan SubscriptionEvent {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.events
|
||||
}
|
||||
|
||||
func TestManager_SubscribeUnsubscribeRace(t *testing.T) {
|
||||
m := NewTestManager(mocks_cups.NewMockCUPSClientInterface(t), nil)
|
||||
m.subscription = &stubSubscription{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 8 {
|
||||
wg.Go(func() {
|
||||
id := fmt.Sprintf("client-%d", i)
|
||||
for range 50 {
|
||||
m.Subscribe(id)
|
||||
m.Unsubscribe(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestManager_Close(t *testing.T) {
|
||||
mockClient := mocks_cups.NewMockCUPSClientInterface(t)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ type Manager struct {
|
||||
client CUPSClientInterface
|
||||
pkHelper PkHelper
|
||||
subscription SubscriptionManagerInterface
|
||||
subLifecycleMu sync.Mutex
|
||||
stateMutex sync.RWMutex
|
||||
subscribers syncmap.Map[string, chan CUPSState]
|
||||
stopChan chan struct{}
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
|
||||
return nil, fmt.Errorf("failed to find keyboards: %w", err)
|
||||
}
|
||||
|
||||
initialCapsLock := readInitialCapsLockState(devices[0])
|
||||
initialCapsLock, _ := capsLockFromDevices(devices)
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func readInitialCapsLockState(device EvdevDevice) bool {
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil {
|
||||
log.Debugf("Could not read LED state: %v", err)
|
||||
return false
|
||||
func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
|
||||
for _, device := range devices {
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
|
||||
m.devicesMutex.RUnlock()
|
||||
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()
|
||||
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read LED state: %v", err)
|
||||
capsLockState, ok := capsLockFromDevices(ordered)
|
||||
if !ok {
|
||||
log.Debug("No LED-capable device available for caps lock state")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestReadInitialCapsLockState(t *testing.T) {
|
||||
func TestCapsLockFromDevices(t *testing.T) {
|
||||
t.Run("caps lock is on", func(t *testing.T) {
|
||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||
ledState := evdev.StateMap{
|
||||
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||
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)
|
||||
})
|
||||
|
||||
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) {
|
||||
|
||||
@@ -117,11 +117,12 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "cups.") {
|
||||
if cupsManager == nil {
|
||||
mgr, err := ensureCupsManager()
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, "CUPS manager not initialized")
|
||||
return
|
||||
}
|
||||
cups.HandleRequest(conn, req, cupsManager)
|
||||
cups.HandleRequest(conn, req, mgr)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -85,8 +84,8 @@ var geoClientInstance geolocation.Client
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||
var cupsSubscribers syncmap.Map[string, bool]
|
||||
var cupsSubscriberCount atomic.Int32
|
||||
var cupsMu sync.Mutex
|
||||
var cupsSubscriberCount int
|
||||
|
||||
func getSocketDir() string {
|
||||
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
||||
@@ -243,17 +242,55 @@ func InitializeAppPickerManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeCupsManager() error {
|
||||
func initializeCupsManagerLocked() (bool, error) {
|
||||
if cupsManager != nil {
|
||||
return false, nil
|
||||
}
|
||||
manager, err := cups.NewManager()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize cups manager: %v", err)
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
cupsManager = manager
|
||||
|
||||
log.Info("CUPS manager initialized")
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func ensureCupsManager() (*cups.Manager, error) {
|
||||
cupsMu.Lock()
|
||||
created, err := initializeCupsManagerLocked()
|
||||
mgr := cupsManager
|
||||
cupsMu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if created {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func cupsAvailable() bool {
|
||||
cupsMu.Lock()
|
||||
defer cupsMu.Unlock()
|
||||
return cupsManager != nil
|
||||
}
|
||||
|
||||
func releaseCupsSubscriber() {
|
||||
cupsMu.Lock()
|
||||
cupsSubscriberCount--
|
||||
var mgr *cups.Manager
|
||||
if cupsSubscriberCount == 0 && cupsManager != nil {
|
||||
mgr = cupsManager
|
||||
cupsManager = nil
|
||||
}
|
||||
cupsMu.Unlock()
|
||||
if mgr == nil {
|
||||
return
|
||||
}
|
||||
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
|
||||
mgr.Close()
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
|
||||
func InitializeBrightnessManager() error {
|
||||
@@ -463,7 +500,7 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "browser")
|
||||
}
|
||||
|
||||
if cupsManager != nil {
|
||||
if cupsAvailable() {
|
||||
caps = append(caps, "cups")
|
||||
}
|
||||
|
||||
@@ -533,7 +570,7 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "browser")
|
||||
}
|
||||
|
||||
if cupsManager != nil {
|
||||
if cupsAvailable() {
|
||||
caps = append(caps, "cups")
|
||||
}
|
||||
|
||||
@@ -997,38 +1034,31 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
||||
}
|
||||
|
||||
if shouldSubscribe("cups") {
|
||||
cupsSubscribers.Store(clientID+"-cups", true)
|
||||
count := cupsSubscriberCount.Add(1)
|
||||
cupsMu.Lock()
|
||||
cupsSubscriberCount++
|
||||
created, err := initializeCupsManagerLocked()
|
||||
mgr := cupsManager
|
||||
cupsMu.Unlock()
|
||||
|
||||
if count == 1 {
|
||||
if err := InitializeCupsManager(); err != nil {
|
||||
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
|
||||
} else if created {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
|
||||
if cupsManager != nil {
|
||||
if mgr == nil {
|
||||
releaseCupsSubscriber()
|
||||
} else {
|
||||
wg.Add(1)
|
||||
cupsChan := cupsManager.Subscribe(clientID + "-cups")
|
||||
cupsChan := mgr.Subscribe(clientID + "-cups")
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
cupsManager.Unsubscribe(clientID + "-cups")
|
||||
cupsSubscribers.Delete(clientID + "-cups")
|
||||
count := cupsSubscriberCount.Add(-1)
|
||||
|
||||
if count == 0 {
|
||||
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
|
||||
if cupsManager != nil {
|
||||
cupsManager.Close()
|
||||
cupsManager = nil
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
}
|
||||
mgr.Unsubscribe(clientID + "-cups")
|
||||
releaseCupsSubscriber()
|
||||
}()
|
||||
|
||||
initialState := cupsManager.GetState()
|
||||
initialState := mgr.GetState()
|
||||
select {
|
||||
case eventChan <- ServiceEvent{Service: "cups", Data: initialState}:
|
||||
case <-stopChan:
|
||||
@@ -1338,9 +1368,12 @@ func cleanupManagers() {
|
||||
if appPickerManager != nil {
|
||||
appPickerManager.Close()
|
||||
}
|
||||
cupsMu.Lock()
|
||||
if cupsManager != nil {
|
||||
cupsManager.Close()
|
||||
cupsManager = nil
|
||||
}
|
||||
cupsMu.Unlock()
|
||||
if brightnessManager != nil {
|
||||
brightnessManager.Close()
|
||||
}
|
||||
@@ -1698,10 +1731,21 @@ func Start(printDocs bool) error {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := InitializeBluezManager(); err != nil {
|
||||
for {
|
||||
err := InitializeBluezManager()
|
||||
if err == nil {
|
||||
notifyCapabilityChange()
|
||||
return
|
||||
}
|
||||
log.Warnf("Bluez manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
if !errors.Is(err, bluez.ErrNoAdapter) {
|
||||
return
|
||||
}
|
||||
if err := bluez.WaitForAdapter(); err != nil {
|
||||
log.Warnf("Bluetooth adapter watch failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Info("Bluetooth adapter appeared, initializing bluez manager")
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -143,7 +143,8 @@ func wrapInTerminal(term, title, shellCmd string, extraArgs []string) []string {
|
||||
case "konsole":
|
||||
argv = []string{term, "-p", "tabtitle=" + title}
|
||||
case "gnome-terminal":
|
||||
argv = []string{term, "--title=" + title}
|
||||
// --wait: the factory process otherwise returns immediately
|
||||
argv = []string{term, "--wait", "--title=" + title}
|
||||
execFlag = "--"
|
||||
default:
|
||||
argv = []string{term}
|
||||
|
||||
@@ -412,25 +412,30 @@ func (m *Manager) runCustomUpgrade(ctx context.Context, opts UpgradeOptions) {
|
||||
onLine := func(line string) { m.appendLog(line) }
|
||||
argv := wrapInTerminal(term, "DMS — System Update (custom)", opts.CustomCommand, opts.TerminalArgs)
|
||||
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
|
||||
code := ErrCodeBackendFailed
|
||||
switch {
|
||||
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
||||
code = ErrCodeTimeout
|
||||
m.failCustomUpgrade(ErrCodeTimeout, err)
|
||||
return
|
||||
case errors.Is(ctx.Err(), context.Canceled):
|
||||
code = ErrCodeCancelled
|
||||
m.failCustomUpgrade(ErrCodeCancelled, err)
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
|
||||
m.mu.Unlock()
|
||||
m.markDirty()
|
||||
return
|
||||
// exit status reflects the trailing `read`, not the update command
|
||||
m.appendLog(fmt.Sprintf("Terminal exited early: %v", err))
|
||||
}
|
||||
|
||||
m.finishSuccessfulUpgrade(false)
|
||||
m.runRefresh(context.Background(), false)
|
||||
}
|
||||
|
||||
func (m *Manager) failCustomUpgrade(code ErrorCode, err error) {
|
||||
m.mu.Lock()
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
|
||||
m.mu.Unlock()
|
||||
m.markDirty()
|
||||
}
|
||||
|
||||
func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
||||
m.appendLog("Upgrade complete.")
|
||||
|
||||
|
||||
@@ -233,3 +233,27 @@ func TestUpgradeBackendsFiltersFlatpakOnly(t *testing.T) {
|
||||
t.Fatalf("upgradeBackends(mixed) = %#v, want dnf5 then flatpak", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapInTerminal(t *testing.T) {
|
||||
tests := []struct {
|
||||
term string
|
||||
wantPrefix []string
|
||||
}{
|
||||
{"kitty", []string{"kitty", "--class", "com.danklinux.dms", "-T", "Title"}},
|
||||
{"gnome-terminal", []string{"gnome-terminal", "--wait", "--title=Title"}},
|
||||
{"foot", []string{"foot", "--app-id=com.danklinux.dms", "--title=Title"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := wrapInTerminal(tt.term, "Title", "echo hi", nil)
|
||||
if len(got) < len(tt.wantPrefix) || !reflect.DeepEqual(got[:len(tt.wantPrefix)], tt.wantPrefix) {
|
||||
t.Errorf("wrapInTerminal(%q) = %#v, want prefix %#v", tt.term, got, tt.wantPrefix)
|
||||
}
|
||||
tail := got[len(got)-3:]
|
||||
if tail[0] != "sh" || tail[1] != "-c" {
|
||||
t.Errorf("wrapInTerminal(%q) tail = %#v, want [sh -c <cmd>]", tt.term, tail)
|
||||
}
|
||||
if !strings.Contains(tail[2], "echo hi") {
|
||||
t.Errorf("wrapInTerminal(%q) command %q does not contain shell command", tt.term, tail[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,6 +760,9 @@ func (m *Manager) schedulerLoop() {
|
||||
|
||||
now := time.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
|
||||
if enabled {
|
||||
@@ -1120,13 +1123,15 @@ func (m *Manager) SetTemperature(low, high int) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.LowTemp = low
|
||||
m.config.HighTemp = high
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.LowTemp = low
|
||||
updated.HighTemp = high
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
@@ -1138,14 +1143,16 @@ func (m *Manager) SetLocation(lat, lon float64) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.Latitude = &lat
|
||||
m.config.Longitude = &lon
|
||||
m.config.UseIPLocation = false
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.Latitude = &lat
|
||||
updated.Longitude = &lon
|
||||
updated.UseIPLocation = false
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
@@ -1180,13 +1187,15 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.ManualSunrise = &sunrise
|
||||
m.config.ManualSunset = &sunset
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.ManualSunrise = &sunrise
|
||||
updated.ManualSunset = &sunset
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
@@ -1209,12 +1218,14 @@ func (m *Manager) SetGamma(gamma float64) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.Gamma = gamma
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.Gamma = gamma
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
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) {
|
||||
@@ -412,3 +413,75 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -928,8 +928,8 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
||||
if a.Opacity != nil {
|
||||
lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity))
|
||||
}
|
||||
if a.OpenFloating != nil && *a.OpenFloating {
|
||||
lines = append(lines, " open-floating true")
|
||||
if a.OpenFloating != nil {
|
||||
lines = append(lines, fmt.Sprintf(" open-floating %t", *a.OpenFloating))
|
||||
}
|
||||
if a.OpenMaximized != nil && *a.OpenMaximized {
|
||||
lines = append(lines, " open-maximized true")
|
||||
|
||||
@@ -11,7 +11,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("CacheData")
|
||||
|
||||
readonly property int cacheConfigVersion: 1
|
||||
readonly property int cacheConfigVersion: 2
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -19,10 +19,21 @@ Singleton {
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
property bool _loading: false
|
||||
property bool _hasLoaded: false
|
||||
property int _loadedCacheVersion: 0
|
||||
|
||||
readonly property var _pinKeys: ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"]
|
||||
readonly property var _dataKeys: ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings"].concat(_pinKeys)
|
||||
|
||||
property string wallpaperLastPath: ""
|
||||
property string profileLastPath: ""
|
||||
|
||||
property var brightnessDevicePins: ({})
|
||||
property var wifiNetworkPins: ({})
|
||||
property var bluetoothDevicePins: ({})
|
||||
property var audioInputDevicePins: ({})
|
||||
property var audioOutputDevicePins: ({})
|
||||
|
||||
property var fileBrowserSettings: ({
|
||||
"wallpaper": {
|
||||
"lastPath": "",
|
||||
@@ -82,8 +93,46 @@ Singleton {
|
||||
|
||||
function loadCache() {
|
||||
_loading = true;
|
||||
parseCache(cacheFile.text());
|
||||
_loading = false;
|
||||
try {
|
||||
parseCache(cacheFile.text());
|
||||
} finally {
|
||||
_loading = false;
|
||||
_hasLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
if (_dataKeys.indexOf(key) < 0) {
|
||||
log.warn("Unknown cache key:", key);
|
||||
return;
|
||||
}
|
||||
root[key] = value;
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function migratePins(pins) {
|
||||
if (!pins)
|
||||
return;
|
||||
if (!_hasLoaded)
|
||||
loadCache();
|
||||
if (_loadedCacheVersion >= cacheConfigVersion)
|
||||
return;
|
||||
|
||||
let migrated = false;
|
||||
for (const key of _pinKeys) {
|
||||
const legacy = pins[key];
|
||||
if (!legacy || Object.keys(legacy).length === 0)
|
||||
continue;
|
||||
if (Object.keys(root[key] || {}).length > 0)
|
||||
continue;
|
||||
root[key] = legacy;
|
||||
migrated = true;
|
||||
}
|
||||
|
||||
if (!migrated)
|
||||
return;
|
||||
log.info("Migrated device pins from settings.json");
|
||||
saveCache();
|
||||
}
|
||||
|
||||
function parseCache(content) {
|
||||
@@ -91,6 +140,7 @@ Singleton {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const cache = JSON.parse(content);
|
||||
_loadedCacheVersion = cache.configVersion || 0;
|
||||
|
||||
wallpaperLastPath = cache.wallpaperLastPath !== undefined ? cache.wallpaperLastPath : "";
|
||||
profileLastPath = cache.profileLastPath !== undefined ? cache.profileLastPath : "";
|
||||
@@ -126,6 +176,10 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
for (const key of _pinKeys) {
|
||||
root[key] = cache[key] !== undefined ? cache[key] : {};
|
||||
}
|
||||
|
||||
if (cache.configVersion === undefined) {
|
||||
migrateFromUndefinedToV1(cache);
|
||||
cleanupUnusedKeys();
|
||||
@@ -142,12 +196,16 @@ Singleton {
|
||||
function saveCache() {
|
||||
if (_loading)
|
||||
return;
|
||||
cacheFile.setText(JSON.stringify({
|
||||
const data = {
|
||||
"wallpaperLastPath": wallpaperLastPath,
|
||||
"profileLastPath": profileLastPath,
|
||||
"fileBrowserSettings": fileBrowserSettings,
|
||||
"configVersion": cacheConfigVersion
|
||||
}, null, 2));
|
||||
};
|
||||
for (const key of _pinKeys) {
|
||||
data[key] = root[key];
|
||||
}
|
||||
cacheFile.setText(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(cache) {
|
||||
@@ -155,7 +213,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function cleanupUnusedKeys() {
|
||||
const validKeys = ["wallpaperLastPath", "profileLastPath", "fileBrowserSettings", "configVersion"];
|
||||
const validKeys = _dataKeys.concat(["configVersion"]);
|
||||
|
||||
try {
|
||||
const content = cacheFile.text();
|
||||
|
||||
@@ -15,7 +15,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SettingsData")
|
||||
|
||||
readonly property int settingsConfigVersion: 12
|
||||
readonly property int settingsConfigVersion: 13
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -773,11 +773,6 @@ Singleton {
|
||||
property bool fadeToDpmsEnabled: true
|
||||
property int fadeToDpmsGracePeriod: 5
|
||||
property string launchPrefix: ""
|
||||
property var brightnessDevicePins: ({})
|
||||
property var wifiNetworkPins: ({})
|
||||
property var bluetoothDevicePins: ({})
|
||||
property var audioInputDevicePins: ({})
|
||||
property var audioOutputDevicePins: ({})
|
||||
|
||||
property bool gtkThemingEnabled: false
|
||||
property bool qtThemingEnabled: false
|
||||
@@ -1741,6 +1736,7 @@ Singleton {
|
||||
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
|
||||
|
||||
const oldVersion = obj?.configVersion ?? 0;
|
||||
const legacyPins = oldVersion < 13 ? Store.extractPins(obj) : null;
|
||||
if (oldVersion < settingsConfigVersion) {
|
||||
const migrated = Store.migrateToVersion(obj, settingsConfigVersion);
|
||||
if (migrated) {
|
||||
@@ -1748,6 +1744,8 @@ Singleton {
|
||||
obj = migrated;
|
||||
}
|
||||
}
|
||||
if (legacyPins)
|
||||
Qt.callLater(() => CacheData.migratePins(legacyPins));
|
||||
|
||||
if (obj?.lockScreenActiveMonitor !== undefined) {
|
||||
var oldVal = obj.lockScreenActiveMonitor;
|
||||
|
||||
@@ -354,11 +354,6 @@ var SPEC = {
|
||||
fadeToDpmsEnabled: { def: true },
|
||||
fadeToDpmsGracePeriod: { def: 5 },
|
||||
launchPrefix: { def: "" },
|
||||
brightnessDevicePins: { def: {} },
|
||||
wifiNetworkPins: { def: {} },
|
||||
bluetoothDevicePins: { def: {} },
|
||||
audioInputDevicePins: { def: {} },
|
||||
audioOutputDevicePins: { def: {} },
|
||||
|
||||
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
|
||||
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
.import "./SettingsSpec.js" as SpecModule
|
||||
|
||||
var PIN_KEYS = ["brightnessDevicePins", "wifiNetworkPins", "bluetoothDevicePins", "audioInputDevicePins", "audioOutputDevicePins"];
|
||||
|
||||
function extractPins(obj) {
|
||||
if (!obj) return null;
|
||||
|
||||
var pins = null;
|
||||
for (var i = 0; i < PIN_KEYS.length; i++) {
|
||||
var value = obj[PIN_KEYS[i]];
|
||||
if (!value || Object.keys(value).length === 0) continue;
|
||||
if (!pins) pins = {};
|
||||
pins[PIN_KEYS[i]] = value;
|
||||
}
|
||||
return pins;
|
||||
}
|
||||
|
||||
function parse(root, jsonObj) {
|
||||
var SPEC = SpecModule.SPEC;
|
||||
|
||||
@@ -263,6 +278,17 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 12;
|
||||
}
|
||||
|
||||
if (currentVersion < 13) {
|
||||
console.info("Migrating settings from version", currentVersion, "to version 13");
|
||||
console.info("Moving device and network pins to cache.json");
|
||||
|
||||
for (var p = 0; p < PIN_KEYS.length; p++) {
|
||||
delete settings[PIN_KEYS[p]];
|
||||
}
|
||||
|
||||
settings.configVersion = 13;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -982,6 +982,9 @@ Item {
|
||||
case "reboot":
|
||||
SessionService.reboot();
|
||||
break;
|
||||
case "softreboot":
|
||||
SessionService.softReboot();
|
||||
break;
|
||||
case "poweroff":
|
||||
SessionService.poweroff();
|
||||
break;
|
||||
|
||||
@@ -449,7 +449,12 @@ Item {
|
||||
searchQuery = query;
|
||||
requestSearch();
|
||||
|
||||
if (searchMode !== "plugins" && query.startsWith("/")) {
|
||||
if (autoSwitchedToFiles && !query.startsWith("/")) {
|
||||
restorePreviousMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.startsWith("/")) {
|
||||
var prefix = Utils.parseFileSearchPrefix(query);
|
||||
var explicitType = prefix && prefix.type !== null ? prefix.type : null;
|
||||
var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all");
|
||||
@@ -814,16 +819,24 @@ Item {
|
||||
}
|
||||
|
||||
if (isCategoryFiltered) {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; 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]));
|
||||
var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
|
||||
if (categoryPluginId) {
|
||||
var pluginCategoryItems = getPluginItems(categoryPluginId, "");
|
||||
for (var i = 0; i < pluginCategoryItems.length; i++) {
|
||||
allItems.push(pluginCategoryItems[i]);
|
||||
}
|
||||
} else {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; 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 {
|
||||
var apps = searchApps(searchQuery);
|
||||
@@ -834,7 +847,7 @@ Item {
|
||||
|
||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||
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 i = 0; i < newSections.length; i++) {
|
||||
|
||||
@@ -174,9 +174,12 @@ Item {
|
||||
spotlightContent.controller.selectedFlatIndex = 0;
|
||||
spotlightContent.controller.selectedItem = null;
|
||||
spotlightContent.controller.historyIndex = -1;
|
||||
spotlightContent.controller.searchQuery = targetQuery;
|
||||
|
||||
spotlightContent.controller.performSearch();
|
||||
if (targetQuery) {
|
||||
spotlightContent.controller.setSearchQuery(targetQuery);
|
||||
} else {
|
||||
spotlightContent.controller.searchQuery = "";
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
@@ -271,7 +274,7 @@ Item {
|
||||
target: spotlightContent?.controller ?? null
|
||||
|
||||
function onModeChanged(mode, userInitiated) {
|
||||
if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps"))
|
||||
if (!userInitiated || !SettingsData.rememberLastMode)
|
||||
return;
|
||||
SessionData.setLauncherLastMode(mode);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,8 @@ DankModal {
|
||||
visibleActions = allActions.filter(action => {
|
||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||
return false;
|
||||
if (action === "softreboot" && !SessionService.softRebootSupported)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -223,6 +225,12 @@ DankModal {
|
||||
"label": I18n.tr("Reboot"),
|
||||
"key": "R"
|
||||
};
|
||||
case "softreboot":
|
||||
return {
|
||||
"icon": "autorenew",
|
||||
"label": I18n.tr("Soft Reboot"),
|
||||
"key": "B"
|
||||
};
|
||||
case "logout":
|
||||
return {
|
||||
"icon": "logout",
|
||||
@@ -370,7 +378,7 @@ DankModal {
|
||||
|
||||
function handleListNavigation(event, 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();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -429,6 +437,12 @@ DankModal {
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_B:
|
||||
if (visibleActions.includes("softreboot")) {
|
||||
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_X:
|
||||
if (visibleActions.includes("logout")) {
|
||||
startHold("logout", visibleActions.indexOf("logout"));
|
||||
@@ -464,7 +478,7 @@ DankModal {
|
||||
|
||||
function handleGridNavigation(event, 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();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -539,6 +553,12 @@ DankModal {
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_B:
|
||||
if (visibleActions.includes("softreboot")) {
|
||||
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_X:
|
||||
if (visibleActions.includes("logout")) {
|
||||
startHold("logout", visibleActions.indexOf("logout"));
|
||||
@@ -597,7 +617,7 @@ DankModal {
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
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
|
||||
|
||||
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||
@@ -627,7 +647,7 @@ DankModal {
|
||||
color: {
|
||||
if (gridButtonRect.modelData === "poweroff")
|
||||
return Theme.errorSelected;
|
||||
if (gridButtonRect.modelData === "reboot")
|
||||
if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
|
||||
return Theme.withAlpha(Theme.warning, 0.3);
|
||||
return Theme.primarySelected;
|
||||
}
|
||||
@@ -722,7 +742,7 @@ DankModal {
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
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
|
||||
|
||||
width: parent.width
|
||||
@@ -752,7 +772,7 @@ DankModal {
|
||||
color: {
|
||||
if (listButtonRect.modelData === "poweroff")
|
||||
return Theme.errorSelected;
|
||||
if (listButtonRect.modelData === "reboot")
|
||||
if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
|
||||
return Theme.withAlpha(Theme.warning, 0.3);
|
||||
return Theme.primarySelected;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ FloatingWindow {
|
||||
signal closingModal
|
||||
|
||||
function show() {
|
||||
if (visible && !backingWindowVisible) {
|
||||
visible = false;
|
||||
}
|
||||
visible = true;
|
||||
}
|
||||
|
||||
@@ -49,7 +52,11 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
visible = !visible;
|
||||
if (visible && backingWindowVisible) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
show();
|
||||
}
|
||||
|
||||
function setTabIndex(tabIndex: int) {
|
||||
@@ -61,13 +68,12 @@ FloatingWindow {
|
||||
|
||||
function showWithTab(tabIndex: int) {
|
||||
setTabIndex(tabIndex);
|
||||
visible = true;
|
||||
show();
|
||||
}
|
||||
|
||||
function showWithTabName(tabName: string) {
|
||||
var idx = sidebar.resolveTabIndex(tabName);
|
||||
setTabIndex(idx);
|
||||
visible = true;
|
||||
setTabIndex(sidebar.resolveTabIndex(tabName));
|
||||
show();
|
||||
}
|
||||
|
||||
function resolveTabIndex(tabName: string): int {
|
||||
|
||||
@@ -52,6 +52,7 @@ FloatingWindow {
|
||||
condInitialised.triState = 0;
|
||||
opacityEnabled.checked = false;
|
||||
opacitySlider.value = 100;
|
||||
floatingCond.triState = 0;
|
||||
floatingToggle.checked = false;
|
||||
maximizedToggle.checked = false;
|
||||
maximizedToEdgesToggle.checked = false;
|
||||
@@ -164,6 +165,7 @@ FloatingWindow {
|
||||
opacityEnabled.checked = hasOpacity;
|
||||
opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100;
|
||||
|
||||
floatingCond.triState = triFromBool(actions.openFloating);
|
||||
floatingToggle.checked = actions.openFloating || false;
|
||||
maximizedToggle.checked = actions.openMaximized || false;
|
||||
maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false;
|
||||
@@ -318,7 +320,9 @@ FloatingWindow {
|
||||
|
||||
if (opacityEnabled.checked)
|
||||
actions.opacity = opacitySlider.value / 100;
|
||||
if (floatingToggle.checked)
|
||||
if (isNiri)
|
||||
applyCond(actions, "openFloating", floatingCond.triState);
|
||||
else if (floatingToggle.checked)
|
||||
actions.openFloating = true;
|
||||
if (maximizedToggle.checked)
|
||||
actions.openMaximized = true;
|
||||
@@ -943,6 +947,17 @@ FloatingWindow {
|
||||
title: I18n.tr("Window Opening")
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: isNiri
|
||||
|
||||
MatchCond {
|
||||
id: floatingCond
|
||||
label: I18n.tr("Float")
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
@@ -950,6 +965,7 @@ FloatingWindow {
|
||||
CheckboxRow {
|
||||
id: floatingToggle
|
||||
label: I18n.tr("Float")
|
||||
visible: !isNiri
|
||||
}
|
||||
CheckboxRow {
|
||||
id: maximizedToggle
|
||||
|
||||
@@ -469,6 +469,8 @@ PluginComponent {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: peerMouseArea
|
||||
|
||||
z: -1
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
@@ -159,7 +159,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getPinnedInputs() {
|
||||
const pins = SettingsData.audioInputDevicePins || {};
|
||||
const pins = CacheData.audioInputDevicePins || {};
|
||||
return normalizePinList(pins["preferredInput"]);
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.audioInputDevicePins || {}));
|
||||
let pinnedList = audioContent.normalizePinList(pins["preferredInput"]);
|
||||
const pinIndex = pinnedList.indexOf(modelData.name);
|
||||
|
||||
@@ -332,7 +332,7 @@ Rectangle {
|
||||
else
|
||||
delete pins["preferredInput"];
|
||||
|
||||
SettingsData.set("audioInputDevicePins", pins);
|
||||
CacheData.set("audioInputDevicePins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getPinnedOutputs() {
|
||||
const pins = SettingsData.audioOutputDevicePins || {};
|
||||
const pins = CacheData.audioOutputDevicePins || {};
|
||||
return normalizePinList(pins["preferredOutput"]);
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.audioOutputDevicePins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.audioOutputDevicePins || {}));
|
||||
let pinnedList = audioContent.normalizePinList(pins["preferredOutput"]);
|
||||
const pinIndex = pinnedList.indexOf(modelData.name);
|
||||
|
||||
@@ -341,7 +341,7 @@ Rectangle {
|
||||
else
|
||||
delete pins["preferredOutput"];
|
||||
|
||||
SettingsData.set("audioOutputDevicePins", pins);
|
||||
CacheData.set("audioOutputDevicePins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getPinnedDevices() {
|
||||
const pins = SettingsData.bluetoothDevicePins || {};
|
||||
const pins = CacheData.bluetoothDevicePins || {};
|
||||
return normalizePinList(pins["preferredDevice"]);
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.bluetoothDevicePins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.bluetoothDevicePins || {}));
|
||||
let pinnedList = root.normalizePinList(pins["preferredDevice"]);
|
||||
const pinIndex = pinnedList.indexOf(pairedDelegate.modelData.address);
|
||||
|
||||
@@ -418,7 +418,7 @@ Rectangle {
|
||||
delete pins["preferredDevice"];
|
||||
}
|
||||
|
||||
SettingsData.set("bluetoothDevicePins", pins);
|
||||
CacheData.set("bluetoothDevicePins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ Rectangle {
|
||||
|
||||
const pinKey = getScreenPinKey();
|
||||
if (pinKey.length > 0) {
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const pinnedDevice = pins[pinKey];
|
||||
if (pinnedDevice && pinnedDevice.length > 0) {
|
||||
const found = devices.find(d => d.name === pinnedDevice);
|
||||
@@ -85,12 +85,12 @@ Rectangle {
|
||||
}
|
||||
const pinKey = getScreenPinKey();
|
||||
if (pinKey.length > 0) {
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const existing = pins[pinKey];
|
||||
if (existing && existing !== deviceName) {
|
||||
const next = JSON.parse(JSON.stringify(pins));
|
||||
delete next[pinKey];
|
||||
SettingsData.set("brightnessDevicePins", next);
|
||||
CacheData.set("brightnessDevicePins", next);
|
||||
}
|
||||
}
|
||||
root.currentDeviceName = deviceName;
|
||||
@@ -106,7 +106,7 @@ Rectangle {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey || !deviceName)
|
||||
return false;
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
return pins[pinKey] === deviceName;
|
||||
}
|
||||
|
||||
@@ -114,13 +114,13 @@ Rectangle {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey || !deviceName)
|
||||
return;
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.brightnessDevicePins || {}));
|
||||
if (pins[pinKey] === deviceName) {
|
||||
delete pins[pinKey];
|
||||
} else {
|
||||
pins[pinKey] = deviceName;
|
||||
}
|
||||
SettingsData.set("brightnessDevicePins", pins);
|
||||
CacheData.set("brightnessDevicePins", pins);
|
||||
}
|
||||
|
||||
implicitHeight: {
|
||||
@@ -269,7 +269,7 @@ Rectangle {
|
||||
|
||||
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
|
||||
readonly property bool devicePinnedHere: {
|
||||
SettingsData.brightnessDevicePins;
|
||||
CacheData.brightnessDevicePins;
|
||||
return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getPinnedNetworks() {
|
||||
const pins = SettingsData.wifiNetworkPins || {};
|
||||
const pins = CacheData.wifiNetworkPins || {};
|
||||
return normalizePinList(pins["preferredWifi"]);
|
||||
}
|
||||
|
||||
@@ -710,7 +710,7 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
|
||||
let pinnedList = root.normalizePinList(pins["preferredWifi"]);
|
||||
const pinIndex = pinnedList.indexOf(modelData.ssid);
|
||||
|
||||
@@ -727,7 +727,7 @@ Rectangle {
|
||||
else
|
||||
delete pins["preferredWifi"];
|
||||
|
||||
SettingsData.set("wifiNetworkPins", pins);
|
||||
CacheData.set("wifiNetworkPins", pins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ Row {
|
||||
if (screenName && screenName.length > 0) {
|
||||
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const pinnedDevice = pins[pinKey];
|
||||
if (pinnedDevice && pinnedDevice.length > 0) {
|
||||
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
|
||||
|
||||
@@ -201,7 +201,16 @@ Item {
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: 4
|
||||
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
|
||||
shadowOffsetX: root.shadowOffsetX
|
||||
|
||||
@@ -29,6 +29,8 @@ Item {
|
||||
property var centerWidgets: []
|
||||
property int totalWidgets: 0
|
||||
property real totalSize: 0
|
||||
property real contentStart: 0
|
||||
property real contentSize: 0
|
||||
|
||||
function updateLayout() {
|
||||
if (SettingsData.centeringMode === "geometric") {
|
||||
@@ -36,6 +38,25 @@ Item {
|
||||
} else {
|
||||
applyIndexLayout();
|
||||
}
|
||||
updateContentExtent();
|
||||
}
|
||||
|
||||
function updateContentExtent() {
|
||||
if (centerWidgets.length === 0) {
|
||||
contentStart = 0;
|
||||
contentSize = 0;
|
||||
return;
|
||||
}
|
||||
let start = Infinity;
|
||||
let end = -Infinity;
|
||||
for (const widget of centerWidgets) {
|
||||
const pos = isVertical ? widget.y : widget.x;
|
||||
const size = isVertical ? widget.height : widget.width;
|
||||
start = Math.min(start, pos);
|
||||
end = Math.max(end, pos + size);
|
||||
}
|
||||
contentStart = start;
|
||||
contentSize = end - start;
|
||||
}
|
||||
|
||||
function applyGeometricLayout() {
|
||||
|
||||
@@ -833,16 +833,30 @@ PanelWindow {
|
||||
const pos = section.mapToItem(barWindow.contentItem, 0, 0);
|
||||
const implW = section.implicitWidth || 0;
|
||||
const implH = section.implicitHeight || 0;
|
||||
const contentSize = isCenter ? (section.contentSize || 0) : 0;
|
||||
|
||||
const offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
|
||||
const offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
|
||||
let offsetX = isCenter && !barWindow.isVertical ? (section.width - implW) / 2 : 0;
|
||||
let offsetY = !barWindow.isVertical ? (section.height - implH) / 2 : (isCenter ? (section.height - implH) / 2 : 0);
|
||||
let w = implW;
|
||||
let h = implH;
|
||||
|
||||
// index centering lays content out asymmetrically; use the real extent
|
||||
if (contentSize > 0) {
|
||||
if (barWindow.isVertical) {
|
||||
offsetY = section.contentStart;
|
||||
h = contentSize;
|
||||
} else {
|
||||
offsetX = section.contentStart;
|
||||
w = contentSize;
|
||||
}
|
||||
}
|
||||
|
||||
const edgePad = 2;
|
||||
return {
|
||||
"x": pos.x + offsetX - edgePad,
|
||||
"y": pos.y + offsetY - edgePad,
|
||||
"w": implW + edgePad * 2,
|
||||
"h": implH + edgePad * 2
|
||||
"w": w + edgePad * 2,
|
||||
"h": h + edgePad * 2
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ BasePill {
|
||||
const pinKey = getScreenPinKey();
|
||||
if (!pinKey)
|
||||
return "";
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
return pins[pinKey] || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -35,44 +35,42 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: (rx, ry) => {
|
||||
onRightClicked: {
|
||||
const screen = root.parentScreen || Screen;
|
||||
if (!screen)
|
||||
return;
|
||||
const globalPos = root.visualContent.mapToItem(null, 0, 0);
|
||||
|
||||
const isVertical = root.axis?.isVertical ?? false;
|
||||
const edge = root.axis?.edge ?? "top";
|
||||
const gap = Math.max(Theme.spacingXS, root.barSpacing ?? Theme.spacingXS);
|
||||
const barOffset = root.barThickness + root.barSpacing + gap;
|
||||
const localPos = root.visualContent.mapToItem(null, root.visualContent.width / 2, root.visualContent.height / 2);
|
||||
|
||||
let anchorX;
|
||||
let anchorY;
|
||||
let anchorEdge;
|
||||
if (isVertical) {
|
||||
anchorY = globalPos.y - (screen.y || 0) + root.visualContent.height / 2;
|
||||
if (edge === "left") {
|
||||
anchorX = barOffset;
|
||||
anchorEdge = "top";
|
||||
} else {
|
||||
anchorX = screen.width - barOffset;
|
||||
anchorEdge = "top";
|
||||
}
|
||||
anchorX = edge === "left" ? barOffset : screen.width - barOffset;
|
||||
anchorY = localPos.y;
|
||||
} else {
|
||||
anchorX = globalPos.x - (screen.x || 0) + root.visualContent.width / 2;
|
||||
if (edge === "bottom") {
|
||||
anchorY = screen.height - barOffset;
|
||||
anchorEdge = "bottom";
|
||||
} else {
|
||||
anchorY = barOffset;
|
||||
anchorEdge = "top";
|
||||
}
|
||||
anchorX = localPos.x;
|
||||
anchorY = edge === "bottom" ? screen.height - barOffset : barOffset;
|
||||
}
|
||||
|
||||
dndPopupLoader.active = true;
|
||||
const popup = dndPopupLoader.item;
|
||||
if (!popup)
|
||||
return;
|
||||
popup.showAt(anchorX, anchorY, screen, anchorEdge);
|
||||
|
||||
popup.showAt(anchorX, anchorY, isVertical, edge, screen);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
onPressed: mouse => {
|
||||
root.triggerRipple(this, mouse.x, mouse.y);
|
||||
SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -490,7 +490,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: activePlayer?.trackAlbum || ""
|
||||
text: MprisController.stableAlbum
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextSecondary
|
||||
width: parent.width
|
||||
|
||||
@@ -438,20 +438,55 @@ Variants {
|
||||
parent: dock.contentItem
|
||||
visible: false
|
||||
readonly property bool expanded: dock.reveal
|
||||
readonly property bool chrome: dock.usesConnectedFrameChrome
|
||||
readonly property bool atEndEdge: SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
readonly property real innerReach: borderThickness + (SettingsData.appsDockEnlargeOnHover ? animationHeadroom : 0)
|
||||
readonly property real bodyX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
||||
readonly property real bodyY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
||||
x: {
|
||||
const baseX = dockCore.x + dockMouseArea.x;
|
||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
if (chrome) {
|
||||
const baseX = dockCore.x + dockMouseArea.x;
|
||||
if (isVertical && atEndEdge)
|
||||
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
}
|
||||
if (!isVertical)
|
||||
return dockCore.x + dockMouseArea.x - (expanded ? borderThickness : 0);
|
||||
if (!atEndEdge)
|
||||
return 0;
|
||||
return expanded ? Math.min(bodyX - innerReach, dock.width - 1) : dock.width - 1;
|
||||
}
|
||||
y: {
|
||||
const baseY = dockCore.y + dockMouseArea.y;
|
||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
|
||||
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
|
||||
if (chrome) {
|
||||
const baseY = dockCore.y + dockMouseArea.y;
|
||||
if (!isVertical && atEndEdge)
|
||||
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
|
||||
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
|
||||
}
|
||||
if (isVertical)
|
||||
return dockCore.y + dockMouseArea.y - (expanded ? borderThickness : 0);
|
||||
if (!atEndEdge)
|
||||
return 0;
|
||||
return expanded ? Math.min(bodyY - innerReach, dock.height - 1) : dock.height - 1;
|
||||
}
|
||||
width: {
|
||||
if (chrome)
|
||||
return dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0);
|
||||
if (!isVertical)
|
||||
return dockMouseArea.width + (expanded ? borderThickness * 2 : 0);
|
||||
if (!expanded)
|
||||
return 1;
|
||||
return atEndEdge ? dock.width - x : Math.max(bodyX + dockBackground.width + innerReach, 1);
|
||||
}
|
||||
height: {
|
||||
if (chrome)
|
||||
return dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0);
|
||||
if (isVertical)
|
||||
return dockMouseArea.height + (expanded ? borderThickness * 2 : 0);
|
||||
if (!expanded)
|
||||
return 1;
|
||||
return atEndEdge ? dock.height - y : Math.max(bodyY + dockBackground.height + innerReach, 1);
|
||||
}
|
||||
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
|
||||
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
|
||||
@@ -1773,6 +1773,7 @@ Item {
|
||||
const idx = GreeterState.sessionList.indexOf(value);
|
||||
if (idx < 0)
|
||||
return;
|
||||
GreeterState.sessionManuallySelected = true;
|
||||
GreeterState.currentSessionIndex = idx;
|
||||
GreeterState.selectedSession = GreeterState.sessionExecs[idx];
|
||||
GreeterState.selectedSessionPath = GreeterState.sessionPaths[idx];
|
||||
@@ -1785,6 +1786,8 @@ Item {
|
||||
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
|
||||
|
||||
function finalizeSessionSelection() {
|
||||
if (GreeterState.sessionManuallySelected)
|
||||
return;
|
||||
if (GreeterState.sessionList.length === 0)
|
||||
return;
|
||||
if (!GreetdMemory.memoryReady)
|
||||
|
||||
@@ -21,6 +21,7 @@ Singleton {
|
||||
property var sessionPaths: []
|
||||
property var sessionDesktopIds: []
|
||||
property int currentSessionIndex: 0
|
||||
property bool sessionManuallySelected: false
|
||||
property var availableUsers: []
|
||||
property int selectedUserIndex: -1
|
||||
|
||||
|
||||
@@ -386,16 +386,20 @@ end)
|
||||
HYPRLAND_LUA_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
||||
TEMP_CONFIG=$(mktemp)
|
||||
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
|
||||
env = DMS_RUN_GREETER,1
|
||||
TEMP_CONFIG=$(mktemp --suffix=.lua)
|
||||
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
|
||||
hl.env("DMS_RUN_GREETER", "1")
|
||||
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
}
|
||||
hl.config({
|
||||
misc = {
|
||||
disable_hyprland_logo = true,
|
||||
},
|
||||
})
|
||||
|
||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
||||
HYPRLAND_EOF
|
||||
hl.on("hyprland.start", function()
|
||||
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
|
||||
end)
|
||||
HYPRLAND_LUA_EOF
|
||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||
else
|
||||
TEMP_CONFIG=$(mktemp)
|
||||
|
||||
@@ -10,6 +10,7 @@ PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
property bool _completed: false
|
||||
|
||||
signal fadeCompleted
|
||||
signal fadeCancelled
|
||||
@@ -36,7 +37,8 @@ PanelWindow {
|
||||
opacity: 0
|
||||
|
||||
onOpacityChanged: {
|
||||
if (opacity >= 0.99 && root.active) {
|
||||
if (opacity >= 0.99 && root.active && !root._completed) {
|
||||
root._completed = true;
|
||||
root.fadeCompleted();
|
||||
}
|
||||
}
|
||||
@@ -59,6 +61,7 @@ PanelWindow {
|
||||
function startFade() {
|
||||
if (!SettingsData.fadeToDpmsEnabled)
|
||||
return;
|
||||
_completed = false;
|
||||
active = true;
|
||||
fadeOverlay.opacity = 0.0;
|
||||
fadeSeq.stop();
|
||||
@@ -74,6 +77,7 @@ PanelWindow {
|
||||
fadeSeq.stop();
|
||||
fadeOverlay.opacity = 0.0;
|
||||
active = false;
|
||||
_completed = false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -27,11 +27,14 @@ Item {
|
||||
function hide() {
|
||||
if (isKeyboardActive && keyboard !== null) {
|
||||
keyboard.destroy();
|
||||
keyboard = null;
|
||||
isKeyboardActive = false;
|
||||
} else
|
||||
log.debug("The keyboard is already hidden");
|
||||
}
|
||||
|
||||
Component.onDestruction: hide()
|
||||
|
||||
// private
|
||||
property Item keyboard: null
|
||||
Component {
|
||||
|
||||
@@ -35,6 +35,7 @@ Item {
|
||||
property int hyprlandLayoutCount: 0
|
||||
property bool lockerReadySent: false
|
||||
property bool lockerReadyArmed: false
|
||||
property var sessionLock: null
|
||||
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
|
||||
readonly property string lockFontFamily: SettingsData.lockScreenFontFamily
|
||||
|
||||
@@ -111,6 +112,8 @@ Item {
|
||||
}
|
||||
|
||||
function sendLockerReadyOnce() {
|
||||
if (root.demoMode)
|
||||
return;
|
||||
if (lockerReadySent)
|
||||
return;
|
||||
if (root.unlocking)
|
||||
@@ -133,12 +136,27 @@ Item {
|
||||
return;
|
||||
if (!root.visible || root.opacity <= 0)
|
||||
return;
|
||||
// Don't report ready until the compositor has confirmed the session is
|
||||
// locked (ext-session-lock `locked` event). Qt's afterRendering fires
|
||||
// before the lock surface is committed/presented, so releasing the sleep
|
||||
// inhibitor on it lets the machine freeze with the desktop still on screen,
|
||||
// which then flashes on resume. secure=true guarantees the desktop is hidden.
|
||||
if (root.sessionLock && !root.sessionLock.secure)
|
||||
return;
|
||||
Qt.callLater(() => {
|
||||
if (root.visible && root.opacity > 0 && !root.unlocking)
|
||||
sendLockerReadyOnce();
|
||||
});
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.sessionLock
|
||||
enabled: target !== null
|
||||
function onSecureChanged() {
|
||||
root.maybeSend();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.Window.window
|
||||
enabled: target !== null
|
||||
@@ -778,7 +796,7 @@ Item {
|
||||
return "passkey";
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
return "fingerprint_off";
|
||||
if (pam.fprint.active)
|
||||
if (pam.fprint.active || pam.fprint.retrying)
|
||||
return "fingerprint";
|
||||
if (pam.u2f.active)
|
||||
return "passkey";
|
||||
|
||||
@@ -33,6 +33,7 @@ FocusScope {
|
||||
|
||||
anchors.fill: parent
|
||||
demoMode: false
|
||||
sessionLock: root.lock
|
||||
pam: root.pam
|
||||
passwordBuffer: root.sharedPasswordBuffer
|
||||
screenName: root.screenName
|
||||
|
||||
@@ -262,9 +262,11 @@ Scope {
|
||||
property bool available: SettingsData.lockFingerprintReady
|
||||
property int tries
|
||||
property int errorTries
|
||||
property bool retrying: false
|
||||
|
||||
function checkAvail(): void {
|
||||
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
|
||||
retrying = false;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
@@ -273,6 +275,7 @@ Scope {
|
||||
|
||||
tries = 0;
|
||||
errorTries = 0;
|
||||
retrying = false;
|
||||
start();
|
||||
}
|
||||
|
||||
@@ -285,6 +288,7 @@ Scope {
|
||||
|
||||
switch (res) {
|
||||
case PamResult.Success:
|
||||
retrying = false;
|
||||
if (!root.unlockInProgress) {
|
||||
passwd.abort();
|
||||
root.proceedAfterPrimaryAuth();
|
||||
@@ -293,13 +297,16 @@ Scope {
|
||||
case PamResult.Error:
|
||||
errorTries++;
|
||||
if (errorTries < 200) {
|
||||
retrying = true;
|
||||
abort();
|
||||
errorRetry.restart();
|
||||
return;
|
||||
}
|
||||
retrying = false;
|
||||
abort();
|
||||
return;
|
||||
case PamResult.MaxTries:
|
||||
retrying = false;
|
||||
tries++;
|
||||
if (tries < SettingsData.maxFprintTries) {
|
||||
root.fprintState = "fail";
|
||||
@@ -418,7 +425,7 @@ Scope {
|
||||
Timer {
|
||||
id: errorRetry
|
||||
|
||||
interval: 1500
|
||||
interval: Math.min(1500 * Math.pow(2, Math.max(0, fprint.errorTries - 1)), 30000)
|
||||
onTriggered: fprint.start()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,16 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property point anchorPos: Qt.point(0, 0)
|
||||
property string anchorEdge: "top"
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
visible: false
|
||||
|
||||
function showAt(x, y, targetScreen, edge) {
|
||||
function showAt(x, y, vertical, barEdge, targetScreen) {
|
||||
if (targetScreen)
|
||||
root.screen = targetScreen;
|
||||
anchorPos = Qt.point(x, y);
|
||||
anchorEdge = edge || "top";
|
||||
isVertical = vertical ?? false;
|
||||
edge = barEdge ?? "top";
|
||||
visible = true;
|
||||
}
|
||||
|
||||
@@ -66,21 +68,19 @@ PanelWindow {
|
||||
visible: root.visible
|
||||
|
||||
x: {
|
||||
const left = 10;
|
||||
const right = root.width - width - 10;
|
||||
const want = root.anchorPos.x - width / 2;
|
||||
return Math.max(left, Math.min(right, want));
|
||||
if (root.isVertical) {
|
||||
if (root.edge === "left")
|
||||
return Math.min(root.width - width - 10, root.anchorPos.x);
|
||||
return Math.max(10, root.anchorPos.x - width);
|
||||
}
|
||||
return Math.max(10, Math.min(root.width - width - 10, root.anchorPos.x - width / 2));
|
||||
}
|
||||
y: {
|
||||
switch (root.anchorEdge) {
|
||||
case "bottom":
|
||||
return Math.max(10, root.anchorPos.y - height);
|
||||
case "left":
|
||||
case "right":
|
||||
if (root.isVertical)
|
||||
return Math.max(10, Math.min(root.height - height - 10, root.anchorPos.y - height / 2));
|
||||
default:
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y);
|
||||
}
|
||||
if (root.edge === "bottom")
|
||||
return Math.max(10, root.anchorPos.y - height);
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y);
|
||||
}
|
||||
|
||||
onDismissed: root.closeMenu()
|
||||
|
||||
@@ -45,6 +45,14 @@ PanelWindow {
|
||||
blurWidth: !win._finalized && !win.connectedFrameMode ? innerW * s : 0
|
||||
blurHeight: !win._finalized && !win.connectedFrameMode ? innerH * s : 0
|
||||
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
// Slide-out translates the card past the surface edge; intersecting with the
|
||||
// resting bounds keeps the committed region on the visible content instead of
|
||||
// leaving a stray blur sliver at the edge (#2917).
|
||||
clipEnabled: true
|
||||
clipX: content.x + content.cardInset
|
||||
clipY: content.y + content.cardInset
|
||||
clipWidth: innerW
|
||||
clipHeight: innerH
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:notification-popup"
|
||||
|
||||
@@ -156,9 +156,10 @@ DankOSD {
|
||||
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||
return;
|
||||
|
||||
const newTitle = player.trackTitle || "";
|
||||
const newArtist = player.trackArtist || "";
|
||||
const newAlbum = player.trackAlbum || "";
|
||||
const metaPlayer = MprisController.bestMetadataPlayer(player);
|
||||
const newTitle = MprisController.displayTrackTitle(metaPlayer);
|
||||
const newArtist = metaPlayer.trackArtist || "";
|
||||
const newAlbum = metaPlayer.trackAlbum || "";
|
||||
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
||||
|
||||
root._displayTitle = newTitle;
|
||||
@@ -263,37 +264,102 @@ DankOSD {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
Row {
|
||||
id: transportControls
|
||||
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root._displayIcon
|
||||
size: Theme.iconSize
|
||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
Rectangle {
|
||||
width: Theme.iconSize - 4
|
||||
height: Theme.iconSize - 4
|
||||
radius: (Theme.iconSize - 4) / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_previous"
|
||||
size: Theme.iconSize - 10
|
||||
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: root.player?.canGoPrevious ?? false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MprisController.previousOrRewind();
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playPauseButton
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
togglePlaying();
|
||||
root.hide();
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root._displayIcon
|
||||
size: Theme.iconSize
|
||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
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 {
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
x: parent.gap * 2 + transportControls.width
|
||||
width: parent.width - transportControls.width - parent.gap * 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
|
||||
@@ -375,6 +375,8 @@ Item {
|
||||
visible: DesktopService.autostartAvailable
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "autostartAddEntry"
|
||||
tags: ["autostart", "add", "entry", "command", "startup"]
|
||||
width: parent.width
|
||||
iconName: "add_circle"
|
||||
title: I18n.tr("Add Entry")
|
||||
@@ -764,6 +766,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "autostartTrayIconFix"
|
||||
tags: ["tray", "icons", "fix", "systemd"]
|
||||
width: parent.width
|
||||
iconName: "handyman"
|
||||
title: I18n.tr("Tray Icon Fix")
|
||||
|
||||
@@ -463,6 +463,7 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "barEnable"
|
||||
iconName: selectedBarConfig?.enabled ? "visibility" : "visibility_off"
|
||||
title: I18n.tr("Enable Bar")
|
||||
visible: !dankBarTab.appearanceOnly && selectedBarId !== "default"
|
||||
@@ -651,6 +652,8 @@ Item {
|
||||
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barAutoHide"
|
||||
tags: ["autohide", "auto-hide", "reveal", "intellihide"]
|
||||
text: I18n.tr("Auto-hide")
|
||||
description: I18n.tr("Automatically hide the bar when the pointer moves away")
|
||||
checked: selectedBarConfig?.autoHide ?? false
|
||||
@@ -700,6 +703,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barAutoHideStrict"
|
||||
tags: ["autohide", "strict", "popout"]
|
||||
width: parent.width - parent.leftPadding
|
||||
text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open")
|
||||
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
|
||||
@@ -713,6 +718,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barHideWhenWindowsOpen"
|
||||
tags: ["hide", "windows", "empty", "workspace"]
|
||||
width: parent.width - parent.leftPadding
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
text: I18n.tr("Hide When Windows Open")
|
||||
@@ -734,6 +741,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barManualVisibility"
|
||||
tags: ["manual", "show", "hide", "ipc", "toggle"]
|
||||
text: I18n.tr("Manual Show/Hide")
|
||||
description: I18n.tr("Toggle bar visibility manually via IPC")
|
||||
checked: selectedBarConfig?.visible ?? true
|
||||
@@ -753,6 +762,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barClickThrough"
|
||||
tags: ["clickthrough", "click", "through", "mouse", "input", "mask", "passthrough"]
|
||||
text: I18n.tr("Click Through")
|
||||
description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
|
||||
checked: selectedBarConfig?.clickThrough ?? false
|
||||
@@ -910,6 +921,8 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
id: exclusiveZoneSlider
|
||||
settingKey: "barExclusiveZone"
|
||||
tags: ["exclusive", "zone", "reserved", "offset"]
|
||||
visible: !SettingsData.frameEnabled
|
||||
text: I18n.tr("Exclusive Zone Offset")
|
||||
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
|
||||
@@ -933,6 +946,8 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
id: sizeSlider
|
||||
settingKey: "barSize"
|
||||
tags: ["size", "thickness", "height", "inner"]
|
||||
visible: !SettingsData.frameEnabled
|
||||
text: I18n.tr("Size")
|
||||
description: I18n.tr("Adjust the bar height via inner padding")
|
||||
@@ -1154,6 +1169,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barSquareCorners"
|
||||
tags: ["square", "corners", "rounding"]
|
||||
text: I18n.tr("Square Corners")
|
||||
description: I18n.tr("Remove corner rounding from the bar")
|
||||
visible: !SettingsData.frameEnabled
|
||||
@@ -1164,6 +1181,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barNoBackground"
|
||||
tags: ["transparent", "background", "invisible"]
|
||||
text: I18n.tr("No Background")
|
||||
description: I18n.tr("Make the bar background fully transparent")
|
||||
visible: !SettingsData.frameEnabled
|
||||
@@ -1174,6 +1193,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barMaximizeWidgetIcons"
|
||||
tags: ["maximize", "icons", "stretch"]
|
||||
text: I18n.tr("Maximize Widget Icons")
|
||||
description: I18n.tr("Stretch widget icons to fill the available bar height")
|
||||
checked: selectedBarConfig?.maximizeWidgetIcons ?? false
|
||||
@@ -1183,6 +1204,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barMaximizeWidgetText"
|
||||
tags: ["maximize", "text", "stretch"]
|
||||
text: I18n.tr("Maximize Widget Text")
|
||||
description: I18n.tr("Stretch widget text to fill the available bar height")
|
||||
checked: selectedBarConfig?.maximizeWidgetText ?? false
|
||||
@@ -1192,6 +1215,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barRemoveWidgetPadding"
|
||||
tags: ["padding", "compact", "widgets"]
|
||||
text: I18n.tr("Remove Widget Padding")
|
||||
description: I18n.tr("Remove inner padding from all widgets")
|
||||
checked: selectedBarConfig?.removeWidgetPadding ?? false
|
||||
@@ -1208,6 +1233,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barGothCorners"
|
||||
tags: ["goth", "corners", "concave", "cutout"]
|
||||
text: I18n.tr("Goth Corners")
|
||||
description: I18n.tr("Apply inverse concave corner cutouts to the bar")
|
||||
visible: !SettingsData.frameEnabled
|
||||
@@ -1302,6 +1329,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleCard {
|
||||
settingKey: "barMaximizeDetection"
|
||||
tags: ["maximize", "gaps", "border", "fullscreen"]
|
||||
iconName: "fit_screen"
|
||||
title: I18n.tr("Maximize Detection")
|
||||
description: I18n.tr("Remove gaps and border when windows are maximized")
|
||||
@@ -1847,6 +1876,8 @@ Item {
|
||||
|
||||
SettingsToggleCard {
|
||||
iconName: "mouse"
|
||||
settingKey: "barScrollWheel"
|
||||
tags: ["scroll", "wheel", "workspace", "axis"]
|
||||
title: I18n.tr("Scroll Wheel")
|
||||
description: I18n.tr("Control workspaces and columns by scrolling on the bar")
|
||||
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
|
||||
|
||||
@@ -267,6 +267,8 @@ Item {
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "defaultAppsInternet"
|
||||
tags: ["browser", "mail", "email", "web"]
|
||||
title: I18n.tr("Internet", "Internet")
|
||||
iconName: "public"
|
||||
|
||||
@@ -293,6 +295,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "defaultAppsUtilities"
|
||||
tags: ["file", "manager", "terminal", "editor"]
|
||||
title: I18n.tr("Utilities", "Utilities")
|
||||
iconName: "terminal"
|
||||
|
||||
@@ -317,6 +321,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "defaultAppsDocuments"
|
||||
tags: ["pdf", "text", "reader", "office"]
|
||||
title: I18n.tr("Documents", "Documents")
|
||||
iconName: "edit_document"
|
||||
|
||||
@@ -335,6 +341,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "defaultAppsMultimedia"
|
||||
tags: ["image", "video", "music", "viewer", "player"]
|
||||
title: I18n.tr("Multimedia", "Multimedia")
|
||||
iconName: "movie"
|
||||
AppSelector {
|
||||
|
||||
@@ -166,6 +166,8 @@ Item {
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "desktopWidgetsManage"
|
||||
tags: ["desktop", "widgets", "clock", "conky"]
|
||||
width: parent.width
|
||||
iconName: "widgets"
|
||||
title: I18n.tr("Desktop Widgets")
|
||||
@@ -203,6 +205,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
settingKey: "desktopWidgetGroups"
|
||||
tags: ["groups", "profiles", "layouts"]
|
||||
width: parent.width
|
||||
iconName: "folder"
|
||||
title: I18n.tr("Groups")
|
||||
|
||||
@@ -17,6 +17,7 @@ Singleton {
|
||||
readonly property var wlrOutputs: WlrOutputService.outputs
|
||||
property var outputs: ({})
|
||||
property var savedOutputs: ({})
|
||||
property var savedParsedOutputs: ({})
|
||||
property var allOutputs: buildAllOutputsMap()
|
||||
|
||||
property var includeStatus: ({
|
||||
@@ -911,15 +912,18 @@ Singleton {
|
||||
const paths = getConfigPaths();
|
||||
if (!paths) {
|
||||
savedOutputs = {};
|
||||
savedParsedOutputs = {};
|
||||
return;
|
||||
}
|
||||
|
||||
Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => {
|
||||
if (exitCode !== 0 || !content.trim()) {
|
||||
savedOutputs = {};
|
||||
savedParsedOutputs = {};
|
||||
return;
|
||||
}
|
||||
const parsed = parseOutputsConfig(content);
|
||||
savedParsedOutputs = parsed;
|
||||
const filtered = filterDisconnectedOnly(parsed);
|
||||
savedOutputs = filtered;
|
||||
|
||||
@@ -1084,28 +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) {
|
||||
const result = {};
|
||||
const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g;
|
||||
let match;
|
||||
while ((match = outputRegex.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const body = match[2];
|
||||
|
||||
if (body.trim() === "off") {
|
||||
result[name] = {
|
||||
"name": name,
|
||||
"disabled": true,
|
||||
"logical": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"scale": 1.0,
|
||||
"transform": "Normal"
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
for (const block of extractNiriOutputBlocks(content)) {
|
||||
const name = block.name;
|
||||
const body = block.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 posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
|
||||
const scaleMatch = body.match(/scale\s+([\d.]+)/);
|
||||
@@ -1115,6 +1141,7 @@ Singleton {
|
||||
|
||||
result[name] = {
|
||||
"name": name,
|
||||
"disabled": disabled,
|
||||
"logical": {
|
||||
"x": posMatch ? parseInt(posMatch[1]) : 0,
|
||||
"y": posMatch ? parseInt(posMatch[2]) : 0,
|
||||
@@ -1778,6 +1805,41 @@ Singleton {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function findSavedParsedOutput(outputName) {
|
||||
const output = outputs[outputName];
|
||||
const candidates = [outputName];
|
||||
if (output?.make && output?.model) {
|
||||
candidates.push(output.make + " " + output.model + " " + (output.serial || "Unknown"));
|
||||
candidates.push(output.make + " " + output.model);
|
||||
}
|
||||
for (const savedName in savedParsedOutputs) {
|
||||
if (candidates.includes(savedName.trim()))
|
||||
return savedParsedOutputs[savedName];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// A disabled wlr head reports no current mode, scale 0 and position 0,0 —
|
||||
// overlay the last written config so rewrites don't discard real settings.
|
||||
function overlaySavedDisabledOutput(entry, outputName) {
|
||||
const saved = findSavedParsedOutput(outputName);
|
||||
if (!saved)
|
||||
return;
|
||||
|
||||
const savedMode = saved.modes?.[saved.current_mode ?? 0];
|
||||
if (savedMode && !entry.configured_mode)
|
||||
entry.configured_mode = savedMode.width + "x" + savedMode.height + "@" + (savedMode.refresh_rate / 1000).toFixed(3);
|
||||
if (saved.vrr_enabled !== undefined)
|
||||
entry.vrr_enabled = saved.vrr_enabled;
|
||||
if (!saved.logical || !entry.logical)
|
||||
return;
|
||||
|
||||
entry.logical.x = saved.logical.x;
|
||||
entry.logical.y = saved.logical.y;
|
||||
entry.logical.scale = saved.logical.scale;
|
||||
entry.logical.transform = saved.logical.transform;
|
||||
}
|
||||
|
||||
function buildOutputsWithPendingChanges() {
|
||||
const result = {};
|
||||
|
||||
@@ -1787,7 +1849,10 @@ Singleton {
|
||||
}
|
||||
|
||||
for (const outputName in outputs) {
|
||||
result[outputName] = JSON.parse(JSON.stringify(outputs[outputName]));
|
||||
const entry = JSON.parse(JSON.stringify(outputs[outputName]));
|
||||
if (entry.enabled === false)
|
||||
overlaySavedDisabledOutput(entry, outputName);
|
||||
result[outputName] = entry;
|
||||
}
|
||||
|
||||
for (const outputName in pendingChanges) {
|
||||
@@ -1799,6 +1864,8 @@ Singleton {
|
||||
result[outputName].logical.y = changes.position.y;
|
||||
}
|
||||
if (changes.mode !== undefined && result[outputName].modes) {
|
||||
if (result[outputName].configured_mode)
|
||||
result[outputName].configured_mode = changes.mode;
|
||||
for (var i = 0; i < result[outputName].modes.length; i++) {
|
||||
if (formatMode(result[outputName].modes[i]) === changes.mode) {
|
||||
result[outputName].current_mode = i;
|
||||
|
||||
@@ -618,6 +618,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "dockExclusiveZone"
|
||||
tags: ["exclusive", "zone", "reserved", "offset"]
|
||||
text: I18n.tr("Exclusive Zone Offset")
|
||||
visible: !root.connectedFrameModeActive || root.connectedPersistentDockActive
|
||||
value: SettingsData.dockBottomGap
|
||||
|
||||
@@ -38,6 +38,8 @@ Item {
|
||||
settingKey: "mediaPlayer"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "mediaWaveProgress"
|
||||
tags: ["wave", "progress", "animated"]
|
||||
text: I18n.tr("Wave Progress Bars")
|
||||
description: I18n.tr("Use animated wave progress bars for media playback")
|
||||
checked: SettingsData.waveProgressEnabled
|
||||
@@ -45,6 +47,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "mediaScrollTitle"
|
||||
tags: ["scroll", "title", "marquee"]
|
||||
text: I18n.tr("Scroll song title")
|
||||
description: I18n.tr("Scroll title if it doesn't fit in widget")
|
||||
checked: SettingsData.scrollTitleEnabled
|
||||
@@ -52,6 +56,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "mediaVisualizer"
|
||||
tags: ["visualizer", "cava", "spectrum"]
|
||||
text: I18n.tr("Audio Visualizer")
|
||||
description: I18n.tr("Show cava audio visualizer in media widget")
|
||||
checked: SettingsData.audioVisualizerEnabled
|
||||
@@ -59,6 +65,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "mediaAdaptiveWidth"
|
||||
tags: ["adaptive", "width", "shrink"]
|
||||
text: I18n.tr("Adaptive Media Width")
|
||||
description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size")
|
||||
checked: SettingsData.mediaAdaptiveWidthEnabled
|
||||
@@ -66,6 +74,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "mediaAlbumArtAccent"
|
||||
tags: ["album", "art", "accent", "colors"]
|
||||
text: I18n.tr("Use album art accent")
|
||||
description: I18n.tr("Use colors extracted from album art instead of system theme colors")
|
||||
checked: SettingsData.mediaUseAlbumArtAccent
|
||||
@@ -136,6 +146,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "mediaDeviceScrollVolume"
|
||||
tags: ["device", "scroll", "volume"]
|
||||
text: I18n.tr("Device list scroll volume")
|
||||
description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list")
|
||||
checked: SettingsData.audioDeviceScrollVolumeEnabled
|
||||
|
||||
@@ -81,6 +81,7 @@ Item {
|
||||
SettingsCard {
|
||||
tab: "mux"
|
||||
tags: ["mux", "session", "filter", "exclude", "hide"]
|
||||
settingKey: "muxSessionFilter"
|
||||
title: I18n.tr("Session Filter")
|
||||
iconName: "filter_list"
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ Item {
|
||||
}
|
||||
|
||||
function getPinnedWifiNetworks() {
|
||||
const pins = SettingsData.wifiNetworkPins || {};
|
||||
const pins = CacheData.wifiNetworkPins || {};
|
||||
return normalizePinList(pins["preferredWifi"]);
|
||||
}
|
||||
|
||||
function toggleWifiPin(ssid) {
|
||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
||||
const pins = JSON.parse(JSON.stringify(CacheData.wifiNetworkPins || {}));
|
||||
let pinnedList = normalizePinList(pins["preferredWifi"]);
|
||||
const pinIndex = pinnedList.indexOf(ssid);
|
||||
|
||||
@@ -77,7 +77,7 @@ Item {
|
||||
else
|
||||
delete pins["preferredWifi"];
|
||||
|
||||
SettingsData.set("wifiNetworkPins", pins);
|
||||
CacheData.set("wifiNetworkPins", pins);
|
||||
}
|
||||
|
||||
property var forgetNetworkConfirm: ConfirmModal {}
|
||||
|
||||
@@ -400,8 +400,8 @@ Item {
|
||||
settingKey: "powerMenuDefaultAction"
|
||||
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
|
||||
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")]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "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", "softreboot"]
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout";
|
||||
@@ -465,6 +465,12 @@ Item {
|
||||
label: I18n.tr("Show Hibernate"),
|
||||
desc: I18n.tr("Only visible if hibernate is supported by your system"),
|
||||
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]
|
||||
text: modelData.label
|
||||
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)
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
|
||||
@@ -89,6 +89,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
settingKey: "systemUpdaterCheckInterval"
|
||||
tags: ["interval", "poll", "frequency"]
|
||||
text: I18n.tr("Check interval")
|
||||
description: I18n.tr("How often the server polls for new updates.")
|
||||
options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel])
|
||||
@@ -157,6 +159,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "systemUpdaterCheckOnStart"
|
||||
tags: ["startup", "check", "boot"]
|
||||
text: I18n.tr("Check on startup")
|
||||
description: I18n.tr("When enabled, checks updates on startup. When disabled, only the interval above or a manual refresh runs a check.")
|
||||
checked: SettingsData.updaterCheckOnStart
|
||||
@@ -164,6 +168,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "systemUpdaterFlatpak"
|
||||
tags: ["flatpak", "include"]
|
||||
text: I18n.tr("Include Flatpak updates")
|
||||
description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
|
||||
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
|
||||
@@ -172,6 +178,8 @@ Item {
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "systemUpdaterAUR"
|
||||
tags: ["aur", "paru", "yay"]
|
||||
text: I18n.tr("Include AUR updates")
|
||||
description: I18n.tr("Run paru/yay with AUR enabled when 'Update All' is clicked.")
|
||||
visible: (SystemUpdateService.backends || []).some(b => b.id === "paru" || b.id === "yay")
|
||||
@@ -328,6 +336,8 @@ Item {
|
||||
settingKey: "systemUpdaterAdvanced"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "systemUpdaterCustomCommand"
|
||||
tags: ["custom", "command", "terminal"]
|
||||
text: I18n.tr("Use Custom Command")
|
||||
description: I18n.tr("Open a terminal and run a custom command instead of the in-shell upgrade flow.")
|
||||
checked: SettingsData.updaterUseCustomCommand
|
||||
|
||||
@@ -167,7 +167,13 @@ FloatingWindow {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
model: root.filteredApps
|
||||
// Start with no model. It is (re)bound in show() so that each open
|
||||
// gets a fresh QQmlDelegateModel, avoiding a crash where a stale
|
||||
// incubation queue from a previous open races with a background
|
||||
// refreshApplications() replacing the underlying DesktopEntries
|
||||
// QObjectModel (SIGSEGV in QQmlIncubatorPrivate::incubate via
|
||||
// libQt6QmlModels). See hide()/onVisibleChanged() for the teardown.
|
||||
model: null
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
@@ -316,11 +322,21 @@ FloatingWindow {
|
||||
|
||||
function show() {
|
||||
updateFilteredApps();
|
||||
// Rebind the model reactively (search relies on filteredApps changes flowing
|
||||
// through), and do it after populating so the ListView starts incubating from
|
||||
// a fully-formed array rather than [].
|
||||
appList.model = Qt.binding(() => root.filteredApps);
|
||||
visible = true;
|
||||
Qt.callLater(() => searchField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function hide() {
|
||||
// Drop the model before clearing visible, so the ListView releases its
|
||||
// QQmlDelegateModel (and any in-flight incubators) synchronously on this
|
||||
// frame. This is the crux of the fix: a later show() will allocate a fresh
|
||||
// DelegateModel instead of reusing one whose incubation queue may hold
|
||||
// references invalidated by a concurrent refreshApplications().
|
||||
appList.model = null;
|
||||
visible = false;
|
||||
searchQuery = "";
|
||||
filteredApps = [];
|
||||
@@ -330,6 +346,9 @@ FloatingWindow {
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
// Guard against visibility being cleared without going through hide()
|
||||
// (e.g. window manager close, FloatingWindow.onClosed -> hide()).
|
||||
appList.model = null;
|
||||
searchQuery = "";
|
||||
filteredApps = [];
|
||||
selectedIndex = -1;
|
||||
|
||||
@@ -749,16 +749,24 @@ Singleton {
|
||||
if (category === I18n.tr("All"))
|
||||
return visibleApps;
|
||||
|
||||
const pluginItems = getPluginItems(category, "");
|
||||
if (pluginItems.length > 0)
|
||||
return pluginItems;
|
||||
|
||||
return visibleApps.filter(app => {
|
||||
const appCategories = getCategoriesForApp(app);
|
||||
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
|
||||
function getPluginCategories() {
|
||||
if (typeof PluginService === "undefined") {
|
||||
@@ -778,17 +786,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function getPluginCategoryIcon(category) {
|
||||
if (typeof PluginService === "undefined")
|
||||
const pluginId = getPluginIdForCategory(category);
|
||||
if (!pluginId)
|
||||
return null;
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId];
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return plugin.icon || "extension";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
|
||||
}
|
||||
|
||||
function getAllPluginItems() {
|
||||
@@ -809,17 +811,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function getPluginItems(category, query) {
|
||||
if (typeof PluginService === "undefined")
|
||||
const pluginId = getPluginIdForCategory(category);
|
||||
if (!pluginId)
|
||||
return [];
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId];
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return getPluginItemsForPlugin(pluginId, query);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
return getPluginItemsForPlugin(pluginId, query);
|
||||
}
|
||||
|
||||
function getPluginItemsForPlugin(pluginId, query) {
|
||||
|
||||
@@ -17,7 +17,29 @@ Singleton {
|
||||
interval: 500
|
||||
repeat: false
|
||||
running: true
|
||||
onTriggered: root.suppressSound = false
|
||||
onTriggered: {
|
||||
root.suppressSound = false;
|
||||
root.applyPowerProfile();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
|
||||
function onHasPerformanceProfileChanged() {
|
||||
root.applyPowerProfile();
|
||||
}
|
||||
}
|
||||
|
||||
function applyPowerProfile() {
|
||||
if (!batteryAvailable)
|
||||
return;
|
||||
const profileValue = isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName;
|
||||
if (profileValue === "")
|
||||
return;
|
||||
const targetProfile = parseInt(profileValue);
|
||||
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
||||
return;
|
||||
PowerProfileWatcher.applyProfile(targetProfile);
|
||||
}
|
||||
|
||||
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
||||
@@ -206,14 +228,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
const profileValue = BatteryService.isPluggedIn ? SettingsData.acProfileName : SettingsData.batteryProfileName;
|
||||
|
||||
if (profileValue !== "") {
|
||||
const targetProfile = parseInt(profileValue);
|
||||
if (!isNaN(targetProfile) && PowerProfiles.profile !== targetProfile) {
|
||||
PowerProfiles.profile = targetProfile;
|
||||
}
|
||||
}
|
||||
applyPowerProfile();
|
||||
|
||||
previousPluggedState = isPluggedIn;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ Item {
|
||||
property string socketPath: ""
|
||||
readonly property bool socketFound: socketPath.length > 0
|
||||
property bool connected: false
|
||||
property bool binaryExists: false
|
||||
readonly property bool binaryExists: launchKind.length > 0
|
||||
property bool binaryChecked: false
|
||||
// "dcal" for a host install, "flatpak" for a sandboxed one, "" for neither.
|
||||
property string launchKind: ""
|
||||
|
||||
property var calendars: []
|
||||
property var events: []
|
||||
@@ -55,18 +57,20 @@ Item {
|
||||
|
||||
Process {
|
||||
id: binaryCheck
|
||||
command: ["sh", "-c", "command -v dcal"]
|
||||
command: ["sh", "-c", "if command -v dcal >/dev/null 2>&1; then echo dcal; elif command -v flatpak >/dev/null 2>&1 && flatpak info com.danklinux.dankcalendar >/dev/null 2>&1; then echo flatpak; fi"]
|
||||
running: false
|
||||
onExited: code => {
|
||||
root.binaryExists = (code === 0);
|
||||
root.binaryChecked = true;
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.launchKind = text.trim();
|
||||
root.binaryChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: discoverProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"]
|
||||
command: ["sh", "-c", "s=\"${DANKCAL_SOCKET:-}\"; if [ -S \"$s\" ]; then echo \"$s\"; exit 0; fi; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/app/com.danklinux.dankcalendar/dankcal-*.sock; do if [ -S \"$f\" ]; then echo \"$f\"; exit 0; fi; done; for f in \"${XDG_RUNTIME_DIR:-/tmp}\"/dankcal-*.sock /tmp/dankcal-*.sock; do [ -S \"$f\" ] || continue; p=$(basename \"$f\" .sock); p=${p#dankcal-}; if kill -0 \"$p\" 2>/dev/null; then echo \"$f\"; exit 0; fi; done"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
@@ -98,9 +102,16 @@ Item {
|
||||
}
|
||||
|
||||
function launch() {
|
||||
if (!binaryExists)
|
||||
switch (launchKind) {
|
||||
case "dcal":
|
||||
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]);
|
||||
break;
|
||||
case "flatpak":
|
||||
Quickshell.execDetached(["flatpak", "run", "com.danklinux.dankcalendar", "run", "-d", "--hidden"]);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
Quickshell.execDetached(["dcal", "run", "-d", "--hidden"]);
|
||||
}
|
||||
if (enabled && !connected)
|
||||
discoverProcess.running = true;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ Singleton {
|
||||
return "wifi_1_bar";
|
||||
}
|
||||
|
||||
property string userPreference: "auto"
|
||||
readonly property string userPreference: SettingsData.networkPreference
|
||||
property bool isConnecting: false
|
||||
property string connectingSSID: ""
|
||||
property string connectionError: ""
|
||||
@@ -187,7 +187,6 @@ Singleton {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.userPreference = SettingsData.networkPreference;
|
||||
lastConnectedVpnUuid = SessionData.vpnLastConnected || "";
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
checkDMSCapabilities();
|
||||
@@ -672,7 +671,6 @@ Singleton {
|
||||
function setNetworkPreference(preference) {
|
||||
if (!networkAvailable)
|
||||
return;
|
||||
userPreference = preference;
|
||||
changingPreference = true;
|
||||
targetPreference = preference;
|
||||
SettingsData.set("networkPreference", preference);
|
||||
|
||||
@@ -11,7 +11,7 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DMSService")
|
||||
|
||||
property bool dmsAvailable: false
|
||||
readonly property bool dmsAvailable: isConnected
|
||||
property var capabilities: []
|
||||
property int apiVersion: 0
|
||||
property string cliVersion: ""
|
||||
@@ -21,7 +21,7 @@ Singleton {
|
||||
property var availableThemes: []
|
||||
property var installedThemes: []
|
||||
property bool isConnected: false
|
||||
property bool isConnecting: false
|
||||
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
|
||||
property bool subscribeConnected: false
|
||||
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
@@ -69,12 +69,13 @@ Singleton {
|
||||
property bool screensaverInhibited: false
|
||||
property var screensaverInhibitors: []
|
||||
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"]
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
detectUpdateCommand();
|
||||
}
|
||||
if (!socketPath || socketPath.length === 0)
|
||||
return;
|
||||
detectUpdateCommand();
|
||||
requestSocket.connected = true;
|
||||
}
|
||||
|
||||
function detectUpdateCommand() {
|
||||
@@ -82,12 +83,6 @@ Singleton {
|
||||
checkAurHelper.running = true;
|
||||
}
|
||||
|
||||
function startSocketConnection() {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
testProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkAurHelper
|
||||
command: ["sh", "-c", "command -v paru || command -v yay"]
|
||||
@@ -105,7 +100,6 @@ Singleton {
|
||||
} else {
|
||||
updateCommand = "dms update";
|
||||
checkingUpdateCommand = false;
|
||||
startSocketConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +108,6 @@ Singleton {
|
||||
if (exitCode !== 0) {
|
||||
updateCommand = "dms update";
|
||||
checkingUpdateCommand = false;
|
||||
startSocketConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +128,6 @@ Singleton {
|
||||
updateCommand = "dms update";
|
||||
}
|
||||
checkingUpdateCommand = false;
|
||||
startSocketConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,52 +135,27 @@ Singleton {
|
||||
if (exitCode !== 0) {
|
||||
updateCommand = "dms update";
|
||||
checkingUpdateCommand = false;
|
||||
startSocketConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: testProcess
|
||||
command: ["test", "-S", root.socketPath]
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.dmsAvailable = true;
|
||||
connectSocket();
|
||||
} else {
|
||||
root.dmsAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function connectSocket() {
|
||||
if (!dmsAvailable || isConnected || isConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
isConnecting = true;
|
||||
requestSocket.connected = true;
|
||||
}
|
||||
|
||||
DankSocket {
|
||||
id: requestSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected) {
|
||||
if (linkUp) {
|
||||
root.isConnected = true;
|
||||
root.isConnecting = false;
|
||||
root.connectionStateChanged();
|
||||
subscribeSocket.connected = true;
|
||||
} else {
|
||||
root.isConnected = false;
|
||||
root.isConnecting = false;
|
||||
root.apiVersion = 0;
|
||||
root.capabilities = [];
|
||||
root.connectionStateChanged();
|
||||
return;
|
||||
}
|
||||
root.isConnected = false;
|
||||
root.apiVersion = 0;
|
||||
root.capabilities = [];
|
||||
root.failPendingRequests();
|
||||
root.connectionStateChanged();
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
@@ -219,10 +186,10 @@ Singleton {
|
||||
connected: false
|
||||
|
||||
onConnectionStateChanged: {
|
||||
root.subscribeConnected = connected;
|
||||
if (connected) {
|
||||
sendSubscribeRequest();
|
||||
}
|
||||
root.subscribeConnected = linkUp;
|
||||
if (!linkUp)
|
||||
return;
|
||||
sendSubscribeRequest();
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
@@ -335,6 +302,7 @@ Singleton {
|
||||
const data = response.result.data;
|
||||
|
||||
if (service === "server") {
|
||||
const prevCapabilities = capabilities;
|
||||
apiVersion = data.apiVersion || 0;
|
||||
cliVersion = data.cliVersion || "";
|
||||
capabilities = data.capabilities || [];
|
||||
@@ -346,6 +314,12 @@ Singleton {
|
||||
}
|
||||
|
||||
capabilitiesReceived();
|
||||
|
||||
const capabilitiesChanged = prevCapabilities.length !== capabilities.length || capabilities.some(c => !prevCapabilities.includes(c));
|
||||
if (prevCapabilities.length > 0 && capabilitiesChanged) {
|
||||
log.info("Capabilities changed, resubscribing");
|
||||
subscribe(activeSubscriptions);
|
||||
}
|
||||
} else if (service === "network") {
|
||||
networkStateUpdate(data);
|
||||
} else if (service === "network.credentials") {
|
||||
@@ -439,10 +413,20 @@ Singleton {
|
||||
|
||||
function handleResponse(response) {
|
||||
const callback = pendingRequests[response.id];
|
||||
if (!callback)
|
||||
return;
|
||||
delete pendingRequests[response.id];
|
||||
callback(response);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
delete pendingRequests[response.id];
|
||||
callback(response);
|
||||
function failPendingRequests() {
|
||||
const pending = pendingRequests;
|
||||
pendingRequests = {};
|
||||
clipboardRequestIds = {};
|
||||
for (const id in pending) {
|
||||
pending[id]({
|
||||
"error": "not connected to DMS socket"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ Singleton {
|
||||
if (!focusedScreen)
|
||||
return "";
|
||||
|
||||
const pins = SettingsData.brightnessDevicePins || {};
|
||||
const pins = CacheData.brightnessDevicePins || {};
|
||||
const screenKey = SettingsData.getScreenDisplayName(focusedScreen);
|
||||
if (!screenKey)
|
||||
return "";
|
||||
@@ -1155,6 +1155,8 @@ Singleton {
|
||||
return "Temperature must be between 2500K and 6000K";
|
||||
|
||||
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);
|
||||
|
||||
if (root.nightModeEnabled) {
|
||||
@@ -1184,6 +1186,8 @@ Singleton {
|
||||
return "Temperature must be between 2500K and 6500K";
|
||||
|
||||
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);
|
||||
|
||||
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
|
||||
|
||||
@@ -3,10 +3,13 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool wantsLocation: SettingsData.weatherEnabled && SettingsData.useAutoLocation
|
||||
readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location")
|
||||
readonly property bool valid: latitude !== 0 || longitude !== 0
|
||||
|
||||
@@ -15,19 +18,46 @@ Singleton {
|
||||
|
||||
signal locationChanged(var data)
|
||||
|
||||
onLocationAvailableChanged: {
|
||||
if (locationAvailable && !valid)
|
||||
getState();
|
||||
onWantsLocationChanged: {
|
||||
if (wantsLocation) {
|
||||
ensureSubscription();
|
||||
} else if (DMSService.activeSubscriptions.includes("location")) {
|
||||
DMSService.removeSubscription("location");
|
||||
}
|
||||
}
|
||||
|
||||
onLocationAvailableChanged: ensureSubscription()
|
||||
|
||||
Component.onCompleted: ensureSubscription()
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onLocationStateUpdate(data) {
|
||||
if (!locationAvailable)
|
||||
return;
|
||||
handleStateUpdate(data);
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected)
|
||||
root.ensureSubscription();
|
||||
}
|
||||
|
||||
function onLocationStateUpdate(data) {
|
||||
if (!root.wantsLocation)
|
||||
return;
|
||||
root.handleStateUpdate(data);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSubscription() {
|
||||
if (!wantsLocation)
|
||||
return;
|
||||
if (!locationAvailable)
|
||||
return;
|
||||
if (DMSService.activeSubscriptions.includes("location"))
|
||||
return;
|
||||
if (DMSService.activeSubscriptions.includes("all"))
|
||||
return;
|
||||
|
||||
DMSService.addSubscription("location");
|
||||
if (!valid)
|
||||
getState();
|
||||
}
|
||||
|
||||
function handleStateUpdate(data) {
|
||||
@@ -42,6 +72,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function getState() {
|
||||
if (!wantsLocation)
|
||||
return;
|
||||
if (!locationAvailable)
|
||||
return;
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ Singleton {
|
||||
// Chromium can report blank metadata between tracks
|
||||
property string stableTitle: ""
|
||||
property string stableArtist: ""
|
||||
property string stableAlbum: ""
|
||||
|
||||
Connections {
|
||||
target: root.activePlayer
|
||||
@@ -59,6 +60,9 @@ Singleton {
|
||||
root._syncStableMeta();
|
||||
root._checkIdle();
|
||||
}
|
||||
function onTrackAlbumChanged() {
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onLengthChanged() {
|
||||
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
||||
root.activePlayerStableLength = root.activePlayer.length;
|
||||
@@ -72,8 +76,10 @@ Singleton {
|
||||
|
||||
onActivePlayerChanged: {
|
||||
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
||||
stableTitle = activePlayer?.trackTitle || "";
|
||||
stableArtist = activePlayer?.trackArtist || "";
|
||||
stableTitle = "";
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
_syncStableMeta();
|
||||
_checkIdle();
|
||||
}
|
||||
|
||||
@@ -82,14 +88,24 @@ Singleton {
|
||||
if (!p) {
|
||||
stableTitle = "";
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
return;
|
||||
}
|
||||
if (isFirefoxYoutubeHoverPreview(p))
|
||||
return;
|
||||
if (p.trackTitle)
|
||||
stableTitle = p.trackTitle;
|
||||
if (p.trackArtist)
|
||||
stableArtist = p.trackArtist;
|
||||
const metadataPlayer = bestMetadataPlayer(p);
|
||||
const nextTitle = displayTrackTitle(metadataPlayer);
|
||||
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
|
||||
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
|
||||
@@ -101,6 +117,7 @@ Singleton {
|
||||
return;
|
||||
root.stableTitle = "";
|
||||
root.stableArtist = "";
|
||||
root.stableAlbum = "";
|
||||
root._resolveActivePlayer();
|
||||
}
|
||||
}
|
||||
@@ -122,9 +139,30 @@ Singleton {
|
||||
delegate: Connections {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
ignoreUnknownSignals: true
|
||||
function onIsPlayingChanged() {
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackTitleChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackArtistChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackAlbumChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onMetadataChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,9 +171,101 @@ Singleton {
|
||||
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// A playing player always wins; otherwise keep the selection stable w/idle
|
||||
const playing = availablePlayers.find(p => p.isPlaying);
|
||||
const playing = _bestPlayingPlayer();
|
||||
if (playing) {
|
||||
if (activePlayer !== playing) {
|
||||
activePlayer = playing;
|
||||
|
||||
@@ -1487,8 +1487,6 @@ window-rule {
|
||||
|
||||
if (outputSettings.disabled) {
|
||||
kdlContent += ` off\n`;
|
||||
kdlContent += `}\n\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output.configured_mode) {
|
||||
|
||||
@@ -425,7 +425,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
settingsModal?.close();
|
||||
settingsModal?.hide();
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
|
||||
@@ -17,6 +17,7 @@ Singleton {
|
||||
property bool loginctlCommandAvailable: false
|
||||
property bool systemctlCommandAvailable: false
|
||||
property bool hibernateSupported: false
|
||||
readonly property bool softRebootSupported: systemctlCommandAvailable
|
||||
property bool inhibitorAvailable: true
|
||||
property bool idleInhibited: false
|
||||
property string inhibitReason: "Keep system awake"
|
||||
@@ -217,6 +218,64 @@ Singleton {
|
||||
return envObj;
|
||||
}
|
||||
|
||||
function splitShellArgs(str) {
|
||||
const args = [];
|
||||
let current = "";
|
||||
let hasToken = false;
|
||||
let quote = "";
|
||||
let escaped = false;
|
||||
for (const ch of str) {
|
||||
if (escaped) {
|
||||
current += ch;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
switch (quote) {
|
||||
case "'":
|
||||
if (ch === "'") {
|
||||
quote = "";
|
||||
continue;
|
||||
}
|
||||
current += ch;
|
||||
continue;
|
||||
case "\"":
|
||||
switch (ch) {
|
||||
case "\"":
|
||||
quote = "";
|
||||
continue;
|
||||
case "\\":
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
current += ch;
|
||||
continue;
|
||||
}
|
||||
switch (ch) {
|
||||
case "\\":
|
||||
escaped = true;
|
||||
continue;
|
||||
case "'":
|
||||
case "\"":
|
||||
quote = ch;
|
||||
hasToken = true;
|
||||
continue;
|
||||
case " ":
|
||||
case "\t":
|
||||
case "\n":
|
||||
if (!hasToken && current.length === 0)
|
||||
continue;
|
||||
args.push(current);
|
||||
current = "";
|
||||
hasToken = false;
|
||||
continue;
|
||||
}
|
||||
current += ch;
|
||||
}
|
||||
if (current.length > 0 || hasToken)
|
||||
args.push(current);
|
||||
return args;
|
||||
}
|
||||
|
||||
function launchDesktopEntry(desktopEntry, useNvidia) {
|
||||
if (!desktopEntry || !desktopEntry.command)
|
||||
return;
|
||||
@@ -230,8 +289,7 @@ Singleton {
|
||||
cmd = [nvidiaCommand].concat(cmd);
|
||||
|
||||
if (override?.extraFlags) {
|
||||
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
|
||||
cmd = cmd.concat(extraArgs);
|
||||
cmd = cmd.concat(splitShellArgs(override.extraFlags));
|
||||
}
|
||||
|
||||
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
||||
@@ -395,6 +453,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function softReboot() {
|
||||
Quickshell.execDetached(["systemctl", "soft-reboot"]);
|
||||
}
|
||||
|
||||
function poweroff() {
|
||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
||||
|
||||
@@ -26,7 +26,7 @@ Singleton {
|
||||
return hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
function getArtworkUrl(player) {
|
||||
function _directArtworkUrl(player) {
|
||||
if (!player) return "";
|
||||
|
||||
let artUrl = player.trackArtUrl || "";
|
||||
@@ -54,6 +54,17 @@ Singleton {
|
||||
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) {
|
||||
resolvedArtUrl = u;
|
||||
_committedArtKey = u !== "" ? artKey : "";
|
||||
@@ -171,11 +182,25 @@ Singleton {
|
||||
onActivePlayerChanged: _updateArtUrl()
|
||||
|
||||
Connections {
|
||||
target: root.activePlayer
|
||||
ignoreUnknownSignals: true
|
||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||
function onMetadataChanged() { root._updateArtUrl(); }
|
||||
target: MprisController
|
||||
function onAvailablePlayersChanged() {
|
||||
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() {
|
||||
@@ -201,8 +226,8 @@ Singleton {
|
||||
}
|
||||
_pendingArtKey = key;
|
||||
const url = getArtworkUrl(activePlayer);
|
||||
// Ignore Chrome's same-track thumbnail size updates.
|
||||
if (key !== "" && key === _committedArtKey)
|
||||
// Ignore duplicate notifications, but let a richer peer replace same-track art
|
||||
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
|
||||
return;
|
||||
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
||||
// Chrome can publish track metadata before its new artwork URL.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v1.5.2
|
||||
v1.5.3
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user