1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-02 02:22:06 -04:00

Compare commits

...

19 Commits

Author SHA1 Message Date
bbedward
c1cbd0994f settings: fix semvar signal moved to different service 2026-05-01 18:38:48 -04:00
bbedward
c81645bacb add pre-commit hook for console.log 2026-04-30 16:59:26 -04:00
Archit Arora
cdc4ca7e1f matugen: generate theme for Vencord (#2320) 2026-04-30 16:16:55 -04:00
gibbert
7d92842ff2 matugen: fix emacs template constant line number size (#2317)
Made it so line numbers don't stay a constant size when changing buffer
text scale.

See this thread:
<https://emacs.stackexchange.com/questions/74507/constant-font-size-in-display-line-numbers-mode-when-zooming-in-and-out>
2026-04-30 11:47:24 -04:00
Body
d8bf3bdfe8 processes: fix list gaps and overlap when searching (#2315) 2026-04-30 11:45:46 -04:00
David Mireles
23ed795e85 Fix VPN UI for active transient entries (#2312)
Co-authored-by: louzt <18044171+louzt@users.noreply.github.com>
2026-04-30 11:41:41 -04:00
bbedward
2877c63c97 system update: make refresh synchronous 2026-04-30 11:41:07 -04:00
bbedward
86096db26b system update: general fixes to flatpak parsing 2026-04-29 16:14:19 -04:00
bbedward
f76724f7cd logger: add a dedicated QML logging Singleton
- adds log.info/error/debug/warn/fatal
- adds ability to write logs to any file
- add CLI options in addition to env to set log levels
2026-04-29 15:42:30 -04:00
bbedward
3b96c6ab22 Revert "system updater: make all distros use terminal"
This reverts commit 1467f5dba9.
2026-04-29 14:56:54 -04:00
bbedward
1467f5dba9 system updater: make all distros use terminal 2026-04-29 14:41:24 -04:00
dms-ci[bot]
baaa30c94e nix: update vendorHash for go.mod changes 2026-04-29 16:42:28 +00:00
bbedward
24a3cd5a3d core: update go dependencies 2026-04-29 12:40:24 -04:00
bbedward
65151dbfd7 i18n: term sync 2026-04-29 12:39:32 -04:00
bbedward
7bd9574868 system updater: complete overhaul
Move system update flow to GO, with a CLI (convenient AIO tool) and
server integration. All lifecycle, scheduling, execution occurs on
backend side.

Run some backends via pkexec, some via terminal like paru/yay.

Incorporate flatpak as an option to update.

Add terminal override setting in GUI, in addition to $TERMINAL env
variable.

fixes #2307
fixes #822
fixes #1102
fixes #1812
fixes #1087
fixes #1743
2026-04-29 12:33:57 -04:00
purian23
a4cfdf4a59 (dms): Add input group to dms setup
- Suppress fix/warnings
2026-04-28 14:03:37 -04:00
bbedward
fd651dc943 niri overlay: fix state binding
fixes #2301
2026-04-28 13:19:34 -04:00
Kangheng Liu
919b09fc96 feat(desktop): expose screen var to desktop plugins (#2300) 2026-04-28 11:45:34 -04:00
bbedward
aeb3fdd637 osd(media): workaround for firefox reporting youtube thumbnails as
players
fixes #2298
2026-04-28 11:27:16 -04:00
157 changed files with 13346 additions and 4665 deletions

View File

@@ -20,3 +20,11 @@ repos:
language: system language: system
files: ^core/.*\.(go|mod|sum)$ files: ^core/.*\.(go|mod|sum)$
pass_filenames: false pass_filenames: false
- repo: local
hooks:
- id: no-console-in-qml
name: no console.* in QML (use Log service)
entry: bash -c 'if grep -nE "console\.(log|error|info|warn|debug)" "$@"; then echo "Use the Log service (log.info/warn/error/debug/fatal) instead of console.*" >&2; exit 1; fi' --
language: system
files: ^quickshell/.*\.qml$
exclude: ^quickshell/(Services/Log\.qml$|dms-plugins/|PLUGINS/)

View File

@@ -26,6 +26,17 @@ var runCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
daemon, _ := cmd.Flags().GetBool("daemon") daemon, _ := cmd.Flags().GetBool("daemon")
session, _ := cmd.Flags().GetBool("session") session, _ := cmd.Flags().GetBool("session")
if v, _ := cmd.Flags().GetString("log-level"); v != "" {
if err := os.Setenv("DMS_LOG_LEVEL", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_LEVEL: %v", err)
}
}
if v, _ := cmd.Flags().GetString("log-file"); v != "" {
if err := os.Setenv("DMS_LOG_FILE", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_FILE: %v", err)
}
}
log.ApplyEnvOverrides()
if daemon { if daemon {
runShellDaemon(session) runShellDaemon(session)
} else { } else {
@@ -527,5 +538,6 @@ func getCommonCommands() []*cobra.Command {
randrCmd, randrCmd,
blurCmd, blurCmd,
trashCmd, trashCmd,
systemCmd,
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -11,6 +12,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -267,6 +269,8 @@ func runSetupDmsConfig(name string) error {
func runSetup() error { func runSetup() error {
fmt.Println("=== DMS Configuration Setup ===") fmt.Println("=== DMS Configuration Setup ===")
ensureInputGroup()
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd() useSystemd := promptSystemd()
@@ -340,6 +344,37 @@ func runSetup() error {
return nil return nil
} }
// Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() {
if !utils.HasGroup("input") {
return
}
currentUser := os.Getenv("USER")
if currentUser == "" {
currentUser = os.Getenv("LOGNAME")
}
if currentUser == "" {
return
}
out, err := execGroups(currentUser)
if err == nil && strings.Contains(out, "input") {
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
return
}
fmt.Println("Adding user to input group for Caps Lock OSD support...")
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
} else {
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
}
}
func execGroups(user string) (string, error) {
out, err := exec.Command("groups", user).Output()
return string(out), err
}
func promptCompositor() (deps.WindowManager, bool) { func promptCompositor() (deps.WindowManager, bool) {
fmt.Println("Select compositor:") fmt.Println("Select compositor:")
fmt.Println("1) Niri") fmt.Println("1) Niri")

View File

@@ -0,0 +1,277 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/spf13/cobra"
)
var systemCmd = &cobra.Command{
Use: "system",
Short: "System operations",
Long: "System-level operations (updates, etc.). Runs against installed package managers directly; does not require the DMS server.",
}
var systemUpdateCmd = &cobra.Command{
Use: "update",
Short: "Apply or list system updates",
Long: `Apply or list system updates across detected package managers.
Default behavior is to apply available updates after prompting for confirmation.
Use --check to list updates without applying.
Examples:
dms system update --check # list available updates
dms system update # apply updates (interactive prompt)
dms system update --noconfirm # apply updates without prompting
dms system update --dry # simulate without changing anything
dms system update --no-flatpak --noconfirm # apply system updates only
dms system update --interval 3600 # set the server poll interval to 1h`,
Run: runSystemUpdate,
}
var (
sysUpdateCheck bool
sysUpdateNoConfirm bool
sysUpdateDry bool
sysUpdateJSON bool
sysUpdateNoFlatpak bool
sysUpdateNoAUR bool
sysUpdateIntervalS int
sysUpdateListPmTime = 5 * time.Minute
)
func init() {
systemUpdateCmd.Flags().BoolVar(&sysUpdateCheck, "check", false, "List available updates without applying")
systemUpdateCmd.Flags().BoolVarP(&sysUpdateNoConfirm, "noconfirm", "y", false, "Apply updates without prompting")
systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes")
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
systemCmd.AddCommand(systemUpdateCmd)
}
func runSystemUpdate(cmd *cobra.Command, args []string) {
switch {
case sysUpdateIntervalS >= 0:
runSystemUpdateSetInterval(sysUpdateIntervalS)
case sysUpdateCheck:
runSystemUpdateCheck()
default:
runSystemUpdateApply()
}
}
func selectBackends(ctx context.Context) []sysupdate.Backend {
sel := sysupdate.Select(ctx)
backends := sel.All()
if !sysUpdateNoFlatpak {
return backends
}
out := backends[:0]
for _, b := range backends {
if b.Repo() == sysupdate.RepoFlatpak {
continue
}
out = append(out, b)
}
return out
}
func runSystemUpdateCheck() {
ctx, cancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
defer cancel()
backends := selectBackends(ctx)
if len(backends) == 0 {
log.Fatal("No supported package manager found")
}
type backendResult struct {
ID string `json:"id"`
Display string `json:"displayName"`
Packages []sysupdate.Package `json:"packages"`
}
var results []backendResult
var allPkgs []sysupdate.Package
var firstErr error
for _, b := range backends {
pkgs, err := b.CheckUpdates(ctx)
if err != nil && firstErr == nil {
firstErr = fmt.Errorf("%s: %w", b.ID(), err)
}
results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: pkgs})
allPkgs = append(allPkgs, pkgs...)
}
if sysUpdateJSON {
out, _ := json.MarshalIndent(map[string]any{
"backends": results,
"packages": allPkgs,
"error": errOrEmpty(firstErr),
"count": len(allPkgs),
}, "", " ")
fmt.Println(string(out))
return
}
printBackends(backends)
fmt.Printf("Updates: %d\n", len(allPkgs))
if firstErr != nil {
fmt.Printf("Error: %v\n", firstErr)
}
if len(allPkgs) == 0 {
return
}
fmt.Println()
for _, p := range allPkgs {
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?"))
}
}
func runSystemUpdateApply() {
checkCtx, checkCancel := context.WithTimeout(context.Background(), sysUpdateListPmTime)
defer checkCancel()
backends := selectBackends(checkCtx)
if len(backends) == 0 {
log.Fatal("No supported package manager found")
}
pkgs, firstErr := collectUpdates(checkCtx, backends)
if firstErr != nil {
fmt.Printf("Warning: %v\n\n", firstErr)
}
printBackends(backends)
fmt.Printf("Updates: %d\n", len(pkgs))
if len(pkgs) == 0 {
fmt.Println("Nothing to upgrade.")
return
}
fmt.Println()
for _, p := range pkgs {
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?"))
}
fmt.Println()
if !sysUpdateNoConfirm && !sysUpdateDry {
if !promptYesNo("Proceed with upgrade? [y/N]: ") {
fmt.Println("Aborted.")
return
}
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
opts := sysupdate.UpgradeOptions{
IncludeFlatpak: !sysUpdateNoFlatpak,
IncludeAUR: !sysUpdateNoAUR,
DryRun: sysUpdateDry,
}
onLine := func(line string) { fmt.Println(line) }
for _, b := range backends {
fmt.Printf("\n== %s ==\n", b.DisplayName())
if err := b.Upgrade(ctx, opts, onLine); err != nil {
log.Fatalf("%s upgrade failed: %v", b.ID(), err)
}
}
if sysUpdateDry {
fmt.Println("\nDry run complete (no changes applied).")
return
}
fmt.Println("\nUpgrade complete.")
}
func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupdate.Package, error) {
var all []sysupdate.Package
var firstErr error
for _, b := range backends {
pkgs, err := b.CheckUpdates(ctx)
if err != nil && firstErr == nil {
firstErr = fmt.Errorf("%s: %w", b.ID(), err)
}
all = append(all, pkgs...)
}
return all, firstErr
}
func runSystemUpdateSetInterval(seconds int) {
resp, err := sendServerRequest(models.Request{
ID: 1,
Method: "sysupdate.setInterval",
Params: map[string]any{"seconds": float64(seconds)},
})
if err != nil {
log.Fatalf("Failed: %v (is dms server running?)", err)
}
if resp.Error != "" {
log.Fatalf("Error: %s", resp.Error)
}
fmt.Printf("Interval set to %d seconds.\n", seconds)
}
func promptYesNo(prompt string) bool {
if !stdinIsTTY() {
log.Fatal("Refusing to apply updates non-interactively. Re-run with --noconfirm or --check.")
}
fmt.Print(prompt)
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
return false
}
switch strings.ToLower(strings.TrimSpace(line)) {
case "y", "yes":
return true
default:
return false
}
}
func printBackends(backends []sysupdate.Backend) {
if len(backends) == 0 {
return
}
names := make([]string, 0, len(backends))
for _, b := range backends {
names = append(names, b.DisplayName())
}
fmt.Printf("Backends: %s\n", strings.Join(names, ", "))
}
func stdinIsTTY() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) != 0
}
func errOrEmpty(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func defaultIfEmpty(s, def string) string {
if s == "" {
return def
}
return s
}

View File

@@ -15,6 +15,8 @@ func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)

View File

@@ -15,6 +15,8 @@ func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)

View File

@@ -80,6 +80,16 @@ func getRuntimeDir() string {
return os.TempDir() return os.TempDir()
} }
func appendLogEnv(env []string) []string {
if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
env = append(env, "DMS_LOG_LEVEL="+v)
}
if v := os.Getenv("DMS_LOG_FILE"); v != "" {
env = append(env, "DMS_LOG_FILE="+v)
}
return env
}
func hasSystemdRun() bool { func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run") _, err := exec.LookPath("systemd-run")
return err == nil return err == nil
@@ -216,6 +226,8 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
} }
cmd.Env = appendLogEnv(cmd.Env)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -459,6 +471,8 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
} }
cmd.Env = appendLogEnv(cmd.Env)
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0) devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil { if err != nil {
log.Fatalf("Error opening /dev/null: %v", err) log.Fatalf("Error opening /dev/null: %v", err)

View File

@@ -6,11 +6,11 @@ toolchain go1.26.1
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.23.1 github.com/alecthomas/chroma/v2 v2.24.0
github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v1.0.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.2.2 github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
@@ -20,28 +20,27 @@ require (
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0 github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/image v0.36.0 golang.org/x/image v0.39.0
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect github.com/fogleman/gg v1.3.0 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect github.com/go-git/go-billy/v6 v6.0.0-20260424211911-732291493fb8 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
@@ -49,36 +48,37 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f github.com/go-git/go-git/v6 v6.0.0-alpha.2
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.41.0 golang.org/x/sys v0.43.0
golang.org/x/text v0.34.0 golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )

View File

@@ -1,14 +1,14 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U= github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg= github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.24.0 h1:zrg+k0tAaVbM8whaT2hR5DOUqAdopsDaH998EGi6Llk=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/chroma/v2 v2.24.0/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -24,22 +24,22 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
@@ -52,8 +52,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -66,12 +66,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs= github.com/go-git/go-billy/v6 v6.0.0-20260424211911-732291493fb8 h1:QRpwB1ans3fB3Cmeuog1ATzvXg/xhqubqiQi97xNO6E=
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= github.com/go-git/go-billy/v6 v6.0.0-20260424211911-732291493fb8/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s= github.com/go-git/go-git/v6 v6.0.0-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU= github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -79,8 +79,6 @@ github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -95,20 +93,20 @@ github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7Dmvb
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -125,8 +123,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E= github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
@@ -155,35 +153,33 @@ github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0= github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,12 +1,16 @@
package log package log
import ( import (
"io"
"os" "os"
"regexp"
"strings" "strings"
"sync" "sync"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
cblog "github.com/charmbracelet/log" cblog "github.com/charmbracelet/log"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
) )
// Logger embeds the Charm Logger and adds Printf/Fatalf // Logger embeds the Charm Logger and adds Printf/Fatalf
@@ -21,8 +25,26 @@ func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...)
var ( var (
logger *Logger logger *Logger
initLogger sync.Once initLogger sync.Once
logMu sync.Mutex
logFile *os.File
logStderr io.Writer = os.Stderr
ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
) )
// ansiStripWriter strips ANSI escape sequences before forwarding to w. Used
// for the file sink so colored stderr stays colored while the file stays plain.
type ansiStripWriter struct{ w io.Writer }
func (a *ansiStripWriter) Write(p []byte) (int, error) {
stripped := ansiRe.ReplaceAll(p, nil)
if _, err := a.w.Write(stripped); err != nil {
return 0, err
}
return len(p), nil
}
func parseLogLevel(level string) cblog.Level { func parseLogLevel(level string) cblog.Level {
switch strings.ToLower(level) { switch strings.ToLower(level) {
case "debug": case "debug":
@@ -86,7 +108,7 @@ func GetLogger() *Logger {
SetString(" DEBUG"). SetString(" DEBUG").
Foreground(lipgloss.Color("4")) Foreground(lipgloss.Color("4"))
base := cblog.New(os.Stderr) base := cblog.New(logStderr)
base.SetStyles(styles) base.SetStyles(styles)
base.SetReportTimestamp(false) base.SetReportTimestamp(false)
@@ -98,10 +120,85 @@ func GetLogger() *Logger {
base.SetPrefix(" go") base.SetPrefix(" go")
logger = &Logger{base} logger = &Logger{base}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
_ = SetLogFile(path)
}
}) })
return logger return logger
} }
// SetLevel updates the active log level. Accepts the same strings as
// DMS_LOG_LEVEL. Unknown values default to info.
func SetLevel(level string) {
GetLogger().SetLevel(parseLogLevel(level))
}
// SetLogFile makes the logger append to path in addition to stderr. Passing an
// empty string detaches the file sink. Atomic per-line writes (≤PIPE_BUF) on
// O_APPEND keep concurrent Go and QML writers from corrupting each other.
//
// Color handling: charmbracelet/log auto-detects color support from its
// io.Writer, and io.MultiWriter doesn't pass that through, so we force the ANSI
// profile when stderr is a TTY and route the file through ansiStripWriter so
// the file stays plain while stderr keeps its colors.
func SetLogFile(path string) error {
logMu.Lock()
defer logMu.Unlock()
if logFile != nil {
logFile.Close()
logFile = nil
}
l := GetLogger()
if path == "" {
l.SetOutput(logStderr)
applyColorProfile(l, logStderr)
return nil
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return err
}
logFile = f
out := io.MultiWriter(logStderr, &ansiStripWriter{w: f})
l.SetOutput(out)
applyColorProfile(l, logStderr)
return nil
}
// applyColorProfile forces the renderer's color profile to match what stderr
// would produce on its own, undoing the auto-downgrade triggered by wrapping
// stderr in a non-TTY writer (e.g. io.MultiWriter).
func applyColorProfile(l *Logger, stderr io.Writer) {
f, ok := stderr.(*os.File)
if !ok {
l.SetColorProfile(termenv.Ascii)
return
}
if isatty.IsTerminal(f.Fd()) {
l.SetColorProfile(termenv.ANSI)
return
}
l.SetColorProfile(termenv.Ascii)
}
// ApplyEnvOverrides re-reads DMS_LOG_LEVEL and DMS_LOG_FILE and reconfigures
// the singleton. Safe to call after CLI flags have rewritten the environment.
func ApplyEnvOverrides() {
GetLogger()
if level := os.Getenv("DMS_LOG_LEVEL"); level != "" {
SetLevel(level)
}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
if err := SetLogFile(path); err != nil {
Warnf("Failed to open log file %q: %v", path, err)
}
}
}
// * Convenience wrappers // * Convenience wrappers
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) } func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) }

View File

@@ -60,6 +60,7 @@ var templateRegistry = []TemplateDef{
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, {ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"}, {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigFile: "vencord.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"}, {ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal}, {ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},

View File

@@ -391,7 +391,7 @@ func (m *Manager) Close() {
func InitializeManager() (*Manager, error) { func InitializeManager() (*Manager, error) {
if os.Getuid() != 0 && !hasInputGroupAccess() { if os.Getuid() != 0 && !hasInputGroupAccess() {
return nil, fmt.Errorf("insufficient permissions to access input devices") return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
} }
return NewManager() return NewManager()

View File

@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
return false return false
} }
if reply != dbus.RequestNameReplyPrimaryOwner { if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name %s already owned by another process", name) log.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", name)
return false return false
} }
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil { if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {

View File

@@ -20,6 +20,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins" serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes" serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
@@ -202,6 +203,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "sysupdate.") {
if sysUpdateManager == nil {
models.RespondError(conn, req.ID, "sysupdate manager not initialized")
return
}
sysupdate.HandleRequest(conn, req, sysUpdateManager)
return
}
switch req.Method { switch req.Method {
case "ping": case "ping":
models.Respond(conn, req.ID, "pong") models.Respond(conn, req.ID, "pong")

View File

@@ -30,6 +30,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
@@ -75,6 +76,7 @@ var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var trayRecoveryManager *trayrecovery.Manager var trayRecoveryManager *trayrecovery.Manager
var locationManager *location.Manager var locationManager *location.Manager
var sysUpdateManager *sysupdate.Manager
var geoClientInstance geolocation.Client var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
@@ -421,6 +423,19 @@ func InitializeLocationManager(geoClient geolocation.Client) error {
return nil return nil
} }
func InitializeSysUpdateManager() error {
manager, err := sysupdate.NewManager()
if err != nil {
log.Warnf("Failed to initialize sysupdate manager: %v", err)
return err
}
sysUpdateManager = manager
log.Info("Sysupdate manager initialized")
return nil
}
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
@@ -506,6 +521,10 @@ func getCapabilities() Capabilities {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
if sysUpdateManager != nil {
caps = append(caps, "sysupdate")
}
return Capabilities{Capabilities: caps} return Capabilities{Capabilities: caps}
} }
@@ -576,6 +595,10 @@ func getServerInfo() ServerInfo {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
if sysUpdateManager != nil {
caps = append(caps, "sysupdate")
}
return ServerInfo{ return ServerInfo{
APIVersion: APIVersion, APIVersion: APIVersion,
CLIVersion: CLIVersion, CLIVersion: CLIVersion,
@@ -1243,6 +1266,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("sysupdate") && sysUpdateManager != nil {
wg.Add(1)
sysupdateChan := sysUpdateManager.Subscribe(clientID + "-sysupdate")
go func() {
defer wg.Done()
defer sysUpdateManager.Unsubscribe(clientID + "-sysupdate")
initialState := sysUpdateManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "sysupdate", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-sysupdateChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "sysupdate", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("dbus") && dbusManager != nil { if shouldSubscribe("dbus") && dbusManager != nil {
wg.Add(1) wg.Add(1)
dbusChan := dbusManager.SubscribeSignals(dbusClientID) dbusChan := dbusManager.SubscribeSignals(dbusClientID)
@@ -1348,6 +1403,9 @@ func cleanupManagers() {
if locationManager != nil { if locationManager != nil {
locationManager.Close() locationManager.Close()
} }
if sysUpdateManager != nil {
sysUpdateManager.Close()
}
if geoClientInstance != nil { if geoClientInstance != nil {
geoClientInstance.Close() geoClientInstance.Close()
} }
@@ -1733,6 +1791,10 @@ func Start(printDocs bool) error {
} }
}() }()
if err := InitializeSysUpdateManager(); err != nil {
log.Warnf("Sysupdate manager unavailable: %v", err)
}
log.Info("") log.Info("")
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities) log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)

View File

@@ -0,0 +1,96 @@
package sysupdate
import (
"context"
"os/exec"
"sync"
)
type Backend interface {
ID() string
DisplayName() string
Repo() RepoKind
IsAvailable(ctx context.Context) bool
NeedsAuth() bool
RunsInTerminal() bool
CheckUpdates(ctx context.Context) ([]Package, error)
Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error
}
type Selection struct {
System Backend
Overlay []Backend
}
func (s Selection) All() []Backend {
if s.System == nil {
return s.Overlay
}
out := make([]Backend, 0, 1+len(s.Overlay))
out = append(out, s.System)
out = append(out, s.Overlay...)
return out
}
func (s Selection) Info() []BackendInfo {
all := s.All()
out := make([]BackendInfo, 0, len(all))
for _, b := range all {
out = append(out, BackendInfo{
ID: b.ID(),
DisplayName: b.DisplayName(),
Repo: b.Repo(),
NeedsAuth: b.NeedsAuth(),
RunsInTerminal: b.RunsInTerminal(),
})
}
return out
}
var (
registryMu sync.RWMutex
systemCandidates []func() Backend
overlayCandidate []func() Backend
)
func RegisterSystemBackend(factory func() Backend) {
registryMu.Lock()
defer registryMu.Unlock()
systemCandidates = append(systemCandidates, factory)
}
func RegisterOverlayBackend(factory func() Backend) {
registryMu.Lock()
defer registryMu.Unlock()
overlayCandidate = append(overlayCandidate, factory)
}
func Select(ctx context.Context) Selection {
registryMu.RLock()
sys := append([]func() Backend(nil), systemCandidates...)
ov := append([]func() Backend(nil), overlayCandidate...)
registryMu.RUnlock()
var sel Selection
for _, factory := range sys {
b := factory()
if !b.IsAvailable(ctx) {
continue
}
sel.System = b
break
}
for _, factory := range ov {
b := factory()
if !b.IsAvailable(ctx) {
continue
}
sel.Overlay = append(sel.Overlay, b)
}
return sel
}
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}

View File

@@ -0,0 +1,79 @@
package sysupdate
import (
"context"
"os/exec"
"regexp"
"strings"
)
func init() {
RegisterSystemBackend(func() Backend { return &aptBackend{} })
}
var aptUpgradableLine = regexp.MustCompile(`^([^/]+)/\S+\s+(\S+)\s+\S+\s+\[upgradable from:\s+([^\]]+)\]`)
type aptBackend struct{}
func (aptBackend) ID() string { return "apt" }
func (aptBackend) DisplayName() string { return "APT" }
func (aptBackend) Repo() RepoKind { return RepoSystem }
func (aptBackend) NeedsAuth() bool { return true }
func (aptBackend) RunsInTerminal() bool { return false }
func (aptBackend) IsAvailable(_ context.Context) bool {
return commandExists("apt") || commandExists("apt-get")
}
func (aptBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable")
cmd.Env = append(cmd.Environ(), "LC_ALL=C")
out, err := cmd.Output()
if err != nil {
return nil, err
}
return parseAptUpgradable(string(out)), nil
}
func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
bin := "apt-get"
if !commandExists(bin) {
bin = "apt"
}
if opts.DryRun {
return Run(ctx, []string{bin, "upgrade", "--dry-run"}, RunOptions{
Env: []string{"DEBIAN_FRONTEND=noninteractive", "LC_ALL=C"},
OnLine: onLine,
})
}
names := pickTargetNames(opts.Targets, "apt", true)
if len(names) == 0 {
return nil
}
argv := append([]string{"pkexec", "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "install", "-y", "--only-upgrade"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine})
}
func parseAptUpgradable(text string) []Package {
if text == "" {
return nil
}
var pkgs []Package
for line := range strings.SplitSeq(text, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
m := aptUpgradableLine.FindStringSubmatch(line)
if m == nil {
continue
}
pkgs = append(pkgs, Package{
Name: m[1],
Repo: RepoSystem,
Backend: "apt",
FromVersion: m[3],
ToVersion: m[2],
})
}
return pkgs
}

View File

@@ -0,0 +1,72 @@
package sysupdate
import (
"reflect"
"testing"
)
func TestParseAptUpgradable(t *testing.T) {
tests := []struct {
name string
input string
want []Package
}{
{
name: "empty",
input: "",
want: nil,
},
{
name: "header line only",
input: `Listing... Done
`,
want: nil,
},
{
name: "single upgradable",
input: `Listing... Done
bash/stable 5.2.40-1 amd64 [upgradable from: 5.2.39-1]`,
want: []Package{
{Name: "bash", Repo: RepoSystem, Backend: "apt", FromVersion: "5.2.39-1", ToVersion: "5.2.40-1"},
},
},
{
name: "multiple architectures and suites",
input: `Listing... Done
bash/stable 5.2.40-1 amd64 [upgradable from: 5.2.39-1]
libfoo/stable-security 1.0.0-2 amd64 [upgradable from: 1.0.0-1]
zsh/testing 5.9-6 arm64 [upgradable from: 5.9-5]`,
want: []Package{
{Name: "bash", Repo: RepoSystem, Backend: "apt", FromVersion: "5.2.39-1", ToVersion: "5.2.40-1"},
{Name: "libfoo", Repo: RepoSystem, Backend: "apt", FromVersion: "1.0.0-1", ToVersion: "1.0.0-2"},
{Name: "zsh", Repo: RepoSystem, Backend: "apt", FromVersion: "5.9-5", ToVersion: "5.9-6"},
},
},
{
name: "package name with hyphens, dots, plus signs",
input: `Listing... Done
g++/stable 4:13.3.0-1 amd64 [upgradable from: 4:13.2.0-1]
libsdl2-2.0-0/stable 2.30.0+dfsg-1 amd64 [upgradable from: 2.28.5+dfsg-1]`,
want: []Package{
{Name: "g++", Repo: RepoSystem, Backend: "apt", FromVersion: "4:13.2.0-1", ToVersion: "4:13.3.0-1"},
{Name: "libsdl2-2.0-0", Repo: RepoSystem, Backend: "apt", FromVersion: "2.28.5+dfsg-1", ToVersion: "2.30.0+dfsg-1"},
},
},
{
name: "non-matching lines ignored",
input: "WARNING: this is some warning\nbash/stable 5.2.40-1 amd64 [upgradable from: 5.2.39-1]",
want: []Package{
{Name: "bash", Repo: RepoSystem, Backend: "apt", FromVersion: "5.2.39-1", ToVersion: "5.2.40-1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseAptUpgradable(tt.input)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseAptUpgradable() = %#v\nwant %#v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,125 @@
package sysupdate
import (
"context"
"errors"
"os/exec"
"strings"
)
func init() {
RegisterSystemBackend(func() Backend { return &dnfBackend{bin: "dnf5"} })
RegisterSystemBackend(func() Backend { return &dnfBackend{bin: "dnf"} })
}
type dnfBackend struct {
bin string
}
func (b dnfBackend) ID() string { return b.bin }
func (b dnfBackend) DisplayName() string { return strings.ToUpper(b.bin) }
func (b dnfBackend) Repo() RepoKind { return RepoSystem }
func (b dnfBackend) NeedsAuth() bool { return true }
func (b dnfBackend) RunsInTerminal() bool { return false }
func (b dnfBackend) IsAvailable(ctx context.Context) bool {
if !commandExists(b.bin) {
return false
}
if commandExists("rpm-ostree") && ostreeBooted(ctx) {
return false
}
return true
}
func (b dnfBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
out, err := dnfListUpgrades(ctx, b.bin)
if err != nil {
return nil, err
}
installed := rpmInstalledVersions(ctx)
return parseDnfList(out, b.bin, installed), nil
}
func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
if opts.DryRun {
return Run(ctx, []string{b.bin, "upgrade", "--assumeno"}, RunOptions{OnLine: onLine})
}
names := pickTargetNames(opts.Targets, b.bin, true)
if len(names) == 0 {
return nil
}
argv := append([]string{"pkexec", b.bin, "upgrade", "-y"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine})
}
func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
cmd := exec.CommandContext(ctx, bin, "list", "--upgrades", "--quiet")
out, err := cmd.Output()
if err == nil {
return string(out), nil
}
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 1 {
return "", nil
}
return "", err
}
func rpmInstalledVersions(ctx context.Context) map[string]string {
out, err := exec.CommandContext(ctx, "rpm", "-qa", "--qf", `%{NAME}\t%{VERSION}-%{RELEASE}\n`).Output()
if err != nil {
return nil
}
m := make(map[string]string)
for line := range strings.SplitSeq(string(out), "\n") {
name, ver, ok := strings.Cut(line, "\t")
if !ok {
continue
}
m[name] = ver
}
return m
}
func parseDnfList(text, backendID string, installed map[string]string) []Package {
if text == "" {
return nil
}
var pkgs []Package
for line := range strings.SplitSeq(text, "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
nameArch := fields[0]
version := fields[1]
dot := strings.LastIndex(nameArch, ".")
if dot <= 0 {
continue
}
if !looksLikeRpmVersion(version) {
continue
}
name := nameArch[:dot]
pkgs = append(pkgs, Package{
Name: nameArch,
Repo: RepoSystem,
Backend: backendID,
FromVersion: installed[name],
ToVersion: version,
})
}
return pkgs
}
func looksLikeRpmVersion(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r >= '0' && r <= '9' {
return true
}
}
return false
}

View File

@@ -0,0 +1,80 @@
package sysupdate
import (
"reflect"
"testing"
)
func TestParseDnfList(t *testing.T) {
tests := []struct {
name string
input string
backendID string
installed map[string]string
want []Package
}{
{
name: "empty",
input: "",
want: nil,
},
{
name: "single package with installed cross-ref",
input: "bash.x86_64 5.2.40-1.fc41 updates",
backendID: "dnf",
installed: map[string]string{"bash": "5.2.39-1.fc41"},
want: []Package{
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf", FromVersion: "5.2.39-1.fc41", ToVersion: "5.2.40-1.fc41"},
},
},
{
name: "noarch package and missing installed entry",
input: `bash.x86_64 5.2.40-1.fc41 updates
fonts-misc.noarch 1.0.5-2.fc41 updates`,
backendID: "dnf",
installed: map[string]string{"bash": "5.2.39-1.fc41"},
want: []Package{
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf", FromVersion: "5.2.39-1.fc41", ToVersion: "5.2.40-1.fc41"},
{Name: "fonts-misc.noarch", Repo: RepoSystem, Backend: "dnf", FromVersion: "", ToVersion: "1.0.5-2.fc41"},
},
},
{
name: "skips header rows",
input: `Available
Upgrades
bash.x86_64 5.2.40-1.fc41 updates`,
backendID: "dnf",
installed: nil,
want: []Package{
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf", FromVersion: "", ToVersion: "5.2.40-1.fc41"},
},
},
{
name: "skips lines with too few fields",
input: "incomplete",
backendID: "dnf",
want: nil,
},
{
name: "skips dnf5 banner / column header lines",
input: `Updates available
Last metadata expiration check: 0:01:23 ago on Tue Apr 29 14:00:00 2026.
Package Version Repository Size
bash.x86_64 5.2.40-1.fc41 updates`,
backendID: "dnf",
installed: nil,
want: []Package{
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf", FromVersion: "", ToVersion: "5.2.40-1.fc41"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseDnfList(tt.input, tt.backendID, tt.installed)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseDnfList() = %#v\nwant %#v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,169 @@
package sysupdate
import (
"context"
"os/exec"
"strings"
)
func init() {
RegisterOverlayBackend(func() Backend { return &flatpakBackend{} })
}
type flatpakBackend struct{}
func (flatpakBackend) ID() string { return "flatpak" }
func (flatpakBackend) DisplayName() string { return "Flatpak" }
func (flatpakBackend) Repo() RepoKind { return RepoFlatpak }
func (flatpakBackend) NeedsAuth() bool { return false }
func (flatpakBackend) RunsInTerminal() bool { return false }
func (flatpakBackend) IsAvailable(_ context.Context) bool { return commandExists("flatpak") }
func (flatpakBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
cmd := exec.CommandContext(ctx, "flatpak", "remote-ls", "--updates", "--columns=application,version,branch,commit,name")
out, err := cmd.Output()
if err != nil {
return nil, err
}
installed := flatpakInstalled(ctx)
return parseFlatpakUpdates(string(out), installed), nil
}
func flatpakInstalled(ctx context.Context) map[string]flatpakInstalledEntry {
out, err := exec.CommandContext(ctx, "flatpak", "list", "--columns=application,version,branch,active").Output()
if err != nil {
return nil
}
m := make(map[string]flatpakInstalledEntry)
for line := range strings.SplitSeq(string(out), "\n") {
if line == "" {
continue
}
fields := strings.Split(line, "\t")
if len(fields) == 0 || fields[0] == "" {
continue
}
appID := fields[0]
entry := flatpakInstalledEntry{}
if len(fields) > 1 {
entry.version = fields[1]
}
if len(fields) > 2 {
entry.branch = fields[2]
}
if len(fields) > 3 {
entry.commit = fields[3]
}
key := appID
if entry.branch != "" {
key = appID + "//" + entry.branch
}
m[key] = entry
}
return m
}
type flatpakInstalledEntry struct {
version string
branch string
commit string
}
func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
if opts.DryRun {
return Run(ctx, []string{"flatpak", "update", "--no-deploy", "-y"}, RunOptions{OnLine: onLine})
}
refs := flatpakTargetRefs(opts.Targets)
if len(refs) == 0 {
return nil
}
argv := append([]string{"flatpak", "update", "-y", "--noninteractive"}, refs...)
return Run(ctx, argv, RunOptions{OnLine: onLine})
}
func flatpakTargetRefs(targets []Package) []string {
out := make([]string, 0, len(targets))
for _, p := range targets {
if p.Backend != "flatpak" {
continue
}
ref := p.Ref
if ref == "" {
ref = p.Name
}
out = append(out, ref)
}
return out
}
func parseFlatpakUpdates(text string, installed map[string]flatpakInstalledEntry) []Package {
if text == "" {
return nil
}
var pkgs []Package
for line := range strings.SplitSeq(text, "\n") {
if line == "" {
continue
}
fields := strings.Split(line, "\t")
if len(fields) == 0 || fields[0] == "" {
continue
}
appID := fields[0]
version, branch, commit := "", "", ""
if len(fields) > 1 {
version = fields[1]
}
if len(fields) > 2 {
branch = fields[2]
}
if len(fields) > 3 {
commit = fields[3]
}
display := appID
if len(fields) > 4 && fields[4] != "" {
display = fields[4]
}
key := appID
if branch != "" {
key = appID + "//" + branch
}
inst := installed[key]
if inst.commit != "" && commit != "" && strings.HasPrefix(commit, inst.commit) {
continue
}
from, to := flatpakVersionPair(inst.version, inst.commit, version, commit)
ref := appID
if branch != "" {
ref = appID + "//" + branch
}
pkgs = append(pkgs, Package{
Name: display,
Repo: RepoFlatpak,
Backend: "flatpak",
FromVersion: from,
ToVersion: to,
Ref: ref,
})
}
return pkgs
}
func flatpakVersionPair(installedVer, installedCommit, remoteVer, remoteCommit string) (from, to string) {
if remoteVer != "" {
return installedVer, remoteVer
}
return shortCommit(installedCommit), shortCommit(remoteCommit)
}
func shortCommit(c string) string {
if len(c) > 8 {
return c[:8]
}
return c
}

View File

@@ -0,0 +1,150 @@
package sysupdate
import (
"reflect"
"testing"
)
func TestParseFlatpakUpdates(t *testing.T) {
tests := []struct {
name string
input string
installed map[string]flatpakInstalledEntry
want []Package
}{
{
name: "empty",
input: "",
want: nil,
},
{
name: "real flathub-style row with empty version, falls back to commit",
// columns: application,version,branch,commit,name
input: "com.discordapp.Discord\t\tstable\t43a1e5d2d3a446919356fd86d9f984ad7c6a0e20f109250d9d868223f26ca586\tDiscord",
installed: map[string]flatpakInstalledEntry{
"com.discordapp.Discord//stable": {commit: "8b16fa1a9b2aa189302c2428c8a7bb33dd050faf7e535dd1d975044cb0986855"},
},
want: []Package{
{
Name: "Discord",
Repo: RepoFlatpak,
Backend: "flatpak",
FromVersion: "8b16fa1a",
ToVersion: "43a1e5d2",
Ref: "com.discordapp.Discord//stable",
},
},
},
{
name: "remote provides version, installed version known",
input: "com.example.App\t1.5.0\tstable\tdeadbeefcafe\tExample App",
installed: map[string]flatpakInstalledEntry{
"com.example.App//stable": {version: "1.4.2"},
},
want: []Package{
{
Name: "Example App",
Repo: RepoFlatpak,
Backend: "flatpak",
FromVersion: "1.4.2",
ToVersion: "1.5.0",
Ref: "com.example.App//stable",
},
},
},
{
name: "no installed entry, remote has no version, falls back to commit on both sides",
input: "org.gnome.Platform\t\t49\tbadcd4afb1fe\tgnome platform",
installed: nil,
want: []Package{
{
Name: "gnome platform",
Repo: RepoFlatpak,
Backend: "flatpak",
FromVersion: "",
ToVersion: "badcd4af",
Ref: "org.gnome.Platform//49",
},
},
},
{
name: "missing display name falls back to application id",
input: "com.example.NoName\t2.0\tstable\tabcdef123456\t",
want: []Package{
{
Name: "com.example.NoName",
Repo: RepoFlatpak,
Backend: "flatpak",
FromVersion: "",
ToVersion: "2.0",
Ref: "com.example.NoName//stable",
},
},
},
{
name: "skips blank lines and rows with empty application id",
input: "\n\t\t\t\t\norg.real.App\t1.0\tstable\tdeadbeef\tReal App",
want: []Package{
{
Name: "Real App",
Repo: RepoFlatpak,
Backend: "flatpak",
FromVersion: "",
ToVersion: "1.0",
Ref: "org.real.App//stable",
},
},
},
{
name: "skips phantom updates where remote commit matches installed",
input: "com.phantom.App\t\tstable\tabc12345deadbeef\tPhantom",
installed: map[string]flatpakInstalledEntry{
"com.phantom.App//stable": {commit: "abc12345"},
},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseFlatpakUpdates(tt.input, tt.installed)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseFlatpakUpdates() = %#v\nwant %#v", got, tt.want)
}
})
}
}
func TestFlatpakVersionPair(t *testing.T) {
tests := []struct {
name string
installedVer, installedCommit, remoteVer, remoteCommit string
wantFrom, wantTo string
}{
{
name: "remote has version - prefer versions",
installedVer: "1.0.0", remoteVer: "1.1.0",
wantFrom: "1.0.0", wantTo: "1.1.0",
},
{
name: "remote has no version - both sides fall to short commit",
installedCommit: "8b16fa1a9b2aa189302c2428c8a7bb33dd050faf7e535dd1d975044cb0986855",
remoteCommit: "43a1e5d2d3a446919356fd86d9f984ad7c6a0e20f109250d9d868223f26ca586",
wantFrom: "8b16fa1a", wantTo: "43a1e5d2",
},
{
name: "short commits left as-is",
installedCommit: "abc123", remoteCommit: "def456",
wantFrom: "abc123", wantTo: "def456",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
from, to := flatpakVersionPair(tt.installedVer, tt.installedCommit, tt.remoteVer, tt.remoteCommit)
if from != tt.wantFrom || to != tt.wantTo {
t.Errorf("flatpakVersionPair() = (%q, %q), want (%q, %q)", from, to, tt.wantFrom, tt.wantTo)
}
})
}
}

View File

@@ -0,0 +1,258 @@
package sysupdate
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func init() {
RegisterSystemBackend(func() Backend { return &archHelperBackend{id: "paru"} })
RegisterSystemBackend(func() Backend { return &archHelperBackend{id: "yay"} })
RegisterSystemBackend(func() Backend { return &pacmanBackend{} })
}
var archUpdateLine = regexp.MustCompile(`^(\S+)\s+(\S+)\s+->\s+(\S+)`)
type pacmanBackend struct{}
func (pacmanBackend) ID() string { return "pacman" }
func (pacmanBackend) DisplayName() string { return "Pacman" }
func (pacmanBackend) Repo() RepoKind { return RepoSystem }
func (pacmanBackend) NeedsAuth() bool { return true }
func (pacmanBackend) RunsInTerminal() bool { return false }
func (pacmanBackend) IsAvailable(_ context.Context) bool { return commandExists("pacman") }
func (b pacmanBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
out, err := pacmanRepoUpdates(ctx)
if err != nil {
return nil, err
}
return parseArchUpdates(out, b.ID(), RepoSystem), nil
}
func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
if opts.DryRun {
return Run(ctx, []string{"pacman", "-Sup"}, RunOptions{OnLine: onLine})
}
names := pickTargetNames(opts.Targets, b.ID(), opts.IncludeAUR)
if len(names) == 0 {
return nil
}
argv := append([]string{"pkexec", "pacman", "-Sy", "--noconfirm", "--needed"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine})
}
type archHelperBackend struct {
id string
}
func (b archHelperBackend) ID() string { return b.id }
func (b archHelperBackend) Repo() RepoKind { return RepoSystem }
func (b archHelperBackend) NeedsAuth() bool { return true }
func (b archHelperBackend) RunsInTerminal() bool {
return os.Getenv("DMS_FORCE_PKEXEC") != "1"
}
func (b archHelperBackend) IsAvailable(_ context.Context) bool { return commandExists(b.id) }
func (b archHelperBackend) DisplayName() string {
switch b.id {
case "paru":
return "Paru (AUR)"
case "yay":
return "Yay (AUR)"
default:
return b.id
}
}
func (b archHelperBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
repoOut, err := pacmanRepoUpdates(ctx)
if err != nil {
return nil, err
}
pkgs := parseArchUpdates(repoOut, b.id, RepoSystem)
aurOut, err := capturePermissive(ctx, b.id, "-Qua")
if err != nil {
return nil, err
}
pkgs = append(pkgs, parseArchUpdates(aurOut, b.id, RepoAUR)...)
return pkgs, nil
}
func (b archHelperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
if opts.DryRun {
return Run(ctx, []string{b.id, "-Sup"}, RunOptions{OnLine: onLine})
}
names := pickTargetNames(opts.Targets, b.id, opts.IncludeAUR)
if len(names) == 0 {
return nil
}
if os.Getenv("DMS_FORCE_PKEXEC") == "1" {
argv := append([]string{"pkexec", b.id, "-Sy", "--noconfirm", "--needed"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine})
}
term := findTerminal(opts.Terminal)
if term == "" {
return fmt.Errorf("no terminal found (pick one in DMS settings, set $TERMINAL, or install kitty/ghostty/foot/alacritty)")
}
cmd := fmt.Sprintf("%s -Sy --noconfirm --needed %s", b.id, strings.Join(names, " "))
title := fmt.Sprintf("DMS — System Update (%s)", b.id)
return Run(ctx, wrapInTerminal(term, title, cmd), RunOptions{OnLine: onLine})
}
func pickTargetNames(targets []Package, backendID string, includeAUR bool) []string {
out := make([]string, 0, len(targets))
for _, p := range targets {
if p.Backend != backendID {
continue
}
if !includeAUR && p.Repo == RepoAUR {
continue
}
out = append(out, p.Name)
}
return out
}
func pacmanRepoUpdates(ctx context.Context) (string, error) {
if commandExists("checkupdates") {
return capturePermissive(ctx, "checkupdates")
}
if commandExists("fakeroot") {
out, err := pacmanCheckViaFakeroot(ctx)
if err == nil {
return out, nil
}
log.Warnf("[sysupdate] fakeroot db refresh failed, falling back to stale pacman -Qu: %v", err)
}
return capturePermissive(ctx, "pacman", "-Qu")
}
func pacmanCheckViaFakeroot(ctx context.Context) (string, error) {
dir, err := pacmanPrivateDB()
if err != nil {
return "", err
}
if err := seedPacmanDB(dir); err != nil {
return "", fmt.Errorf("seed sync db: %w", err)
}
refresh := exec.CommandContext(ctx, "fakeroot", "--", "pacman", "-Sy", "--dbpath", dir, "--logfile", "/dev/null", "--disable-sandbox")
if out, err := refresh.CombinedOutput(); err != nil {
return "", fmt.Errorf("fakeroot pacman -Sy: %w (%s)", err, strings.TrimSpace(string(out)))
}
return capturePermissive(ctx, "pacman", "-Qu", "--dbpath", dir)
}
func seedPacmanDB(dir string) error {
syncDir := filepath.Join(dir, "sync")
if err := os.MkdirAll(syncDir, 0o755); err != nil {
return err
}
dbs, err := filepath.Glob("/var/lib/pacman/sync/*.db")
if err != nil {
return err
}
for _, src := range dbs {
if err := copyFile(src, filepath.Join(syncDir, filepath.Base(src))); err != nil {
return err
}
}
localLink := filepath.Join(dir, "local")
if fi, err := os.Lstat(localLink); err == nil {
if fi.Mode()&os.ModeSymlink == 0 {
if err := os.RemoveAll(localLink); err != nil {
return err
}
} else {
return nil
}
}
return os.Symlink("/var/lib/pacman/local", localLink)
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}
func pacmanPrivateDB() (string, error) {
tmp := os.Getenv("TMPDIR")
if tmp == "" {
tmp = "/tmp"
}
dir := filepath.Join(tmp, fmt.Sprintf("dms-checkup-db-%d", os.Getuid()))
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", err
}
return dir, nil
}
func capturePermissive(ctx context.Context, argv ...string) (string, error) {
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
out, err := cmd.Output()
if err == nil {
return string(out), nil
}
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
switch exitErr.ExitCode() {
case 1, 2:
return string(out), nil
}
}
return "", err
}
func parseArchUpdates(text, backendID string, repo RepoKind) []Package {
if text == "" {
return nil
}
var pkgs []Package
for line := range strings.SplitSeq(text, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
m := archUpdateLine.FindStringSubmatch(line)
if m == nil {
continue
}
p := Package{
Name: m[1],
Repo: repo,
Backend: backendID,
FromVersion: m[2],
ToVersion: m[3],
}
if repo == RepoAUR {
p.ChangelogURL = "https://aur.archlinux.org/packages/" + p.Name
}
pkgs = append(pkgs, p)
}
return pkgs
}

View File

@@ -0,0 +1,114 @@
package sysupdate
import (
"reflect"
"testing"
)
func TestParseArchUpdates(t *testing.T) {
tests := []struct {
name string
input string
backendID string
repo RepoKind
want []Package
}{
{
name: "empty",
input: "",
backendID: "paru",
repo: RepoSystem,
want: nil,
},
{
name: "whitespace only",
input: " \n\n \n",
backendID: "paru",
repo: RepoSystem,
want: nil,
},
{
name: "single repo update",
input: "bat 0.26.0-1 -> 0.26.1-2",
backendID: "paru",
repo: RepoSystem,
want: []Package{
{Name: "bat", Repo: RepoSystem, Backend: "paru", FromVersion: "0.26.0-1", ToVersion: "0.26.1-2"},
},
},
{
name: "multiple updates with epoch versions",
input: `cups 2:2.4.18-1 -> 2:2.4.19-1
linux 6.18.0-1 -> 6.18.1-1
mesa 26.4.0-1 -> 26.4.1-1`,
backendID: "paru",
repo: RepoSystem,
want: []Package{
{Name: "cups", Repo: RepoSystem, Backend: "paru", FromVersion: "2:2.4.18-1", ToVersion: "2:2.4.19-1"},
{Name: "linux", Repo: RepoSystem, Backend: "paru", FromVersion: "6.18.0-1", ToVersion: "6.18.1-1"},
{Name: "mesa", Repo: RepoSystem, Backend: "paru", FromVersion: "26.4.0-1", ToVersion: "26.4.1-1"},
},
},
{
name: "AUR update with changelog url",
input: "google-chrome 147.0.7727.116-1 -> 147.0.7727.137-1",
backendID: "paru",
repo: RepoAUR,
want: []Package{
{
Name: "google-chrome",
Repo: RepoAUR,
Backend: "paru",
FromVersion: "147.0.7727.116-1",
ToVersion: "147.0.7727.137-1",
ChangelogURL: "https://aur.archlinux.org/packages/google-chrome",
},
},
},
{
name: "git package latest-commit marker",
input: "niri-git 26.04.r5.ga85b922-1 -> latest-commit",
backendID: "yay",
repo: RepoAUR,
want: []Package{
{
Name: "niri-git",
Repo: RepoAUR,
Backend: "yay",
FromVersion: "26.04.r5.ga85b922-1",
ToVersion: "latest-commit",
ChangelogURL: "https://aur.archlinux.org/packages/niri-git",
},
},
},
{
name: "skips lines that don't match arrow format",
input: `bat 0.26.0-1 -> 0.26.1-2
this is not an update line
foo`,
backendID: "pacman",
repo: RepoSystem,
want: []Package{
{Name: "bat", Repo: RepoSystem, Backend: "pacman", FromVersion: "0.26.0-1", ToVersion: "0.26.1-2"},
},
},
{
name: "extra whitespace tolerated",
input: " bat 0.26.0-1 -> 0.26.1-2 ",
backendID: "paru",
repo: RepoSystem,
want: []Package{
{Name: "bat", Repo: RepoSystem, Backend: "paru", FromVersion: "0.26.0-1", ToVersion: "0.26.1-2"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseArchUpdates(tt.input, tt.backendID, tt.repo)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseArchUpdates() = %#v\nwant %#v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,125 @@
package sysupdate
import (
"context"
"encoding/json"
"errors"
"os/exec"
)
const ostreeExitUpdateAvailable = 77
func init() {
RegisterSystemBackend(func() Backend { return &rpmOstreeBackend{} })
}
type rpmOstreeBackend struct{}
func (rpmOstreeBackend) ID() string { return "rpm-ostree" }
func (rpmOstreeBackend) DisplayName() string { return "rpm-ostree" }
func (rpmOstreeBackend) Repo() RepoKind { return RepoOSTree }
func (rpmOstreeBackend) NeedsAuth() bool { return true }
func (rpmOstreeBackend) RunsInTerminal() bool { return false }
func (b rpmOstreeBackend) IsAvailable(ctx context.Context) bool {
if !commandExists("rpm-ostree") {
return false
}
return ostreeBooted(ctx)
}
type ostreeStatus struct {
Deployments []ostreeDeployment `json:"deployments"`
CachedUpdate *ostreeCached `json:"cached-update"`
}
type ostreeDeployment struct {
Origin string `json:"origin"`
Version string `json:"version"`
Timestamp int64 `json:"timestamp"`
Booted bool `json:"booted"`
}
type ostreeCached struct {
Origin string `json:"origin"`
Version string `json:"version"`
Timestamp int64 `json:"timestamp"`
Checksum string `json:"checksum"`
}
func ostreeBooted(ctx context.Context) bool {
cmd := exec.CommandContext(ctx, "rpm-ostree", "status", "--json")
out, err := cmd.Output()
if err != nil {
return false
}
var s ostreeStatus
if err := json.Unmarshal(out, &s); err != nil {
return false
}
return len(s.Deployments) > 0
}
func (rpmOstreeBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
cmd := exec.CommandContext(ctx, "rpm-ostree", "upgrade", "--check")
if err := cmd.Run(); err != nil {
exitErr, ok := errors.AsType[*exec.ExitError](err)
if !ok || exitErr.ExitCode() != ostreeExitUpdateAvailable {
return nil, err
}
}
statusOut, err := exec.CommandContext(ctx, "rpm-ostree", "status", "--json").Output()
if err != nil {
return nil, err
}
return parseRpmOstreeStatus(statusOut)
}
func parseRpmOstreeStatus(statusOut []byte) ([]Package, error) {
var s ostreeStatus
if err := json.Unmarshal(statusOut, &s); err != nil {
return nil, err
}
if s.CachedUpdate == nil {
return nil, nil
}
booted := bootedDeployment(s.Deployments)
from := ""
if booted != nil {
from = booted.Version
}
if from == s.CachedUpdate.Version {
return nil, nil
}
name := s.CachedUpdate.Origin
if name == "" {
name = "system"
}
return []Package{{
Name: name,
Repo: RepoOSTree,
Backend: "rpm-ostree",
FromVersion: from,
ToVersion: s.CachedUpdate.Version,
}}, nil
}
func bootedDeployment(deps []ostreeDeployment) *ostreeDeployment {
for i := range deps {
if deps[i].Booted {
return &deps[i]
}
}
return nil
}
func (rpmOstreeBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
argv := []string{"rpm-ostree", "upgrade"}
if opts.DryRun {
argv = append(argv, "--check")
}
return Run(ctx, argv, RunOptions{OnLine: onLine})
}

View File

@@ -0,0 +1,104 @@
package sysupdate
import (
"reflect"
"testing"
)
func TestParseRpmOstreeStatus(t *testing.T) {
tests := []struct {
name string
input string
want []Package
wantErr bool
}{
{
name: "no cached update",
input: `{"deployments":[{"version":"39.20240101.0","booted":true}],"cached-update":null}`,
want: nil,
},
{
name: "cached update available, booted version differs",
input: `{
"deployments": [
{"origin": "fedora:fedora/x86_64/silverblue", "version": "39.20240101.0", "booted": true},
{"origin": "fedora:fedora/x86_64/silverblue", "version": "39.20231215.0", "booted": false}
],
"cached-update": {
"origin": "fedora:fedora/x86_64/silverblue",
"version": "39.20240115.0",
"checksum": "abc123"
}
}`,
want: []Package{
{
Name: "fedora:fedora/x86_64/silverblue",
Repo: RepoOSTree,
Backend: "rpm-ostree",
FromVersion: "39.20240101.0",
ToVersion: "39.20240115.0",
},
},
},
{
name: "cached update equals booted version (no real update)",
input: `{
"deployments": [{"version": "39.20240101.0", "booted": true}],
"cached-update": {"origin": "x", "version": "39.20240101.0"}
}`,
want: nil,
},
{
name: "no booted deployment falls back to empty from",
input: `{
"deployments": [{"version": "39.20240101.0", "booted": false}],
"cached-update": {"origin": "fedora:silverblue", "version": "39.20240115.0"}
}`,
want: []Package{
{
Name: "fedora:silverblue",
Repo: RepoOSTree,
Backend: "rpm-ostree",
FromVersion: "",
ToVersion: "39.20240115.0",
},
},
},
{
name: "missing origin defaults to system",
input: `{
"deployments": [{"version": "1.0", "booted": true}],
"cached-update": {"version": "1.1"}
}`,
want: []Package{
{
Name: "system",
Repo: RepoOSTree,
Backend: "rpm-ostree",
FromVersion: "1.0",
ToVersion: "1.1",
},
},
},
{
name: "malformed JSON",
input: `{not json`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseRpmOstreeStatus([]byte(tt.input))
if (err != nil) != tt.wantErr {
t.Fatalf("parseRpmOstreeStatus() err = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseRpmOstreeStatus() = %#v\nwant %#v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,83 @@
package sysupdate
import (
"context"
"encoding/xml"
"errors"
"os/exec"
)
func init() {
RegisterSystemBackend(func() Backend { return &zypperBackend{} })
}
type zypperBackend struct{}
func (zypperBackend) ID() string { return "zypper" }
func (zypperBackend) DisplayName() string { return "Zypper" }
func (zypperBackend) Repo() RepoKind { return RepoSystem }
func (zypperBackend) NeedsAuth() bool { return true }
func (zypperBackend) RunsInTerminal() bool { return false }
func (zypperBackend) IsAvailable(_ context.Context) bool { return commandExists("zypper") }
type zypperUpdateList struct {
XMLName xml.Name `xml:"stream"`
Updates []zypperUpdate `xml:"update-list>update"`
}
type zypperUpdate struct {
Name string `xml:"name,attr"`
Edition string `xml:"edition,attr"`
EditionOld string `xml:"edition-old,attr"`
Kind string `xml:"kind,attr"`
}
func (zypperBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
cmd := exec.CommandContext(ctx, "zypper", "--non-interactive", "--xmlout", "list-updates")
out, err := cmd.Output()
if err != nil {
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
switch exitErr.ExitCode() {
case 100, 101, 102, 103:
err = nil
}
}
if err != nil {
return nil, err
}
}
return parseZypperXML(out)
}
func parseZypperXML(out []byte) ([]Package, error) {
var list zypperUpdateList
if err := xml.Unmarshal(out, &list); err != nil {
return nil, err
}
pkgs := make([]Package, 0, len(list.Updates))
for _, u := range list.Updates {
if u.Kind != "" && u.Kind != "package" {
continue
}
pkgs = append(pkgs, Package{
Name: u.Name,
Repo: RepoSystem,
Backend: "zypper",
FromVersion: u.EditionOld,
ToVersion: u.Edition,
})
}
return pkgs, nil
}
func (zypperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
if opts.DryRun {
return Run(ctx, []string{"zypper", "--non-interactive", "--dry-run", "update"}, RunOptions{OnLine: onLine})
}
names := pickTargetNames(opts.Targets, "zypper", true)
if len(names) == 0 {
return nil
}
argv := append([]string{"pkexec", "zypper", "--non-interactive", "update"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine})
}

View File

@@ -0,0 +1,80 @@
package sysupdate
import (
"reflect"
"testing"
)
func TestParseZypperXML(t *testing.T) {
tests := []struct {
name string
input string
want []Package
wantErr bool
}{
{
name: "empty stream",
input: `<?xml version="1.0"?><stream><update-list></update-list></stream>`,
want: []Package{},
},
{
name: "single package update",
input: `<?xml version="1.0"?>
<stream>
<update-list>
<update name="zsh" edition="5.9-6" edition-old="5.9-5" kind="package" arch="x86_64">
<source url="https://download.opensuse.org/" alias="repo-oss"/>
</update>
</update-list>
</stream>`,
want: []Package{
{Name: "zsh", Repo: RepoSystem, Backend: "zypper", FromVersion: "5.9-5", ToVersion: "5.9-6"},
},
},
{
name: "skips non-package kinds",
input: `<?xml version="1.0"?>
<stream>
<update-list>
<update name="foo" edition="2.0" edition-old="1.0" kind="package"/>
<update name="security-patch" edition="1" edition-old="0" kind="patch"/>
<update name="bar" edition="3.0" edition-old="2.0" kind="package"/>
</update-list>
</stream>`,
want: []Package{
{Name: "foo", Repo: RepoSystem, Backend: "zypper", FromVersion: "1.0", ToVersion: "2.0"},
{Name: "bar", Repo: RepoSystem, Backend: "zypper", FromVersion: "2.0", ToVersion: "3.0"},
},
},
{
name: "treats missing kind as package",
input: `<?xml version="1.0"?>
<stream><update-list>
<update name="kernel" edition="6.18.1-1" edition-old="6.18.0-1"/>
</update-list></stream>`,
want: []Package{
{Name: "kernel", Repo: RepoSystem, Backend: "zypper", FromVersion: "6.18.0-1", ToVersion: "6.18.1-1"},
},
},
{
name: "malformed XML returns error",
input: `not xml at all`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseZypperXML([]byte(tt.input))
if (err != nil) != tt.wantErr {
t.Fatalf("parseZypperXML() err = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseZypperXML() = %#v\nwant %#v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,125 @@
package sysupdate
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"sync"
"syscall"
)
type RunOptions struct {
Env []string
OnLine func(string)
}
func Run(ctx context.Context, argv []string, opts RunOptions) error {
if len(argv) == 0 {
return fmt.Errorf("sysupdate.Run: empty argv")
}
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
if len(opts.Env) > 0 {
cmd.Env = append(cmd.Environ(), opts.Env...)
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Cancel = func() error {
if cmd.Process == nil {
return nil
}
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(2)
go pump(stdout, opts.OnLine, &wg)
go pump(stderr, opts.OnLine, &wg)
wg.Wait()
return cmd.Wait()
}
func pump(r io.Reader, onLine func(string), wg *sync.WaitGroup) {
defer wg.Done()
if onLine == nil {
_, _ = io.Copy(io.Discard, r)
return
}
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
for scanner.Scan() {
onLine(scanner.Text())
}
}
func Capture(ctx context.Context, argv []string) (string, error) {
if len(argv) == 0 {
return "", fmt.Errorf("sysupdate.Capture: empty argv")
}
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
out, err := cmd.Output()
return string(out), err
}
func findTerminal(override string) string {
if override != "" && commandExists(override) {
return override
}
if t := os.Getenv("TERMINAL"); t != "" && commandExists(t) {
return t
}
for _, t := range []string{"ghostty", "kitty", "foot", "alacritty", "wezterm", "konsole", "gnome-terminal", "xterm"} {
if commandExists(t) {
return t
}
}
return ""
}
func wrapInTerminal(term, title, shellCmd string) []string {
const appID = "dms-sysupdate"
banner := fmt.Sprintf(
`printf '\033[1;36m=== %s ===\033[0m\n'; printf '\033[2m$ %s\033[0m\n'; printf '\033[33mYou may be prompted for your sudo password to apply system updates.\033[0m\n\n'`,
title, shellCmd,
)
closer := `printf '\n\033[1;32m=== Done. Press Enter to close. ===\033[0m\n'; read`
export := `export SUDO_PROMPT="[DMS] sudo password for %u: "; `
full := export + banner + "; " + shellCmd + "; " + closer
switch term {
case "kitty":
return []string{term, "--class", appID, "-T", title, "-e", "sh", "-c", full}
case "alacritty":
return []string{term, "--class", appID, "-T", title, "-e", "sh", "-c", full}
case "foot":
return []string{term, "--app-id=" + appID, "--title=" + title, "-e", "sh", "-c", full}
case "ghostty":
return []string{term, "--class=" + appID, "--title=" + title, "-e", "sh", "-c", full}
case "wezterm":
return []string{term, "--class", appID, "-T", title, "-e", "sh", "-c", full}
case "xterm":
return []string{term, "-class", appID, "-T", title, "-e", "sh", "-c", full}
case "konsole":
return []string{term, "-p", "tabtitle=" + title, "-e", "sh", "-c", full}
case "gnome-terminal":
return []string{term, "--title=" + title, "--", "sh", "-c", full}
default:
return []string{term, "-e", "sh", "-c", full}
}
}

View File

@@ -0,0 +1,55 @@
package sysupdate
import (
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
)
func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
switch req.Method {
case "sysupdate.getState":
models.Respond(conn, req.ID, m.GetState())
case "sysupdate.refresh":
force := params.BoolOpt(req.Params, "force", false)
m.Refresh(RefreshOptions{Force: force})
models.Respond(conn, req.ID, m.GetState())
case "sysupdate.upgrade":
handleUpgrade(conn, req, m)
case "sysupdate.cancel":
m.Cancel()
models.Respond(conn, req.ID, m.GetState())
case "sysupdate.acquire":
m.Acquire()
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
case "sysupdate.release":
m.Release()
models.Respond(conn, req.ID, models.SuccessResult{Success: true})
case "sysupdate.setInterval":
seconds, err := params.Int(req.Params, "seconds")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
m.SetInterval(seconds)
models.Respond(conn, req.ID, m.GetState())
default:
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
}
}
func handleUpgrade(conn net.Conn, req models.Request, m *Manager) {
opts := UpgradeOptions{
IncludeFlatpak: params.BoolOpt(req.Params, "includeFlatpak", true),
IncludeAUR: params.BoolOpt(req.Params, "includeAUR", true),
DryRun: params.BoolOpt(req.Params, "dry", false),
CustomCommand: params.StringOpt(req.Params, "customCommand", ""),
Terminal: params.StringOpt(req.Params, "terminal", ""),
}
if err := m.Upgrade(opts); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, m.GetState())
}

View File

@@ -0,0 +1,506 @@
package sysupdate
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const (
defaultIntervalSeconds = 30 * 60
minIntervalSeconds = 5 * 60
recentLogCapacity = 200
checkTimeout = 5 * time.Minute
upgradeTimeout = 30 * time.Minute
)
type Manager struct {
mu sync.RWMutex
state State
subscribers syncmap.Map[string, chan State]
selection Selection
notifyDirty chan struct{}
stopChan chan struct{}
notifierWG sync.WaitGroup
schedulerWG sync.WaitGroup
acquireCount int32
wakeSched chan struct{}
refreshSerial sync.Mutex
opMu sync.Mutex
opCtx context.Context
opCancel context.CancelFunc
}
func NewManager() (*Manager, error) {
m := &Manager{
notifyDirty: make(chan struct{}, 1),
stopChan: make(chan struct{}),
wakeSched: make(chan struct{}, 1),
}
m.state = State{
Phase: PhaseIdle,
IntervalSeconds: defaultIntervalSeconds,
Backends: []BackendInfo{},
Packages: []Package{},
}
id, pretty := readOSRelease()
m.state.Distro = id
m.state.DistroPretty = pretty
m.selection = Select(context.Background())
m.state.Backends = m.selection.Info()
if len(m.state.Backends) == 0 {
m.state.Error = &ErrorInfo{
Code: ErrCodeNoBackend,
Message: "no supported package manager found",
Hint: "install a supported package manager (pacman, dnf, apt, zypper) or flatpak",
}
}
m.notifierWG.Add(1)
go m.notifier()
m.schedulerWG.Add(1)
go m.scheduler()
go m.runRefresh(context.Background())
return m, nil
}
func (m *Manager) GetState() State {
m.mu.RLock()
defer m.mu.RUnlock()
return cloneState(m.state)
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 16)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.opMu.Lock()
if m.opCancel != nil {
m.opCancel()
}
m.opMu.Unlock()
select {
case m.wakeSched <- struct{}{}:
default:
}
m.schedulerWG.Wait()
m.notifierWG.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) SetInterval(seconds int) {
if seconds < minIntervalSeconds {
seconds = minIntervalSeconds
}
m.mu.Lock()
m.state.IntervalSeconds = seconds
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) Refresh(opts RefreshOptions) {
m.mu.RLock()
phase := m.state.Phase
m.mu.RUnlock()
switch {
case phase == PhaseUpgrading:
return
case phase == PhaseRefreshing && !opts.Force:
m.refreshSerial.Lock()
m.refreshSerial.Unlock()
return
}
m.runRefresh(context.Background())
}
func (m *Manager) Upgrade(opts UpgradeOptions) error {
if len(m.selection.All()) == 0 {
return errors.New("no backend available")
}
m.opMu.Lock()
if m.opCancel != nil {
m.opMu.Unlock()
return errors.New("operation already running")
}
ctx, cancel := context.WithTimeout(context.Background(), upgradeTimeout)
m.opCtx = ctx
m.opCancel = cancel
m.opMu.Unlock()
go m.runUpgrade(ctx, opts)
return nil
}
func (m *Manager) Cancel() {
m.opMu.Lock()
cancel := m.opCancel
m.opMu.Unlock()
if cancel == nil {
return
}
cancel()
}
func (m *Manager) Acquire() {
first := atomic.AddInt32(&m.acquireCount, 1) == 1
select {
case m.wakeSched <- struct{}{}:
default:
}
if first {
go m.runRefresh(context.Background())
}
}
func (m *Manager) Release() {
if atomic.AddInt32(&m.acquireCount, -1) < 0 {
atomic.StoreInt32(&m.acquireCount, 0)
}
}
func (m *Manager) scheduler() {
defer m.schedulerWG.Done()
for {
if atomic.LoadInt32(&m.acquireCount) == 0 {
select {
case <-m.stopChan:
return
case <-m.wakeSched:
}
continue
}
m.mu.RLock()
interval := m.state.IntervalSeconds
m.mu.RUnlock()
if interval < minIntervalSeconds {
interval = minIntervalSeconds
}
t := time.NewTimer(time.Duration(interval) * time.Second)
select {
case <-m.stopChan:
t.Stop()
return
case <-m.wakeSched:
t.Stop()
case <-t.C:
m.runRefresh(context.Background())
}
}
}
func (m *Manager) runRefresh(parent context.Context) {
m.refreshSerial.Lock()
defer m.refreshSerial.Unlock()
if len(m.selection.All()) == 0 {
return
}
ctx, cancel := context.WithTimeout(parent, checkTimeout)
defer cancel()
m.mu.Lock()
if m.state.Phase == PhaseUpgrading {
m.mu.Unlock()
return
}
m.state.Phase = PhaseRefreshing
m.state.Error = nil
m.state.RecentLog = nil
m.mu.Unlock()
m.markDirty()
type backendResult struct {
pkgs []Package
err error
}
backends := m.selection.All()
results := make([]backendResult, len(backends))
var wg sync.WaitGroup
for i, b := range backends {
wg.Add(1)
go func(i int, b Backend) {
defer wg.Done()
pkgs, err := b.CheckUpdates(ctx)
results[i] = backendResult{pkgs: pkgs, err: err}
}(i, b)
}
wg.Wait()
now := time.Now().Unix()
m.mu.Lock()
m.state.LastCheckUnix = now
m.state.Packages = m.state.Packages[:0]
var firstErr error
for i, r := range results {
if r.err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("%s: %w", backends[i].ID(), r.err)
}
continue
}
m.state.Packages = append(m.state.Packages, r.pkgs...)
}
m.state.Count = len(m.state.Packages)
if firstErr != nil {
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: ErrCodeBackendFailed, Message: firstErr.Error()}
} else {
m.state.Phase = PhaseIdle
m.state.LastSuccessUnix = now
m.state.NextCheckUnix = now + int64(m.state.IntervalSeconds)
}
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) {
defer func() {
m.opMu.Lock()
if m.opCancel != nil {
m.opCancel = nil
m.opCtx = nil
}
m.opMu.Unlock()
}()
if opts.CustomCommand != "" {
m.runCustomUpgrade(ctx, opts.CustomCommand, opts.Terminal)
return
}
backends := upgradeBackends(m.selection, opts)
if len(backends) == 0 {
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
return
}
if len(opts.Targets) == 0 {
m.mu.RLock()
opts.Targets = append([]Package(nil), m.state.Packages...)
m.mu.RUnlock()
}
opID := fmt.Sprintf("op-%d", time.Now().UnixNano())
m.mu.Lock()
m.state.Phase = PhaseUpgrading
m.state.OperationID = opID
m.state.OperationStarted = time.Now().Unix()
m.state.RecentLog = m.state.RecentLog[:0]
m.state.Error = nil
m.mu.Unlock()
m.markDirty()
onLine := func(line string) { m.appendLog(line) }
for _, b := range backends {
m.appendLog(fmt.Sprintf("== %s ==", b.DisplayName()))
if err := b.Upgrade(ctx, opts, onLine); err != nil {
code := ErrCodeBackendFailed
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
code = ErrCodeTimeout
} else if errors.Is(ctx.Err(), context.Canceled) {
code = ErrCodeCancelled
}
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: fmt.Sprintf("%s: %v", b.ID(), err)}
m.mu.Unlock()
m.markDirty()
return
}
}
m.mu.Lock()
m.state.Phase = PhaseIdle
m.state.OperationID = ""
m.state.OperationStarted = 0
m.mu.Unlock()
m.markDirty()
go m.runRefresh(context.Background())
}
func (m *Manager) runCustomUpgrade(ctx context.Context, command, terminalOverride string) {
term := findTerminal(terminalOverride)
if term == "" {
m.setError(ErrCodeBackendFailed, "no terminal found (pick one in DMS settings, set $TERMINAL, or install kitty/ghostty/foot/alacritty)")
return
}
opID := fmt.Sprintf("op-%d", time.Now().UnixNano())
m.mu.Lock()
m.state.Phase = PhaseUpgrading
m.state.OperationID = opID
m.state.OperationStarted = time.Now().Unix()
m.state.RecentLog = m.state.RecentLog[:0]
m.state.Error = nil
m.mu.Unlock()
m.markDirty()
onLine := func(line string) { m.appendLog(line) }
argv := wrapInTerminal(term, "DMS — System Update (custom)", command)
if err := Run(ctx, argv, RunOptions{OnLine: onLine}); err != nil {
code := ErrCodeBackendFailed
switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded):
code = ErrCodeTimeout
case errors.Is(ctx.Err(), context.Canceled):
code = ErrCodeCancelled
}
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: err.Error()}
m.mu.Unlock()
m.markDirty()
return
}
m.mu.Lock()
m.state.Phase = PhaseIdle
m.state.OperationID = ""
m.state.OperationStarted = 0
m.mu.Unlock()
m.markDirty()
go m.runRefresh(context.Background())
}
func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend {
var out []Backend
if sel.System != nil {
out = append(out, sel.System)
}
for _, b := range sel.Overlay {
switch {
case b.Repo() == RepoFlatpak && !opts.IncludeFlatpak:
continue
}
out = append(out, b)
}
return out
}
func (m *Manager) appendLog(line string) {
m.mu.Lock()
if cap(m.state.RecentLog) == 0 {
m.state.RecentLog = make([]string, 0, recentLogCapacity)
}
if len(m.state.RecentLog) >= recentLogCapacity {
copy(m.state.RecentLog, m.state.RecentLog[1:])
m.state.RecentLog = m.state.RecentLog[:recentLogCapacity-1]
}
m.state.RecentLog = append(m.state.RecentLog, line)
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) setError(code ErrorCode, msg string) {
m.mu.Lock()
m.state.Phase = PhaseError
m.state.Error = &ErrorInfo{Code: code, Message: msg}
m.mu.Unlock()
m.markDirty()
}
func (m *Manager) markDirty() {
select {
case m.notifyDirty <- struct{}{}:
default:
}
}
func (m *Manager) notifier() {
defer m.notifierWG.Done()
for {
select {
case <-m.stopChan:
return
case <-m.notifyDirty:
snap := m.GetState()
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- snap:
default:
}
return true
})
}
}
}
func cloneState(s State) State {
out := s
out.Backends = append([]BackendInfo(nil), s.Backends...)
out.Packages = append([]Package(nil), s.Packages...)
out.RecentLog = append([]string(nil), s.RecentLog...)
if s.Error != nil {
errCopy := *s.Error
out.Error = &errCopy
}
return out
}
func readOSRelease() (id, pretty string) {
f, err := os.Open("/etc/os-release")
if err != nil {
return "", ""
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
k, v, ok := strings.Cut(scanner.Text(), "=")
if !ok {
continue
}
v = strings.Trim(v, "\"")
switch k {
case "ID":
id = v
case "PRETTY_NAME":
pretty = v
}
}
if err := scanner.Err(); err != nil {
log.Debugf("[sysupdate] read os-release: %v", err)
}
return id, pretty
}

View File

@@ -0,0 +1,86 @@
package sysupdate
type Phase string
const (
PhaseIdle Phase = "idle"
PhaseRefreshing Phase = "refreshing"
PhaseUpgrading Phase = "upgrading"
PhaseError Phase = "error"
)
type RepoKind string
const (
RepoSystem RepoKind = "system"
RepoAUR RepoKind = "aur"
RepoFlatpak RepoKind = "flatpak"
RepoOSTree RepoKind = "ostree"
)
type ErrorCode string
const (
ErrCodeNone ErrorCode = ""
ErrCodeNoBackend ErrorCode = "no-backend"
ErrCodeBusy ErrorCode = "busy"
ErrCodeBackendFailed ErrorCode = "backend-failed"
ErrCodeTimeout ErrorCode = "timeout"
ErrCodeCancelled ErrorCode = "cancelled"
ErrCodeInvalidRequest ErrorCode = "invalid-request"
)
type Package struct {
Name string `json:"name"`
Repo RepoKind `json:"repo"`
Backend string `json:"backend"`
FromVersion string `json:"fromVersion,omitempty"`
ToVersion string `json:"toVersion,omitempty"`
SizeBytes int64 `json:"sizeBytes,omitempty"`
ChangelogURL string `json:"changelogUrl,omitempty"`
Ref string `json:"-"`
}
type BackendInfo struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Repo RepoKind `json:"repo"`
NeedsAuth bool `json:"needsAuth"`
RunsInTerminal bool `json:"runsInTerminal"`
}
type ErrorInfo struct {
Code ErrorCode `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Hint string `json:"hint,omitempty"`
}
type State struct {
Phase Phase `json:"phase"`
Distro string `json:"distro,omitempty"`
DistroPretty string `json:"distroPretty,omitempty"`
Backends []BackendInfo `json:"backends"`
Packages []Package `json:"packages"`
Count int `json:"count"`
IntervalSeconds int `json:"intervalSeconds"`
LastCheckUnix int64 `json:"lastCheckUnix,omitempty"`
LastSuccessUnix int64 `json:"lastSuccessUnix,omitempty"`
NextCheckUnix int64 `json:"nextCheckUnix,omitempty"`
OperationID string `json:"operationId,omitempty"`
OperationStarted int64 `json:"operationStartedUnix,omitempty"`
RecentLog []string `json:"recentLog,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
}
type UpgradeOptions struct {
IncludeFlatpak bool
IncludeAUR bool
DryRun bool
CustomCommand string
Terminal string
Targets []Package
}
type RefreshOptions struct {
Force bool
}

View File

@@ -111,7 +111,7 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ="; vendorHash = "sha256-kPu3MLqhLaCaBpCwIP8JXep0J/Z45kxDFOEY8JvcWdU=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];

View File

@@ -5,9 +5,11 @@ import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("CacheData")
readonly property int cacheConfigVersion: 1 readonly property int cacheConfigVersion: 1
@@ -131,7 +133,7 @@ Singleton {
} }
} }
} catch (e) { } catch (e) {
console.warn("CacheData: Failed to parse cache:", e.message); log.warn("Failed to parse cache:", e.message);
} finally { } finally {
_loading = false; _loading = false;
} }
@@ -149,7 +151,7 @@ Singleton {
} }
function migrateFromUndefinedToV1(cache) { function migrateFromUndefinedToV1(cache) {
console.info("CacheData: Migrating configuration from undefined to version 1"); log.info("Migrating configuration from undefined to version 1");
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
@@ -164,7 +166,7 @@ Singleton {
for (const key in cache) { for (const key in cache) {
if (!validKeys.includes(key)) { if (!validKeys.includes(key)) {
console.log("CacheData: Removing unused key:", key); log.debug("Removing unused key:", key);
delete cache[key]; delete cache[key];
needsSave = true; needsSave = true;
} }
@@ -174,7 +176,7 @@ Singleton {
cacheFile.setText(JSON.stringify(cache, null, 2)); cacheFile.setText(JSON.stringify(cache, null, 2));
} }
} catch (e) { } catch (e) {
console.warn("CacheData: Failed to cleanup unused keys:", e.message); log.warn("Failed to cleanup unused keys:", e.message);
} }
} }
@@ -184,7 +186,7 @@ Singleton {
if (content && content.trim()) if (content && content.trim())
return JSON.parse(content); return JSON.parse(content);
} catch (e) { } catch (e) {
console.warn("CacheData: Failed to parse launcher cache:", e.message); log.warn("Failed to parse launcher cache:", e.message);
} }
return null; return null;
} }
@@ -220,7 +222,7 @@ Singleton {
} }
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) { if (!isGreeterMode) {
console.info("CacheData: No cache file found, starting fresh"); log.info("No cache file found, starting fresh");
} }
} }
} }

View File

@@ -5,9 +5,11 @@ import QtQuick
import Qt.labs.folderlistmodel import Qt.labs.folderlistmodel
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("I18n")
property string _resolvedLocale: "en" property string _resolvedLocale: "en"
@@ -54,15 +56,15 @@ Singleton {
try { try {
root.translations = JSON.parse(text()); root.translations = JSON.parse(text());
root.translationsLoaded = true; root.translationsLoaded = true;
console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`); log.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
} catch (e) { } catch (e) {
console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English"); log.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
root._fallbackToEnglish(); root._fallbackToEnglish();
} }
} }
onLoadFailed: error => { onLoadFailed: error => {
console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English"); log.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
root._fallbackToEnglish(); root._fallbackToEnglish();
} }
} }
@@ -105,14 +107,14 @@ Singleton {
_selectedPath = fileUrl; _selectedPath = fileUrl;
translationsLoaded = false; translationsLoaded = false;
translations = ({}); translations = ({});
console.info(`I18n: Using locale '${localeTag}' from ${fileUrl}`); log.info(`I18n: Using locale '${localeTag}' from ${fileUrl}`);
} }
function _fallbackToEnglish() { function _fallbackToEnglish() {
_selectedPath = ""; _selectedPath = "";
translationsLoaded = false; translationsLoaded = false;
translations = ({}); translations = ({});
console.warn("I18n: Falling back to built-in English strings"); log.warn("Falling back to built-in English strings");
} }
function tr(term, context) { function tr(term, context) {

View File

@@ -3,9 +3,11 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("Proc")
readonly property int noTimeout: -1 readonly property int noTimeout: -1
property int defaultDebounceMs: 50 property int defaultDebounceMs: 50
@@ -112,7 +114,7 @@ Singleton {
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1; const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
entry.callback(safeOutput, safeExitCode); entry.callback(safeOutput, safeExitCode);
} catch (e) { } catch (e) {
console.warn("runCommand callback error for command:", entry.command, "Error:", e); log.warn("runCommand callback error for command:", entry.command, "Error:", e);
} }
} }
try { try {

View File

@@ -12,6 +12,7 @@ import "settings/SessionStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("SessionData")
readonly property int sessionConfigVersion: 3 readonly property int sessionConfigVersion: 3
@@ -30,9 +31,36 @@ Singleton {
property bool isLightMode: false property bool isLightMode: false
property bool doNotDisturb: false property bool doNotDisturb: false
property real doNotDisturbUntil: 0 property real doNotDisturbUntil: 0
property string terminalOverride: ""
property bool isSwitchingMode: false property bool isSwitchingMode: false
property bool suppressOSD: true property bool suppressOSD: true
readonly property var terminalOptions: ["ghostty", "kitty", "foot", "alacritty", "wezterm", "konsole", "gnome-terminal", "xterm"]
property var installedTerminals: []
function resolveTerminal() {
if (terminalOverride && terminalOverride.length > 0) {
return terminalOverride;
}
const env = Quickshell.env("TERMINAL");
if (env && env.length > 0) {
return env;
}
return "";
}
Process {
id: terminalProbe
running: true
command: ["sh", "-c", "for t in ghostty kitty foot alacritty wezterm konsole gnome-terminal xterm; do command -v \"$t\" >/dev/null 2>&1 && echo \"$t\"; done"]
stdout: StdioCollector {
onStreamFinished: {
const found = text.trim().split("\n").filter(line => line.length > 0);
root.installedTerminals = found;
}
}
}
Timer { Timer {
id: dndExpireTimer id: dndExpireTimer
repeat: false repeat: false
@@ -230,7 +258,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten."); log.error("Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
} }
} }
@@ -310,7 +338,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
console.error("SessionData: Failed to parse session.json - file will not be overwritten."); log.error("Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
} }
} }
@@ -525,7 +553,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found"); log.warn("Screen not found");
return; return;
} }
@@ -622,7 +650,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found"); log.warn("Screen not found");
return; return;
} }
@@ -653,7 +681,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found"); log.warn("Screen not found");
return; return;
} }
@@ -684,7 +712,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found"); log.warn("Screen not found");
return; return;
} }
@@ -715,7 +743,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
console.warn("SessionData: Screen not found"); log.warn("Screen not found");
return; return;
} }

View File

@@ -13,6 +13,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 5 readonly property int settingsConfigVersion: 5
@@ -493,6 +494,7 @@ Singleton {
property bool matugenTemplatePywalfox: true property bool matugenTemplatePywalfox: true
property bool matugenTemplateZenBrowser: true property bool matugenTemplateZenBrowser: true
property bool matugenTemplateVesktop: true property bool matugenTemplateVesktop: true
property bool matugenTemplateVencord: true
property bool matugenTemplateEquibop: true property bool matugenTemplateEquibop: true
property bool matugenTemplateGhostty: true property bool matugenTemplateGhostty: true
property bool matugenTemplateKitty: true property bool matugenTemplateKitty: true
@@ -640,6 +642,9 @@ Singleton {
property bool updaterUseCustomCommand: false property bool updaterUseCustomCommand: false
property string updaterCustomCommand: "" property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: "" property string updaterTerminalAdditionalParams: ""
property int updaterIntervalSeconds: 1800
property bool updaterIncludeFlatpak: true
property bool updaterAllowAUR: true
property string displayNameMode: "system" property string displayNameMode: "system"
property var screenPreferences: ({}) property var screenPreferences: ({})
@@ -1291,7 +1296,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
console.error("SettingsData: Failed to parse settings.json - file will not be overwritten. Error:", msg); log.error("Failed to parse settings.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
applyStoredTheme(); applyStoredTheme();
} finally { } finally {
@@ -1312,12 +1317,12 @@ Singleton {
if (_isReadOnly) { if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges(); _hasUnsavedChanges = _checkForUnsavedChanges();
if (!wasReadOnly) if (!wasReadOnly)
console.info("SettingsData: settings.json is now read-only"); log.info("settings.json is now read-only");
} else { } else {
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasUnsavedChanges = false; _hasUnsavedChanges = false;
if (wasReadOnly) if (wasReadOnly)
console.info("SettingsData: settings.json is now writable"); log.info("settings.json is now writable");
if (_pendingMigration) if (_pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
} }
@@ -1371,7 +1376,7 @@ Singleton {
} catch (e) { } catch (e) {
const msg = e.message || String(e); const msg = e.message || String(e);
if (!_isMissingPluginSettingsError(e)) if (!_isMissingPluginSettingsError(e))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg); log.warn("Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings(); _resetPluginSettings();
} }
} }
@@ -1388,7 +1393,7 @@ Singleton {
} catch (e) { } catch (e) {
_pluginParseError = true; _pluginParseError = true;
const msg = e.message; const msg = e.message;
console.error("SettingsData: Failed to parse plugin_settings.json - file will not be overwritten. Error:", msg); log.error("Failed to parse plugin_settings.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse plugin_settings.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse plugin_settings.json"), msg));
pluginSettings = {}; pluginSettings = {};
} finally { } finally {
@@ -2791,7 +2796,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
console.error("SettingsData: Failed to reload settings.json - file will not be overwritten. Error:", msg); log.error("Failed to reload settings.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
} finally { } finally {
_loading = false; _loading = false;
@@ -2826,7 +2831,7 @@ Singleton {
if (!isGreeterMode) { if (!isGreeterMode) {
const msg = String(error || ""); const msg = String(error || "");
if (!_isMissingPluginSettingsError(error)) if (!_isMissingPluginSettingsError(error))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg); log.warn("Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings(); _resetPluginSettings();
} }
} }

View File

@@ -12,6 +12,7 @@ import "StockThemes.js" as StockThemes
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("Theme")
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell" readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell"
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true" readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
@@ -148,7 +149,7 @@ Singleton {
} }
if (colorsFileLoadFailed && currentTheme === dynamic && rawWallpaperPath) { if (colorsFileLoadFailed && currentTheme === dynamic && rawWallpaperPath) {
console.info("Theme: Matugen now available, regenerating colors for dynamic theme"); log.info("Matugen now available, regenerating colors for dynamic theme");
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode); const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"; const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"; const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
@@ -376,7 +377,7 @@ Singleton {
"use": true "use": true
}, response => { }, response => {
if (!response.error) { if (!response.error) {
console.info("Theme automation: IP location enabled after connection"); log.info("Theme automation: IP location enabled after connection");
} }
}); });
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { } else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
@@ -389,13 +390,13 @@ Singleton {
"longitude": SessionData.longitude "longitude": SessionData.longitude
}, locationResponse => { }, locationResponse => {
if (locationResponse?.error) { if (locationResponse?.error) {
console.warn("Theme automation: Failed to set location", locationResponse.error); log.warn("Theme automation: Failed to set location", locationResponse.error);
} }
}); });
} }
}); });
} else { } else {
console.warn("Theme automation: No location configured"); log.warn("Theme automation: No location configured");
} }
} }
} }
@@ -1525,12 +1526,12 @@ Singleton {
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType, stockColors) { function setDesiredTheme(kind, value, isLight, iconTheme, matugenType, stockColors) {
if (!matugenAvailable) { if (!matugenAvailable) {
console.warn("Theme: matugen not available or disabled - cannot set system theme"); log.warn("matugen not available or disabled - cannot set system theme");
return; return;
} }
if (workerRunning) { if (workerRunning) {
console.info("Theme: Worker already running, queueing request"); log.info("Worker already running, queueing request");
pendingThemeRequest = { pendingThemeRequest = {
kind, kind,
value, value,
@@ -1542,7 +1543,7 @@ Singleton {
return; return;
} }
console.info("Theme: Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)"); log.info("Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)");
if (typeof NiriService !== "undefined" && CompositorService.isNiri) { if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
NiriService.suppressNextToast(); NiriService.suppressNextToast();
@@ -1557,7 +1558,7 @@ Singleton {
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true "runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
}; };
console.log("Theme: Starting matugen worker"); log.debug("Starting matugen worker");
workerRunning = true; workerRunning = true;
const args = ["dms", "matugen", "queue", "--state-dir", stateDir, "--shell-dir", shellDir, "--config-dir", configDir, "--kind", desired.kind, "--value", desired.value, "--mode", desired.mode, "--icon-theme", desired.iconTheme, "--matugen-type", desired.matugenType,]; const args = ["dms", "matugen", "queue", "--state-dir", stateDir, "--shell-dir", shellDir, "--config-dir", configDir, "--kind", desired.kind, "--value", desired.value, "--mode", desired.mode, "--icon-theme", desired.iconTheme, "--matugen-type", desired.matugenType,];
@@ -1581,7 +1582,7 @@ Singleton {
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "vencord", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");
@@ -1603,6 +1604,8 @@ Singleton {
skipTemplates.push("zenbrowser"); skipTemplates.push("zenbrowser");
if (!SettingsData.matugenTemplateVesktop) if (!SettingsData.matugenTemplateVesktop)
skipTemplates.push("vesktop"); skipTemplates.push("vesktop");
if (!SettingsData.matugenTemplateVencord)
skipTemplates.push("vencord");
if (!SettingsData.matugenTemplateEquibop) if (!SettingsData.matugenTemplateEquibop)
skipTemplates.push("equibop"); skipTemplates.push("equibop");
if (!SettingsData.matugenTemplateGhostty) if (!SettingsData.matugenTemplateGhostty)
@@ -1715,7 +1718,7 @@ Singleton {
} }
if (!darkTheme || !darkTheme.primary) { if (!darkTheme || !darkTheme.primary) {
console.warn("Theme data not available for:", currentTheme); log.warn("Theme data not available for:", currentTheme);
return; return;
} }
@@ -1953,10 +1956,10 @@ Singleton {
id: systemThemeGenerator id: systemThemeGenerator
running: false running: false
stdout: SplitParser { stdout: SplitParser {
onRead: data => console.info("Theme worker:", data) onRead: data => log.info("Theme worker:", data)
} }
stderr: SplitParser { stderr: SplitParser {
onRead: data => console.warn("Theme worker:", data) onRead: data => log.warn("Theme worker:", data)
} }
onExited: exitCode => { onExited: exitCode => {
@@ -1965,18 +1968,18 @@ Singleton {
switch (exitCode) { switch (exitCode) {
case 0: case 0:
console.info("Theme: Matugen worker completed successfully"); log.info("Matugen worker completed successfully");
root.matugenCompleted(currentMode, "success"); root.matugenCompleted(currentMode, "success");
break; break;
case 2: case 2:
console.log("Theme: Matugen worker completed with code 2 (no changes needed)"); log.debug("Matugen worker completed with code 2 (no changes needed)");
root.matugenCompleted(currentMode, "no-changes"); root.matugenCompleted(currentMode, "no-changes");
break; break;
default: default:
if (typeof ToastService !== "undefined") { if (typeof ToastService !== "undefined") {
ToastService.showError("Theme worker failed (" + exitCode + ")"); ToastService.showError("Theme worker failed (" + exitCode + ")");
} }
console.warn("Theme: Matugen worker failed with exit code:", exitCode); log.warn("Matugen worker failed with exit code:", exitCode);
root.matugenCompleted(currentMode, "error"); root.matugenCompleted(currentMode, "error");
} }
@@ -1985,7 +1988,7 @@ Singleton {
const req = pendingThemeRequest; const req = pendingThemeRequest;
pendingThemeRequest = null; pendingThemeRequest = null;
console.info("Theme: Processing queued theme request"); log.info("Processing queued theme request");
setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors); setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors);
} }
} }
@@ -2039,7 +2042,7 @@ Singleton {
} }
} }
} catch (e) { } catch (e) {
console.error("Theme: Failed to parse dynamic colors:", e); log.error("Failed to parse dynamic colors:", e);
if (typeof ToastService !== "undefined") { if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"; ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Dynamic colors parse error: " + e.message); ToastService.showError("Dynamic colors parse error: " + e.message);
@@ -2059,11 +2062,11 @@ Singleton {
onLoadFailed: function (error) { onLoadFailed: function (error) {
if (currentTheme === dynamic) { if (currentTheme === dynamic) {
console.warn("Theme: Dynamic colors file load failed, marking for regeneration"); log.warn("Dynamic colors file load failed, marking for regeneration");
colorsFileLoadFailed = true; colorsFileLoadFailed = true;
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (!isGreeterMode && matugenAvailable && rawWallpaperPath) { if (!isGreeterMode && matugenAvailable && rawWallpaperPath) {
console.log("Theme: Matugen available, triggering immediate regeneration"); log.debug("Matugen available, triggering immediate regeneration");
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
} }
@@ -2187,7 +2190,7 @@ Singleton {
"endMinute": endMinute "endMinute": endMinute
}, response => { }, response => {
if (response && response.error) { if (response && response.error) {
console.error("Theme automation: Failed to sync time schedule:", response.error); log.error("Theme automation: Failed to sync time schedule:", response.error);
} }
}); });
@@ -2280,9 +2283,9 @@ Singleton {
if (root.themeModeAutomationActive) { if (root.themeModeAutomationActive) {
if (SessionData.nightModeUseIPLocation) { if (SessionData.nightModeUseIPLocation) {
console.warn("Theme automation: Waiting for IP location from backend"); log.warn("Theme automation: Waiting for IP location from backend");
} else { } else {
console.warn("Theme automation: Location mode requires coordinates"); log.warn("Theme automation: Location mode requires coordinates");
} }
} }
} }
@@ -2364,7 +2367,7 @@ Singleton {
"use": true "use": true
}, response => { }, response => {
if (response?.error) { if (response?.error) {
console.warn("Theme automation: Failed to enable IP location", response.error); log.warn("Theme automation: Failed to enable IP location", response.error);
} }
}); });
return true; return true;
@@ -2378,7 +2381,7 @@ Singleton {
"longitude": SessionData.longitude "longitude": SessionData.longitude
}, locResp => { }, locResp => {
if (locResp?.error) { if (locResp?.error) {
console.warn("Theme automation: Failed to set location", locResp.error); log.warn("Theme automation: Failed to set location", locResp.error);
} }
}); });
} }

View File

@@ -4,6 +4,7 @@ var SPEC = {
isLightMode: { def: false }, isLightMode: { def: false },
doNotDisturb: { def: false }, doNotDisturb: { def: false },
doNotDisturbUntil: { def: 0 }, doNotDisturbUntil: { def: 0 },
terminalOverride: { def: "" },
wallpaperPath: { def: "" }, wallpaperPath: { def: "" },
perMonitorWallpaper: { def: false }, perMonitorWallpaper: { def: false },

View File

@@ -302,6 +302,7 @@ var SPEC = {
matugenTemplatePywalfox: { def: true }, matugenTemplatePywalfox: { def: true },
matugenTemplateZenBrowser: { def: true }, matugenTemplateZenBrowser: { def: true },
matugenTemplateVesktop: { def: true }, matugenTemplateVesktop: { def: true },
matugenTemplateVencord: { def: true },
matugenTemplateEquibop: { def: true }, matugenTemplateEquibop: { def: true },
matugenTemplateGhostty: { def: true }, matugenTemplateGhostty: { def: true },
matugenTemplateKitty: { def: true }, matugenTemplateKitty: { def: true },
@@ -428,6 +429,9 @@ var SPEC = {
updaterUseCustomCommand: { def: false }, updaterUseCustomCommand: { def: false },
updaterCustomCommand: { def: "" }, updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" }, updaterTerminalAdditionalParams: { def: "" },
updaterIntervalSeconds: { def: 1800 },
updaterIncludeFlatpak: { def: true },
updaterAllowAUR: { def: true },
displayNameMode: { def: "system" }, displayNameMode: { def: "system" },
screenPreferences: { def: {} }, screenPreferences: { def: {} },

View File

@@ -27,6 +27,7 @@ import qs.Services
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DMSShell")
property bool osdSurfacesLoaded: true property bool osdSurfacesLoaded: true
property int pendingOsdResumeReloads: 0 property int pendingOsdResumeReloads: 0
@@ -54,7 +55,7 @@ Item {
item.popoutService = PopoutService; item.popoutService = PopoutService;
} }
item.pluginId = pluginId; item.pluginId = pluginId;
console.info("Daemon plugin loaded:", pluginId); log.info("Daemon plugin loaded:", pluginId);
} }
} }
} }
@@ -93,7 +94,7 @@ Item {
} }
onFadeCancelled: { onFadeCancelled: {
console.log("Fade to lock cancelled by user on screen:", fadeWindowLoader.modelData.name); log.debug("Fade to lock cancelled by user on screen:", fadeWindowLoader.modelData.name);
} }
} }
@@ -133,7 +134,7 @@ Item {
} }
onFadeCancelled: { onFadeCancelled: {
console.log("Fade to DPMS cancelled by user on screen:", fadeDpmsWindowLoader.modelData.name); log.debug("Fade to DPMS cancelled by user on screen:", fadeDpmsWindowLoader.modelData.name);
} }
} }
@@ -773,7 +774,7 @@ Item {
cmd += " " + escapedPath; cmd += " " + escapedPath;
} }
console.log("FilePicker: Launching", cmd); log.debug("FilePicker: Launching", cmd);
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
@@ -805,10 +806,10 @@ Item {
} }
function onAppPickerRequested(data) { function onAppPickerRequested(data) {
console.log("DMSShell: App picker requested with data:", JSON.stringify(data)); log.debug("App picker requested with data:", JSON.stringify(data));
if (!data || !data.target) { if (!data || !data.target) {
console.warn("DMSShell: Invalid app picker request data"); log.warn("Invalid app picker request data");
return; return;
} }
@@ -895,7 +896,12 @@ Item {
SystemUpdatePopout { SystemUpdatePopout {
id: systemUpdatePopout id: systemUpdatePopout
onPopoutClosed: PopoutService.unloadSystemUpdate() onPopoutClosed: {
if (systemUpdatePopout._reopenAfterUpgrade) {
return;
}
PopoutService.unloadSystemUpdate();
}
Component.onCompleted: { Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout; PopoutService.systemUpdatePopout = systemUpdatePopout;
@@ -1092,12 +1098,6 @@ Item {
} }
} }
Loader {
id: powerProfileWatcherLoader
active: SettingsData.osdPowerProfileEnabled
source: "Services/PowerProfileWatcher.qml"
}
LazyLoader { LazyLoader {
id: hyprlandOverviewLoader id: hyprlandOverviewLoader
active: CompositorService.isHyprland active: CompositorService.isHyprland

View File

@@ -9,6 +9,7 @@ import qs.Modules.Settings.DisplayConfig
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DMSShellIPC")
required property var powerMenuModalLoader required property var powerMenuModalLoader
required property var processListModalLoader required property var processListModalLoader
@@ -861,7 +862,7 @@ Item {
function set(key: string, value: string): string { function set(key: string, value: string): string {
if (!(key in SettingsData)) { if (!(key in SettingsData)) {
console.warn("Cannot set property, not found:", key); log.warn("Cannot set property, not found:", key);
return "SETTINGS_INVALID_KEY"; return "SETTINGS_INVALID_KEY";
} }
@@ -894,12 +895,12 @@ Item {
throw "Unsupported type"; throw "Unsupported type";
} }
console.warn("Setting:", key, value); log.warn("Setting:", key, value);
SettingsData[key] = value; SettingsData[key] = value;
SettingsData.saveSettings(); SettingsData.saveSettings();
return "SETTINGS_SET_SUCCESS"; return "SETTINGS_SET_SUCCESS";
} catch (e) { } catch (e) {
console.warn("Failed to set property:", key, "error:", e); log.warn("Failed to set property:", key, "error:", e);
return "SETTINGS_SET_FAILURE"; return "SETTINGS_SET_FAILURE";
} }
} }

View File

@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Widgets import qs.Widgets
@@ -7,6 +6,7 @@ import qs.Services
DankModal { DankModal {
id: root id: root
readonly property var log: Log.scoped("AppPickerModal")
property string title: I18n.tr("Select Application") property string title: I18n.tr("Select Application")
property string targetData: "" property string targetData: ""
@@ -30,52 +30,52 @@ DankModal {
onBackgroundClicked: close() onBackgroundClicked: close()
onDialogClosed: { onDialogClosed: {
searchQuery = "" searchQuery = "";
selectedIndex = 0 selectedIndex = 0;
keyboardNavigationActive = false keyboardNavigationActive = false;
} }
onOpened: { onOpened: {
searchQuery = "" searchQuery = "";
updateApplicationList() updateApplicationList();
selectedIndex = 0 selectedIndex = 0;
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) { if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = "" contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus() contentLoader.item.searchField.forceActiveFocus();
} }
}) });
} }
function updateApplicationList() { function updateApplicationList() {
applicationsModel.clear() applicationsModel.clear();
const apps = AppSearchService.applications const apps = AppSearchService.applications;
const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {} const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {};
let filteredApps = [] let filteredApps = [];
for (const app of apps) { for (const app of apps) {
if (!app || !app.categories) continue if (!app || !app.categories)
continue;
let matchesCategory = categoryFilter.length === 0 let matchesCategory = categoryFilter.length === 0;
if (categoryFilter.length > 0) { if (categoryFilter.length > 0) {
try { try {
for (const cat of app.categories) { for (const cat of app.categories) {
if (categoryFilter.includes(cat)) { if (categoryFilter.includes(cat)) {
matchesCategory = true matchesCategory = true;
break break;
} }
} }
} catch (e) { } catch (e) {
console.warn("AppPicker: Error iterating categories for", app.name, ":", e) log.warn("AppPicker: Error iterating categories for", app.name, ":", e);
continue continue;
} }
} }
if (matchesCategory) { if (matchesCategory) {
const name = app.name || "" const name = app.name || "";
const lowerName = name.toLowerCase() const lowerName = name.toLowerCase();
const lowerQuery = searchQuery.toLowerCase() const lowerQuery = searchQuery.toLowerCase();
if (searchQuery === "" || lowerName.includes(lowerQuery)) { if (searchQuery === "" || lowerName.includes(lowerQuery)) {
filteredApps.push({ filteredApps.push({
@@ -84,21 +84,21 @@ DankModal {
exec: app.exec || app.execString || "", exec: app.exec || app.execString || "",
startupClass: app.startupWMClass || "", startupClass: app.startupWMClass || "",
appData: app appData: app
}) });
} }
} }
} }
filteredApps.sort((a, b) => { filteredApps.sort((a, b) => {
const aId = a.appData.id || a.appData.execString || a.appData.exec || "" const aId = a.appData.id || a.appData.execString || a.appData.exec || "";
const bId = b.appData.id || b.appData.execString || b.appData.exec || "" const bId = b.appData.id || b.appData.execString || b.appData.exec || "";
const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0 const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0;
const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0 const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0;
if (aUsage !== bUsage) { if (aUsage !== bUsage) {
return bUsage - aUsage return bUsage - aUsage;
} }
return (a.name || "").localeCompare(b.name || "") return (a.name || "").localeCompare(b.name || "");
}) });
filteredApps.forEach(app => { filteredApps.forEach(app => {
applicationsModel.append({ applicationsModel.append({
@@ -107,10 +107,10 @@ DankModal {
exec: app.exec, exec: app.exec,
startupClass: app.startupClass, startupClass: app.startupClass,
appId: app.appData.id || app.appData.execString || app.appData.exec || "" appId: app.appData.id || app.appData.execString || app.appData.exec || ""
}) });
}) });
console.log("AppPicker: Found " + filteredApps.length + " applications") log.debug("AppPicker: Found " + filteredApps.length + " applications");
} }
onSearchQueryChanged: updateApplicationList() onSearchQueryChanged: updateApplicationList()
@@ -129,56 +129,57 @@ DankModal {
focus: true focus: true
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
root.close() root.close();
event.accepted = true event.accepted = true;
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (applicationsModel.count === 0) return if (applicationsModel.count === 0)
return;
// Toggle view mode with Tab key // Toggle view mode with Tab key
if (event.key === Qt.Key_Tab) { if (event.key === Qt.Key_Tab) {
root.viewMode = root.viewMode === "grid" ? "list" : "grid" root.viewMode = root.viewMode === "grid" ? "list" : "grid";
event.accepted = true event.accepted = true;
return return;
} }
if (root.viewMode === "grid") { if (root.viewMode === "grid") {
if (event.key === Qt.Key_Left) { if (event.key === Qt.Key_Left) {
root.keyboardNavigationActive = true root.keyboardNavigationActive = true;
root.selectedIndex = Math.max(0, root.selectedIndex - 1) root.selectedIndex = Math.max(0, root.selectedIndex - 1);
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Right) { } else if (event.key === Qt.Key_Right) {
root.keyboardNavigationActive = true root.keyboardNavigationActive = true;
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1) root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1);
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Up) { } else if (event.key === Qt.Key_Up) {
root.keyboardNavigationActive = true root.keyboardNavigationActive = true;
root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns) root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns);
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
root.keyboardNavigationActive = true root.keyboardNavigationActive = true;
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns) root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns);
event.accepted = true event.accepted = true;
} }
} else { } else {
if (event.key === Qt.Key_Up) { if (event.key === Qt.Key_Up) {
root.keyboardNavigationActive = true root.keyboardNavigationActive = true;
root.selectedIndex = Math.max(0, root.selectedIndex - 1) root.selectedIndex = Math.max(0, root.selectedIndex - 1);
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
root.keyboardNavigationActive = true root.keyboardNavigationActive = true;
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1) root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1);
event.accepted = true event.accepted = true;
} }
} }
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) { if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) {
const app = applicationsModel.get(root.selectedIndex) const app = applicationsModel.get(root.selectedIndex);
launchApplication(app) launchApplication(app);
} }
event.accepted = true event.accepted = true;
} }
} }
@@ -217,7 +218,7 @@ DankModal {
iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText
backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: { onClicked: {
root.viewMode = "list" root.viewMode = "list";
} }
} }
@@ -229,7 +230,7 @@ DankModal {
iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText
backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: { onClicked: {
root.viewMode = "grid" root.viewMode = "grid";
} }
} }
} }
@@ -257,42 +258,42 @@ DankModal {
keyForwardTargets: [appContent] keyForwardTargets: [appContent]
onTextEdited: { onTextEdited: {
root.searchQuery = text root.searchQuery = text;
} }
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close() root.close();
event.accepted = true event.accepted = true;
return return;
} }
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key) const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key);
const hasText = text.length > 0 const hasText = text.length > 0;
if (isEnterKey && hasText) { if (isEnterKey && hasText) {
if (root.keyboardNavigationActive && applicationsModel.count > 0) { if (root.keyboardNavigationActive && applicationsModel.count > 0) {
const app = applicationsModel.get(root.selectedIndex) const app = applicationsModel.get(root.selectedIndex);
launchApplication(app) launchApplication(app);
} else if (applicationsModel.count > 0) { } else if (applicationsModel.count > 0) {
const app = applicationsModel.get(0) const app = applicationsModel.get(0);
launchApplication(app) launchApplication(app);
} }
event.accepted = true event.accepted = true;
return return;
} }
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab] const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab];
const isNavigationKey = navigationKeys.includes(event.key) const isNavigationKey = navigationKeys.includes(event.key);
const isEmptyEnter = isEnterKey && !hasText const isEmptyEnter = isEnterKey && !hasText;
event.accepted = !(isNavigationKey || isEmptyEnter) event.accepted = !(isNavigationKey || isEmptyEnter);
} }
Connections { Connections {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (!root.shouldBeVisible) { if (!root.shouldBeVisible) {
searchField.focus = false searchField.focus = false;
} }
} }
@@ -303,12 +304,12 @@ DankModal {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: { height: {
let usedHeight = 40 + Theme.spacingS let usedHeight = 40 + Theme.spacingS;
usedHeight += 52 + Theme.spacingS usedHeight += 52 + Theme.spacingS;
if (root.showTargetData) { if (root.showTargetData) {
usedHeight += 36 + Theme.spacingS usedHeight += 36 + Theme.spacingS;
} }
return parent.height - usedHeight return parent.height - usedHeight;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: "transparent" color: "transparent"
@@ -320,14 +321,14 @@ DankModal {
property int itemSpacing: Theme.spacingS property int itemSpacing: Theme.spacingS
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) return if (index < 0 || index >= count)
return;
const itemY = index * (itemHeight + itemSpacing) const itemY = index * (itemHeight + itemSpacing);
const itemBottom = itemY + itemHeight const itemBottom = itemY + itemHeight;
if (itemY < contentY) { if (itemY < contentY) {
contentY = itemY contentY = itemY;
} else if (itemBottom > contentY + height) { } else if (itemBottom > contentY + height) {
contentY = itemBottom - height contentY = itemBottom - height;
} }
} }
@@ -343,9 +344,9 @@ DankModal {
spacing: itemSpacing spacing: itemSpacing
onCurrentIndexChanged: { onCurrentIndexChanged: {
root.selectedIndex = currentIndex root.selectedIndex = currentIndex;
if (root.keyboardNavigationActive) { if (root.keyboardNavigationActive) {
ensureVisible(currentIndex) ensureVisible(currentIndex);
} }
} }
@@ -360,11 +361,11 @@ DankModal {
hoverUpdatesSelection: true hoverUpdatesSelection: true
onItemClicked: (idx, modelData) => { onItemClicked: (idx, modelData) => {
launchApplication(modelData) launchApplication(modelData);
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
root.keyboardNavigationActive = false root.keyboardNavigationActive = false;
} }
} }
} }
@@ -373,14 +374,14 @@ DankModal {
id: appGrid id: appGrid
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) return if (index < 0 || index >= count)
return;
const itemY = Math.floor(index / root.gridColumns) * cellHeight const itemY = Math.floor(index / root.gridColumns) * cellHeight;
const itemBottom = itemY + cellHeight const itemBottom = itemY + cellHeight;
if (itemY < contentY) { if (itemY < contentY) {
contentY = itemY contentY = itemY;
} else if (itemBottom > contentY + height) { } else if (itemBottom > contentY + height) {
contentY = itemBottom - height contentY = itemBottom - height;
} }
} }
@@ -397,9 +398,9 @@ DankModal {
currentIndex: root.selectedIndex currentIndex: root.selectedIndex
onCurrentIndexChanged: { onCurrentIndexChanged: {
root.selectedIndex = currentIndex root.selectedIndex = currentIndex;
if (root.keyboardNavigationActive) { if (root.keyboardNavigationActive) {
ensureVisible(currentIndex) ensureVisible(currentIndex);
} }
} }
@@ -413,11 +414,11 @@ DankModal {
hoverUpdatesSelection: true hoverUpdatesSelection: true
onItemClicked: (idx, modelData) => { onItemClicked: (idx, modelData) => {
launchApplication(modelData) launchApplication(modelData);
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
root.keyboardNavigationActive = false root.keyboardNavigationActive = false;
} }
} }
} }
@@ -449,22 +450,22 @@ DankModal {
} }
function launchApplication(app) { function launchApplication(app) {
if (!app) return if (!app)
return;
root.applicationSelected(app, root.targetData) root.applicationSelected(app, root.targetData);
if (usageHistoryKey && app.appId) { if (usageHistoryKey && app.appId) {
const usageHistory = SettingsData[usageHistoryKey] || {} const usageHistory = SettingsData[usageHistoryKey] || {};
const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0 const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0;
usageHistory[app.appId] = { usageHistory[app.appId] = {
count: currentCount + 1, count: currentCount + 1,
lastUsed: Date.now(), lastUsed: Date.now(),
name: app.name name: app.name
} };
SettingsData.set(usageHistoryKey, usageHistory) SettingsData.set(usageHistoryKey, usageHistory);
} }
root.close() root.close();
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import qs.Widgets
DankModal { DankModal {
id: root id: root
readonly property var log: Log.scoped("BluetoothPairingModal")
layerNamespace: "dms:bluetooth-pairing" layerNamespace: "dms:bluetooth-pairing"
@@ -24,7 +25,7 @@ DankModal {
property string passkeyInput: "" property string passkeyInput: ""
function show(pairingData) { function show(pairingData) {
console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData)); log.debug("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
token = pairingData.token || ""; token = pairingData.token || "";
deviceName = pairingData.deviceName || ""; deviceName = pairingData.deviceName || "";
deviceAddress = pairingData.deviceAddr || ""; deviceAddress = pairingData.deviceAddr || "";
@@ -33,7 +34,7 @@ DankModal {
pinInput = ""; pinInput = "";
passkeyInput = ""; passkeyInput = "";
console.log("BluetoothPairingModal: Calling open()"); log.debug("Calling open()");
open(); open();
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {

View File

@@ -2,9 +2,11 @@ import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Modals import qs.Modals
import qs.Services
AppPickerModal { AppPickerModal {
id: root id: root
readonly property var log: Log.scoped("BrowserPickerModal")
property string url: "" property string url: ""
@@ -17,35 +19,44 @@ AppPickerModal {
showTargetData: true showTargetData: true
function shellEscape(str) { function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'" return "'" + str.replace(/'/g, "'\\''") + "'";
} }
onApplicationSelected: (app, url) => { onApplicationSelected: (app, url) => {
if (!app) return if (!app)
return;
let cmd = app.exec || "";
const escapedUrl = shellEscape(url);
let cmd = app.exec || "" let hasField = false;
const escapedUrl = shellEscape(url) if (cmd.includes("%u")) {
cmd = cmd.replace("%u", escapedUrl);
let hasField = false hasField = true;
if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUrl); hasField = true } } else if (cmd.includes("%U")) {
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUrl); hasField = true } cmd = cmd.replace("%U", escapedUrl);
else if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedUrl); hasField = true } hasField = true;
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedUrl); hasField = true } } else if (cmd.includes("%f")) {
cmd = cmd.replace("%f", escapedUrl);
cmd = cmd.replace(/%[ikc]/g, "") hasField = true;
} else if (cmd.includes("%F")) {
if (!hasField) { cmd = cmd.replace("%F", escapedUrl);
cmd += " " + escapedUrl hasField = true;
} }
console.log("BrowserPicker: Launching", cmd) cmd = cmd.replace(/%[ikc]/g, "");
if (!hasField) {
cmd += " " + escapedUrl;
}
log.debug("BrowserPicker: Launching", cmd);
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
}) });
} }
onViewModeChanged: { onViewModeChanged: {
SettingsData.set("browserPickerViewMode", viewMode) SettingsData.set("browserPickerViewMode", viewMode);
} }
} }

View File

@@ -6,6 +6,7 @@ import qs.Widgets
Item { Item {
id: thumbnail id: thumbnail
readonly property var log: Log.scoped("ClipboardThumbnail")
required property var entry required property var entry
required property string entryType required property string entryType
@@ -52,7 +53,7 @@ Item {
modal.activeImageLoads--; modal.activeImageLoads--;
} }
if (response.error) { if (response.error) {
console.warn("ClipboardThumbnail: Failed to load image:", entry.id); log.warn("Failed to load image:", entry.id);
return; return;
} }
const data = response.result?.data; const data = response.result?.data;

View File

@@ -7,6 +7,7 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankModal")
property string layerNamespace: "dms:modal" property string layerNamespace: "dms:modal"
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
@@ -246,10 +247,10 @@ Item {
return WlrLayershell.Overlay; return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom": case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "background": case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "overlay": case "overlay":
return WlrLayershell.Overlay; return WlrLayershell.Overlay;

View File

@@ -9,6 +9,7 @@ import qs.Widgets
DankModal { DankModal {
id: root id: root
readonly property var log: Log.scoped("DankColorPickerModal")
layerNamespace: "dms:color-picker" layerNamespace: "dms:color-picker"
@@ -50,17 +51,17 @@ DankModal {
function toggle() { function toggle() {
if (shouldBeVisible) { if (shouldBeVisible) {
hide(); hide();
} else { } else {
show(); show();
} }
} }
function toggleInstant() { function toggleInstant() {
if (shouldBeVisible) { if (shouldBeVisible) {
hideInstant(); hideInstant();
} else { } else {
show(); show();
} }
} }
@@ -111,7 +112,7 @@ DankModal {
hideInstant(); hideInstant();
Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => { Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("dms color pick exited with code:", exitCode); log.warn("dms color pick exited with code:", exitCode);
root.show(); root.show();
return; return;
} }
@@ -120,11 +121,11 @@ DankModal {
if (result.hex) { if (result.hex) {
applyPickedColor(result.hex); applyPickedColor(result.hex);
} else { } else {
console.warn("Failed to parse dms color pick output: missing hex"); log.warn("Failed to parse dms color pick output: missing hex");
root.show(); root.show();
} }
} catch (e) { } catch (e) {
console.warn("Failed to parse dms color pick JSON:", e); log.warn("Failed to parse dms color pick JSON:", e);
root.show(); root.show();
} }
}, 0, Proc.noTimeout); }, 0, Proc.noTimeout);
@@ -142,39 +143,39 @@ DankModal {
onBackgroundClicked: hide() onBackgroundClicked: hide()
IpcHandler { IpcHandler {
function open(): string { function open(): string {
root.show(); root.show();
return "COLOR_PICKER_MODAL_OPEN_SUCCESS"; return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
} }
function openColor(color: string): string { function openColor(color: string): string {
root.selectedColor = Qt.color(color); root.selectedColor = Qt.color(color);
root.currentColor = Qt.color(color); root.currentColor = Qt.color(color);
root.updateFromColor(Qt.color(color)); root.updateFromColor(Qt.color(color));
return open(); return open();
} }
function close(): string { function close(): string {
root.hide(); root.hide();
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS"; return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
} }
function closeInstant(): string { function closeInstant(): string {
root.hideInstant(); root.hideInstant();
return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS"; return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS";
} }
function toggle(): string { function toggle(): string {
root.toggle(); root.toggle();
return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS"; return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS";
} }
function toggleInstant(): string { function toggleInstant(): string {
root.toggleInstant(); root.toggleInstant();
return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS"; return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS";
} }
target: "color-picker" target: "color-picker"
} }
content: Component { content: Component {

View File

@@ -1881,7 +1881,7 @@ Item {
function openTerminal(path) { function openTerminal(path) {
if (!path) if (!path)
return; return;
var terminal = Quickshell.env("TERMINAL") || "xterm"; var terminal = SessionData.resolveTerminal() || "xterm";
Quickshell.execDetached({ Quickshell.execDetached({
command: [terminal], command: [terminal],
workingDirectory: path workingDirectory: path

View File

@@ -8,6 +8,7 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankLauncherV2Modal")
visible: false visible: false
@@ -323,10 +324,10 @@ Item {
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom": case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer."); log.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "background": case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer."); log.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "overlay": case "overlay":
return WlrLayershell.Overlay; return WlrLayershell.Overlay;

View File

@@ -1,10 +1,12 @@
import QtQuick import QtQuick
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("GreeterDoctorPage")
property bool isRunning: false property bool isRunning: false
property bool hasRun: false property bool hasRun: false
@@ -228,9 +230,7 @@ Item {
text: { text: {
if (root.errorCount === 0) if (root.errorCount === 0)
return I18n.tr("All checks passed", "greeter doctor page success"); return I18n.tr("All checks passed", "greeter doctor page success");
return root.errorCount === 1 return root.errorCount === 1 ? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount)
: I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
@@ -412,7 +412,7 @@ Item {
else else
root.selectedFilter = "ok"; root.selectedFilter = "ok";
} catch (e) { } catch (e) {
console.error("GreeterDoctorPage: Failed to parse doctor output:", e); log.error("Failed to parse doctor output:", e);
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: root id: root
readonly property var log: Log.scoped("GreeterModal")
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
property int currentPage: 0 property int currentPage: 0
@@ -105,7 +106,7 @@ FloatingWindow {
root.cheatsheetData = JSON.parse(trimmed); root.cheatsheetData = JSON.parse(trimmed);
root.cheatsheetLoaded = true; root.cheatsheetLoaded = true;
} catch (e) { } catch (e) {
console.warn("Greeter: Failed to parse cheatsheet:", e); log.warn("Greeter: Failed to parse cheatsheet:", e);
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: processListModal id: processListModal
readonly property var log: Log.scoped("ProcessListModal")
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
property int currentTab: 0 property int currentTab: 0
@@ -22,7 +23,7 @@ FloatingWindow {
function show() { function show() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); log.warn("dgop is not available");
return; return;
} }
visible = true; visible = true;
@@ -36,7 +37,7 @@ FloatingWindow {
function toggle() { function toggle() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); log.warn("dgop is not available");
return; return;
} }
visible = !visible; visible = !visible;
@@ -44,7 +45,7 @@ FloatingWindow {
function focusOrToggle() { function focusOrToggle() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); log.warn("dgop is not available");
return; return;
} }
if (visible) { if (visible) {

View File

@@ -164,7 +164,8 @@ Rectangle {
"id": "updater", "id": "updater",
"text": I18n.tr("System Updater"), "text": I18n.tr("System Updater"),
"icon": "refresh", "icon": "refresh",
"tabIndex": 20 "tabIndex": 20,
"updaterOnly": true
}, },
{ {
"id": "desktop_widgets", "id": "desktop_widgets",
@@ -340,6 +341,8 @@ Rectangle {
return false; return false;
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23)) if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
return false; return false;
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
return false;
return true; return true;
} }

View File

@@ -7,6 +7,7 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: root id: root
readonly property var log: Log.scoped("WorkspaceRenameModal")
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
@@ -39,7 +40,7 @@ FloatingWindow {
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
HyprlandService.renameWorkspace(name); HyprlandService.renameWorkspace(name);
} else { } else {
console.warn("WorkspaceRenameModal: rename not supported for this compositor"); log.warn("rename not supported for this compositor");
} }
} }

View File

@@ -7,6 +7,7 @@ import "../utils/layout.js" as LayoutUtils
Column { Column {
id: root id: root
readonly property var log: Log.scoped("DragDropGrid")
property bool editMode: false property bool editMode: false
property string expandedSection: "" property string expandedSection: ""
@@ -988,7 +989,7 @@ Column {
return true; return true;
} }
} catch (e) { } catch (e) {
console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading"); log.warn("stale plugin component for", pluginId, "- reloading");
PluginService.reloadPlugin(pluginId); PluginService.reloadPlugin(pluginId);
} }
return false; return false;

View File

@@ -6,6 +6,7 @@ import "../utils/widgets.js" as WidgetUtils
QtObject { QtObject {
id: root id: root
readonly property var log: Log.scoped("WidgetModel")
property var vpnBuiltinInstance: null property var vpnBuiltinInstance: null
property var cupsBuiltinInstance: null property var cupsBuiltinInstance: null
@@ -26,7 +27,7 @@ QtObject {
const widgets = SettingsData.controlCenterWidgets || []; const widgets = SettingsData.controlCenterWidgets || [];
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn"); const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn");
if (!hasVpnWidget && vpnLoader.active) { if (!hasVpnWidget && vpnLoader.active) {
console.log("VpnWidget: No VPN widget in control center, deactivating loader"); log.debug("VpnWidget: No VPN widget in control center, deactivating loader");
vpnLoader.active = false; vpnLoader.active = false;
} }
} }
@@ -55,7 +56,7 @@ QtObject {
const widgets = SettingsData.controlCenterWidgets || []; const widgets = SettingsData.controlCenterWidgets || [];
const hasCupsWidget = widgets.some(w => w.id === "builtin_cups"); const hasCupsWidget = widgets.some(w => w.id === "builtin_cups");
if (!hasCupsWidget && cupsLoader.active) { if (!hasCupsWidget && cupsLoader.active) {
console.log("CupsWidget: No CUPS widget in control center, deactivating loader"); log.debug("CupsWidget: No CUPS widget in control center, deactivating loader");
cupsLoader.active = false; cupsLoader.active = false;
} }
} }

View File

@@ -1,9 +1,11 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modules.ControlCenter.Widgets import qs.Modules.ControlCenter.Widgets
import qs.Services
CompoundPill { CompoundPill {
id: root id: root
readonly property var log: Log.scoped("ColorPickerPill")
property var colorPickerModal: null property var colorPickerModal: null
@@ -14,14 +16,14 @@ CompoundPill {
secondaryText: I18n.tr("Choose a color") secondaryText: I18n.tr("Choose a color")
onToggled: { onToggled: {
console.log("ColorPickerPill toggled, modal:", colorPickerModal); log.debug("ColorPickerPill toggled, modal:", colorPickerModal);
if (colorPickerModal) { if (colorPickerModal) {
colorPickerModal.show(); colorPickerModal.show();
} }
} }
onExpandClicked: { onExpandClicked: {
console.log("ColorPickerPill expandClicked, modal:", colorPickerModal); log.debug("ColorPickerPill expandClicked, modal:", colorPickerModal);
if (colorPickerModal) { if (colorPickerModal) {
colorPickerModal.show(); colorPickerModal.show();
} }

View File

@@ -7,6 +7,7 @@ import qs.Services
PanelWindow { PanelWindow {
id: barWindow id: barWindow
readonly property var log: Log.scoped("DankBarWindow")
required property var rootWindow required property var rootWindow
required property var barConfig required property var barConfig
@@ -164,7 +165,7 @@ PanelWindow {
barWindow.BackgroundEffect.blurRegion = region; barWindow.BackgroundEffect.blurRegion = region;
barWindow.blurRegion = region; barWindow.blurRegion = region;
} catch (e) { } catch (e) {
console.warn("BarBlur: Failed to create blur region:", e); log.warn("BarBlur: Failed to create blur region:", e);
} }
} }
@@ -534,11 +535,11 @@ PanelWindow {
Connections { Connections {
target: PluginService target: PluginService
function onPluginLoaded(pluginId) { function onPluginLoaded(pluginId) {
console.info("DankBar: Plugin loaded:", pluginId); log.info("DankBar: Plugin loaded:", pluginId);
SettingsData.widgetDataChanged(); SettingsData.widgetDataChanged();
} }
function onPluginUnloaded(pluginId) { function onPluginUnloaded(pluginId) {
console.info("DankBar: Plugin unloaded:", pluginId); log.info("DankBar: Plugin unloaded:", pluginId);
SettingsData.widgetDataChanged(); SettingsData.widgetDataChanged();
} }
} }

View File

@@ -0,0 +1,461 @@
import QtQuick
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: systemUpdatePopout
layerNamespace: "dms:system-update"
property var parentWidget: null
property var triggerScreen: null
Ref {
service: SystemUpdateService
}
property bool _reopenAfterUpgrade: false
readonly property bool polkitModalOpen: PopoutService.polkitAuthModal?.visible ?? false
readonly property bool anyModalOpen: polkitModalOpen
backgroundInteractive: !anyModalOpen
customKeyboardFocus: {
if (!shouldBeVisible)
return WlrKeyboardFocus.None;
if (anyModalOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
Connections {
target: SystemUpdateService
function onIsUpgradingChanged() {
if (SystemUpdateService.isUpgrading) {
return;
}
if (!systemUpdatePopout._reopenAfterUpgrade) {
return;
}
systemUpdatePopout._reopenAfterUpgrade = false;
systemUpdatePopout.open();
}
}
popupWidth: 440
popupHeight: 560
triggerWidth: 55
positioning: ""
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: {
if (anyModalOpen)
return;
close();
}
onShouldBeVisibleChanged: {
if (!shouldBeVisible) {
return;
}
const stale = !SystemUpdateService.lastCheckUnix || (Date.now() / 1000 - SystemUpdateService.lastCheckUnix) > 300;
if (stale && !SystemUpdateService.isChecking && !SystemUpdateService.isUpgrading) {
SystemUpdateService.checkForUpdates();
}
}
content: Component {
Rectangle {
id: updaterPanel
color: "transparent"
focus: true
readonly property bool hasTerminalBackend: (SystemUpdateService.backends || []).some(b => b.runsInTerminal === true)
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
systemUpdatePopout.close();
event.accepted = true;
}
}
Component.onCompleted: {
if (systemUpdatePopout.shouldBeVisible) {
forceActiveFocus();
}
}
Item {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
height: 40
StyledText {
text: I18n.tr("System Updates")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: {
switch (true) {
case SystemUpdateService.isUpgrading:
return I18n.tr("Upgrading...");
case SystemUpdateService.isChecking:
return I18n.tr("Checking...");
case SystemUpdateService.hasError:
return I18n.tr("Error");
case SystemUpdateService.updateCount === 0:
return I18n.tr("Up to date");
case SystemUpdateService.updateCount === 1:
return I18n.tr("%1 update").arg(SystemUpdateService.updateCount);
default:
return I18n.tr("%1 updates").arg(SystemUpdateService.updateCount);
}
}
font.pixelSize: Theme.fontSizeMedium
color: SystemUpdateService.hasError ? Theme.error : Theme.surfaceVariantText
}
DankActionButton {
id: refreshButton
buttonSize: 28
iconName: "refresh"
iconSize: 18
iconColor: Theme.surfaceText
enabled: !SystemUpdateService.isChecking && !SystemUpdateService.isUpgrading
opacity: enabled ? 1.0 : 0.5
onClicked: SystemUpdateService.checkForUpdates()
RotationAnimation {
target: refreshButton
property: "rotation"
from: 0
to: 360
duration: 1000
running: SystemUpdateService.isChecking
loops: Animation.Infinite
onRunningChanged: {
if (!running) {
refreshButton.rotation = 0;
}
}
}
}
}
}
StyledText {
id: backendsRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingS
visible: SystemUpdateService.backends.length > 0 && !SystemUpdateService.isUpgrading
text: {
const names = (SystemUpdateService.backends || []).map(b => b.displayName).join(", ");
return I18n.tr("Backends: %1").arg(names);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
elide: Text.ElideRight
}
Row {
id: buttonsRow
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingL
spacing: Theme.spacingM
height: 44
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: primaryMouseArea.containsMouse && primaryMouseArea.enabled ? Theme.primaryHover : Theme.secondaryHover
opacity: primaryMouseArea.enabled ? 1.0 : 0.5
StyledText {
anchors.centerIn: parent
text: SystemUpdateService.isUpgrading ? I18n.tr("Cancel") : I18n.tr("Update All")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
}
MouseArea {
id: primaryMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: SystemUpdateService.isUpgrading || SystemUpdateService.updateCount > 0
onClicked: {
if (SystemUpdateService.isUpgrading) {
SystemUpdateService.cancelUpdates();
return;
}
const opts = {
includeFlatpak: SettingsData.updaterIncludeFlatpak,
includeAUR: SettingsData.updaterAllowAUR,
terminal: SessionData.terminalOverride
};
if (updaterPanel.hasTerminalBackend) {
systemUpdatePopout._reopenAfterUpgrade = true;
SystemUpdateService.runUpdates(opts);
systemUpdatePopout.close();
return;
}
SystemUpdateService.runUpdates(opts);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
StyledText {
anchors.centerIn: parent
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: systemUpdatePopout.close()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Rectangle {
id: bodyArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: backendsRow.visible ? backendsRow.bottom : header.bottom
anchors.bottom: buttonsRow.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
StyledText {
id: statusText
anchors.fill: parent
anchors.margins: Theme.spacingM
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
visible: !SystemUpdateService.isUpgrading && (SystemUpdateService.updateCount === 0 || SystemUpdateService.hasError || SystemUpdateService.isChecking)
text: {
switch (true) {
case SystemUpdateService.hasError:
return I18n.tr("Failed: %1").arg(SystemUpdateService.errorMessage);
case !SystemUpdateService.helperAvailable:
return I18n.tr("No supported package manager found.");
case SystemUpdateService.isChecking:
return I18n.tr("Checking for updates...");
default:
return I18n.tr("Your system is up to date!");
}
}
font.pixelSize: Theme.fontSizeMedium
color: SystemUpdateService.hasError ? Theme.error : Theme.surfaceText
wrapMode: Text.WordWrap
}
DankListView {
id: packagesList
anchors.fill: parent
anchors.margins: Theme.spacingS
visible: !SystemUpdateService.isUpgrading && SystemUpdateService.updateCount > 0 && !SystemUpdateService.hasError && !SystemUpdateService.isChecking
clip: true
spacing: Theme.spacingXS
model: SystemUpdateService.availableUpdates
delegate: Rectangle {
width: ListView.view.width
height: 48
radius: Theme.cornerRadius
color: packageMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
required property var modelData
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 64
height: 18
radius: 9
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.18)
StyledText {
anchors.centerIn: parent
text: modelData.repo || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 64 - Theme.spacingS
spacing: 2
StyledText {
width: parent.width
text: modelData.name || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: {
const from = modelData.fromVersion || "";
const to = modelData.toVersion || "";
if (from && to) {
return `${from} ${to}`;
}
return to || from || "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
}
MouseArea {
id: packageMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.changelogUrl ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (modelData.changelogUrl) {
Qt.openUrlExternally(modelData.changelogUrl);
}
}
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
visible: SystemUpdateService.isUpgrading && updaterPanel.hasTerminalBackend
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "terminal"
size: 32
color: Theme.primary
}
StyledText {
width: parent.width
text: I18n.tr("Running in terminal")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: parent.width
text: I18n.tr("AUR helpers are interactive — see the terminal window for prompts. This popout will return to idle when the upgrade exits.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: SystemUpdateService.isUpgrading && !updaterPanel.hasTerminalBackend
contentWidth: width
contentHeight: logText.implicitHeight
clip: true
onContentHeightChanged: {
if (contentHeight > height) {
contentY = contentHeight - height;
}
}
StyledText {
id: logText
width: parent.width
text: (SystemUpdateService.recentLog || []).join("\n")
font.family: Theme.monoFontFamily || "monospace"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.NoWrap
}
}
}
}
}
}

View File

@@ -9,6 +9,7 @@ import qs.Widgets
BasePill { BasePill {
id: root id: root
readonly property var log: Log.scoped("AppsDock")
enableBackgroundHover: false enableBackgroundHover: false
enableCursor: false enableCursor: false
@@ -550,9 +551,9 @@ BasePill {
showBadge: root.showOverflowBadge showBadge: root.showOverflowBadge
z: 10 z: 10
onClicked: { onClicked: {
console.log("Overflow button clicked! Current state:", root.overflowExpanded); log.debug("Overflow button clicked! Current state:", root.overflowExpanded);
root.overflowExpanded = !root.overflowExpanded; root.overflowExpanded = !root.overflowExpanded;
console.log("New state:", root.overflowExpanded); log.debug("New state:", root.overflowExpanded);
} }
} }

View File

@@ -7,6 +7,7 @@ import qs.Widgets
BasePill { BasePill {
id: battery id: battery
readonly property var log: Log.scoped("Battery")
property bool batteryPopupVisible: false property bool batteryPopupVisible: false
property var popoutTarget: null property var popoutTarget: null
@@ -130,13 +131,13 @@ BasePill {
// Check if this is a touchpad // Check if this is a touchpad
if (delta !== 120 && delta !== -120) { if (delta !== 120 && delta !== -120) {
touchpadAccumulator += delta; touchpadAccumulator += delta;
console.info("Acc: "+touchpadAccumulator); log.info("Acc: " + touchpadAccumulator);
if (Math.abs(touchpadAccumulator) < 500) if (Math.abs(touchpadAccumulator) < 500)
return; return;
delta = touchpadAccumulator; delta = touchpadAccumulator;
touchpadAccumulator = 0; touchpadAccumulator = 0;
} }
console.info("Trigger! Delta: "+delta) log.info("Trigger! Delta: " + delta);
// This is after the other delta checks so it only shows on valid Y scroll // This is after the other delta checks so it only shows on valid Y scroll
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
@@ -149,11 +150,14 @@ BasePill {
var index = profiles.findIndex(profile => PowerProfiles.profile === profile); var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
// Step once based on mouse wheel direction // Step once based on mouse wheel direction
if (delta > 0) index += 1; if (delta > 0)
else index -= 1; index += 1;
else
index -= 1;
// Already at end of list, can't go further // Already at end of list, can't go further
if (index < 0 || index >= profiles.length) return; if (index < 0 || index >= profiles.length)
return;
// Set new profile // Set new profile
PowerProfiles.profile = profiles[index]; PowerProfiles.profile = profiles[index];

View File

@@ -10,6 +10,36 @@ BasePill {
readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool playerAvailable: activePlayer !== null readonly property bool playerAvailable: activePlayer !== null
readonly property bool _hoverPreview: MprisController.isFirefoxYoutubeHoverPreview(activePlayer)
readonly property bool _isPlaying: !!activePlayer && activePlayer.playbackState === 1 && !_hoverPreview
property string _stableTitle: ""
property string _stableArtist: ""
Connections {
target: root.activePlayer
function onTrackTitleChanged() {
root._syncMeta();
}
function onTrackArtistChanged() {
root._syncMeta();
}
}
onActivePlayerChanged: _syncMeta()
function _syncMeta() {
if (!activePlayer) {
_stableTitle = "";
_stableArtist = "";
return;
}
if (MprisController.isFirefoxYoutubeHoverPreview(activePlayer))
return;
_stableTitle = activePlayer.trackTitle || "";
_stableArtist = activePlayer.trackArtist || "";
}
readonly property bool __isChromeBrowser: { readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity) if (!activePlayer?.identity)
return false; return false;
@@ -212,15 +242,15 @@ BasePill {
height: 24 height: 24
radius: 12 radius: 12
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover color: root._isPlaying ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3 opacity: activePlayer ? 1 : 0.3
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" name: root._isPlaying ? "pause" : "play_arrow"
size: 14 size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary color: root._isPlaying ? Theme.background : Theme.primary
} }
MouseArea { MouseArea {
@@ -279,12 +309,10 @@ BasePill {
readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari") readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
property string displayText: { property string displayText: {
if (!activePlayer || !activePlayer.trackTitle) { if (!activePlayer || !root._stableTitle)
return ""; return "";
} const title = isWebMedia ? root._stableTitle : (root._stableTitle || "Unknown Track");
const subtitle = isWebMedia ? (root._stableArtist || cachedIdentity) : (root._stableArtist || "");
const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track");
const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || "");
return subtitle.length > 0 ? title + " • " + subtitle : title; return subtitle.length > 0 ? title + " • " + subtitle : title;
} }
@@ -444,15 +472,15 @@ BasePill {
height: 24 height: 24
radius: 12 radius: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover color: root._isPlaying ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3 opacity: activePlayer ? 1 : 0.3
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" name: root._isPlaying ? "pause" : "play_arrow"
size: 14 size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary color: root._isPlaying ? Theme.background : Theme.primary
} }
MouseArea { MouseArea {

View File

@@ -7,41 +7,33 @@ import qs.Widgets
BasePill { BasePill {
id: root id: root
property var widgetData: null
property bool isActive: false property bool isActive: false
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0 readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking readonly property bool isChecking: SystemUpdateService.isChecking
readonly property bool shouldHide: SettingsData.updaterHideWidget && !hasUpdates && !isChecking && !SystemUpdateService.hasError readonly property bool isClean: SystemUpdateService.sysupdateAvailable && !hasUpdates && !isChecking && !SystemUpdateService.hasError
readonly property bool hideWhenIdle: widgetData?.hideWhenIdle === true
readonly property bool shouldHide: hideWhenIdle && isClean
width: shouldHide ? 0 : (isVerticalOrientation ? barThickness : visualWidth)
height: shouldHide ? 0 : (isVerticalOrientation ? visualHeight : barThickness)
visible: !shouldHide
opacity: shouldHide ? 0 : 1 opacity: shouldHide ? 0 : 1
states: [ Behavior on width {
State { NumberAnimation {
name: "hidden_horizontal" duration: Theme.shortDuration
when: root.shouldHide && !isVerticalOrientation easing.type: Theme.standardEasing
PropertyChanges {
target: root
width: 0
}
},
State {
name: "hidden_vertical"
when: root.shouldHide && isVerticalOrientation
PropertyChanges {
target: root
height: 0
}
} }
] }
transitions: [ Behavior on height {
Transition { NumberAnimation {
NumberAnimation { duration: Theme.shortDuration
properties: "width,height" easing.type: Theme.standardEasing
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
} }
] }
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {

View File

@@ -6,6 +6,7 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
readonly property var log: Log.scoped("CalendarOverviewCard")
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
@@ -521,7 +522,7 @@ Rectangle {
onClicked: { onClicked: {
if (modelData.url && modelData.url !== "") { if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) { if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Failed to open URL: " + modelData.url); log.warn("Failed to open URL: " + modelData.url);
} else { } else {
root.closeDash(); root.closeDash();
} }

View File

@@ -7,6 +7,7 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("WeatherTab")
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
@@ -45,7 +46,7 @@ Item {
hourlyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.hourlyForecast?.length ?? 1) - 1, WeatherService.calendarHourDifference((new Date()), date) + (new Date()).getHours())); hourlyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.hourlyForecast?.length ?? 1) - 1, WeatherService.calendarHourDifference((new Date()), date) + (new Date()).getHours()));
} }
} catch (e) { } catch (e) {
console.warn("Weather Date Sync Error:", e); log.warn("Weather Date Sync Error:", e);
} }
syncing = false; syncing = false;

View File

@@ -5,9 +5,11 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv import "GreetdEnv.js" as GreetdEnv
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("GreetdMemory")
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter" readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
readonly property string sessionConfigPath: greetCfgDir + "/session.json" readonly property string sessionConfigPath: greetCfgDir + "/session.json"
@@ -42,7 +44,7 @@ Singleton {
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false; nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false;
} }
} catch (e) { } catch (e) {
console.warn("Failed to parse greeter session config:", e); log.warn("Failed to parse greeter session config:", e);
} }
} }
@@ -56,7 +58,7 @@ Singleton {
if (!rememberLastSession || !rememberLastUser) if (!rememberLastSession || !rememberLastUser)
saveMemory(); saveMemory();
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd memory:", e); log.warn("Failed to parse greetd memory:", e);
} }
} }
@@ -122,7 +124,7 @@ Singleton {
parseSessionConfig(sessionConfigFileView.text()); parseSessionConfig(sessionConfigFileView.text());
} }
onLoadFailed: error => { onLoadFailed: error => {
console.warn("Could not load greeter session config from", root.sessionConfigPath, "error:", error); log.warn("Could not load greeter session config from", root.sessionConfigPath, "error:", error);
} }
} }
} }

View File

@@ -5,10 +5,12 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
import "GreetdEnv.js" as GreetdEnv import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("GreetdSettings")
readonly property string configPath: { readonly property string configPath: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
@@ -81,8 +83,7 @@ Singleton {
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple"; currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""; customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
registryThemeVariants = settings.registryThemeVariants !== undefined ? registryThemeVariants = settings.registryThemeVariants !== undefined ? settings.registryThemeVariants : ({});
settings.registryThemeVariants : ({});
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"; matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true; use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false; showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
@@ -142,7 +143,7 @@ Singleton {
Theme.applyGreeterTheme(currentThemeName); Theme.applyGreeterTheme(currentThemeName);
} }
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd settings:", e); log.warn("Failed to parse greetd settings:", e);
} finally { } finally {
settingsLoaded = true; settingsLoaded = true;
} }
@@ -192,7 +193,7 @@ Singleton {
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
onLoadFailed: error => { onLoadFailed: error => {
console.warn("Failed to load greetd settings:", error); log.warn("Failed to load greetd settings:", error);
root.parseSettings(""); root.parseSettings("");
} }
} }

View File

@@ -1,9 +1,11 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import qs.Services
Item { Item {
id: keyboard_controller id: keyboard_controller
readonly property var log: Log.scoped("KeyboardController")
// reference on the TextInput // reference on the TextInput
property Item target property Item target
@@ -14,21 +16,20 @@ Item {
function show() { function show() {
if (!isKeyboardActive && keyboard === null) { if (!isKeyboardActive && keyboard === null) {
keyboard = keyboardComponent.createObject( keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
keyboard_controller.rootObject) keyboard.target = keyboard_controller.target;
keyboard.target = keyboard_controller.target keyboard.dismissed.connect(hide);
keyboard.dismissed.connect(hide) isKeyboardActive = true;
isKeyboardActive = true
} else } else
console.log("The keyboard is already shown") log.debug("The keyboard is already shown");
} }
function hide() { function hide() {
if (isKeyboardActive && keyboard !== null) { if (isKeyboardActive && keyboard !== null) {
keyboard.destroy() keyboard.destroy();
isKeyboardActive = false isKeyboardActive = false;
} else } else
console.log("The keyboard is already hidden") log.debug("The keyboard is already hidden");
} }
// private // private

View File

@@ -14,6 +14,7 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("LockScreenContent")
function encodeFileUrl(path) { function encodeFileUrl(path) {
if (!path) if (!path)
@@ -95,9 +96,9 @@ Item {
if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) { if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
DMSService.sendRequest("loginctl.lockerReady", null, resp => { DMSService.sendRequest("loginctl.lockerReady", null, resp => {
if (resp?.error) if (resp?.error)
console.warn("lockerReady failed:", resp.error); log.warn("lockerReady failed:", resp.error);
else else
console.log("lockerReady sent (afterAnimating/afterRendering)"); log.debug("lockerReady sent (afterAnimating/afterRendering)");
}); });
} }
} }
@@ -803,7 +804,7 @@ Item {
} }
if (pam.passwd.active) { if (pam.passwd.active) {
console.log("PAM is active, ignoring input"); log.debug("PAM is active, ignoring input");
event.accepted = true; event.accepted = true;
return; return;
} }
@@ -1622,7 +1623,7 @@ Item {
buttonSize: 40 buttonSize: 40
onClicked: { onClicked: {
if (demoMode) { if (demoMode) {
console.log("Demo: Power Menu"); log.debug("Demo: Power Menu");
} else { } else {
powerMenu.show(); powerMenu.show();
} }

View File

@@ -3,9 +3,11 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Services
PanelWindow { PanelWindow {
id: root id: root
readonly property var log: Log.scoped("LockScreenDemo")
property bool demoActive: false property bool demoActive: false
@@ -25,12 +27,12 @@ PanelWindow {
color: "transparent" color: "transparent"
function showDemo(): void { function showDemo(): void {
console.log("Showing lock screen demo"); log.debug("Showing lock screen demo");
demoActive = true; demoActive = true;
} }
function hideDemo(): void { function hideDemo(): void {
console.log("Hiding lock screen demo"); log.debug("Hiding lock screen demo");
demoActive = false; demoActive = false;
} }

View File

@@ -7,6 +7,7 @@ import qs.Services
Item { Item {
id: root id: root
readonly property var log: Log.scoped("VideoScreensaver")
required property string screenName required property string screenName
property bool active: false property bool active: false
@@ -53,7 +54,7 @@ Item {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0 || !videoPicker.result) { if (exitCode !== 0 || !videoPicker.result) {
console.warn("VideoScreensaver: no video found in folder"); log.warn("no video found in folder");
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("No video found in folder")); ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("No video found in folder"));
root.dismiss(); root.dismiss();
} }
@@ -98,14 +99,14 @@ Item {
`, background, "VideoScreensaver.VideoPlayer"); `, background, "VideoScreensaver.VideoPlayer");
videoPlayer.errorOccurred.connect((error, errorString) => { videoPlayer.errorOccurred.connect((error, errorString) => {
console.warn("VideoScreensaver: playback error:", errorString); log.warn("playback error:", errorString);
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString); ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString);
root.dismiss(); root.dismiss();
}); });
return true; return true;
} catch (e) { } catch (e) {
console.warn("VideoScreensaver: Failed to create video player:", e); log.warn("Failed to create video player:", e);
return false; return false;
} }
} }

View File

@@ -47,6 +47,9 @@ DankOSD {
} }
property bool _pendingShow: false property bool _pendingShow: false
property string _displayTitle: ""
property string _displayArtist: ""
property string _displayAlbum: ""
Timer { Timer {
id: iconDebounce id: iconDebounce
@@ -105,6 +108,12 @@ DankOSD {
return; return;
if (!SettingsData.osdMediaPlaybackEnabled) if (!SettingsData.osdMediaPlaybackEnabled)
return; return;
if (MprisController.isFirefoxYoutubeHoverPreview(player))
return;
root._displayTitle = player.trackTitle || "";
root._displayArtist = player.trackArtist || "";
root._displayAlbum = player.trackAlbum || "";
root.updatePlaybackIcon(); root.updatePlaybackIcon();
TrackArtService.loadArtwork(player.trackArtUrl); TrackArtService.loadArtwork(player.trackArtUrl);
@@ -254,7 +263,7 @@ DankOSD {
StyledText { StyledText {
id: topText id: topText
width: parent.width width: parent.width
text: player ? `${player.trackTitle || I18n.tr("Unknown Title")}` : "" text: player ? (root._displayTitle || I18n.tr("Unknown Title")) : ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -265,7 +274,7 @@ DankOSD {
StyledText { StyledText {
id: bottomText id: bottomText
width: parent.width width: parent.width
text: player ? ((player.trackArtist || I18n.tr("Unknown Artist")) + (player.trackAlbum ? ` ${player.trackAlbum}` : "")) : "" text: player ? ((root._displayArtist || I18n.tr("Unknown Artist")) + (root._displayAlbum ? ` ${root._displayAlbum}` : "")) : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Light font.weight: Font.Light
color: Theme.surfaceText color: Theme.surfaceText

View File

@@ -407,6 +407,8 @@ Item {
item.widgetWidth = Qt.binding(() => contentLoader.width); item.widgetWidth = Qt.binding(() => contentLoader.width);
if (item.widgetHeight !== undefined) if (item.widgetHeight !== undefined)
item.widgetHeight = Qt.binding(() => contentLoader.height); item.widgetHeight = Qt.binding(() => contentLoader.height);
if (item.screen !== undefined)
item.screen = Qt.binding(() => root.screen);
} }
} }

View File

@@ -1,10 +1,12 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("PluginSettings")
required property string pluginId required property string pluginId
property var pluginService: null property var pluginService: null
@@ -131,7 +133,7 @@ Item {
return; return;
} }
if (!hasPermission) { if (!hasPermission) {
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission"); log.warn("Plugin", pluginId, "does not have settings_write permission");
return; return;
} }
if (pluginService.savePluginData) { if (pluginService.savePluginData) {

View File

@@ -301,10 +301,10 @@ Item {
clip: true clip: true
spacing: 2 spacing: 2
add: root.searchText.length > 0 ? ListViewTransitions.add : null add: null
remove: root.searchText.length > 0 ? ListViewTransitions.remove : null remove: null
displaced: root.searchText.length > 0 ? ListViewTransitions.displaced : null displaced: null
move: root.searchText.length > 0 ? ListViewTransitions.move : null move: null
model: ScriptModel { model: ScriptModel {
values: root.cachedProcesses values: root.cachedProcesses

View File

@@ -189,10 +189,10 @@ Item {
StyledText { StyledText {
text: { text: {
if (!SystemUpdateService.shellVersion && !DMSService.cliVersion) if (!ShellVersionService.shellVersion && !DMSService.cliVersion)
return "dms"; return "dms";
let version = SystemUpdateService.shellVersion || ""; let version = ShellVersionService.shellVersion || "";
let cliVersion = DMSService.cliVersion || ""; let cliVersion = DMSService.cliVersion || "";
// Debian/Ubuntu/OpenSUSE git format: 1.0.3+git2264.c5c5ce84 // Debian/Ubuntu/OpenSUSE git format: 1.0.3+git2264.c5c5ce84
@@ -218,7 +218,7 @@ Item {
let baseVersion = extractBaseVersion(cliVersion); let baseVersion = extractBaseVersion(cliVersion);
if (!baseVersion) if (!baseVersion)
baseVersion = extractBaseVersion(SystemUpdateService.semverVersion); baseVersion = extractBaseVersion(ShellVersionService.semverVersion);
if (baseVersion) { if (baseVersion) {
return `dms (git) v${baseVersion}-${match[1]}`; return `dms (git) v${baseVersion}-${match[1]}`;
} }
@@ -253,8 +253,8 @@ Item {
} }
StyledText { StyledText {
visible: SystemUpdateService.shellCodename.length > 0 visible: ShellVersionService.shellCodename.length > 0
text: `"${SystemUpdateService.shellCodename}"` text: `"${ShellVersionService.shellCodename}"`
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.italic: true font.italic: true
color: Theme.surfaceVariantText color: Theme.surfaceVariantText

View File

@@ -9,6 +9,7 @@ import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("DisplayConfigState")
readonly property bool hasOutputBackend: WlrOutputService.wlrOutputAvailable readonly property bool hasOutputBackend: WlrOutputService.wlrOutputAvailable
readonly property var wlrOutputs: WlrOutputService.outputs readonly property var wlrOutputs: WlrOutputService.outputs
@@ -106,7 +107,7 @@ Singleton {
function findMatchingProfile() { function findMatchingProfile() {
const profiles = validatedProfiles; const profiles = validatedProfiles;
console.log("[Profile Match] Current outputs:", JSON.stringify(currentOutputSet)); log.debug("[Profile Match] Current outputs:", JSON.stringify(currentOutputSet));
let bestMatch = ""; let bestMatch = "";
let bestScore = -1; let bestScore = -1;
@@ -116,25 +117,25 @@ Singleton {
const profile = profiles[profileId]; const profile = profiles[profileId];
const profileSet = new Set(profile.outputSet); const profileSet = new Set(profile.outputSet);
console.log("[Profile Match] Checking", profile.name, "outputSet:", JSON.stringify(profile.outputSet)); log.debug("[Profile Match] Checking", profile.name, "outputSet:", JSON.stringify(profile.outputSet));
let allCurrentPresent = true; let allCurrentPresent = true;
for (const output of currentOutputSet) { for (const output of currentOutputSet) {
if (!profileSet.has(output)) { if (!profileSet.has(output)) {
console.log("[Profile Match] - Missing output:", output); log.debug("[Profile Match] - Missing output:", output);
allCurrentPresent = false; allCurrentPresent = false;
break; break;
} }
} }
if (!allCurrentPresent) { if (!allCurrentPresent) {
console.log("[Profile Match] - SKIP: not all current outputs present"); log.debug("[Profile Match] - SKIP: not all current outputs present");
continue; continue;
} }
const disconnectedCount = profile.outputSet.length - currentOutputSet.length; const disconnectedCount = profile.outputSet.length - currentOutputSet.length;
const score = currentOutputSet.length * 100 - disconnectedCount; const score = currentOutputSet.length * 100 - disconnectedCount;
const updatedAt = profile.updatedAt || profile.createdAt || 0; const updatedAt = profile.updatedAt || profile.createdAt || 0;
console.log("[Profile Match] - MATCH score:", score, "(disconnected:", disconnectedCount, "updatedAt:", updatedAt + ")"); log.debug("[Profile Match] - MATCH score:", score, "(disconnected:", disconnectedCount, "updatedAt:", updatedAt + ")");
if (score > bestScore || (score === bestScore && updatedAt > bestUpdatedAt)) { if (score > bestScore || (score === bestScore && updatedAt > bestUpdatedAt)) {
bestScore = score; bestScore = score;
@@ -142,7 +143,7 @@ Singleton {
bestUpdatedAt = updatedAt; bestUpdatedAt = updatedAt;
} }
} }
console.log("[Profile Match] Best match:", bestMatch, "score:", bestScore); log.debug("[Profile Match] Best match:", bestMatch, "score:", bestScore);
return bestMatch; return bestMatch;
} }

View File

@@ -325,6 +325,8 @@ Item {
placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')") placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')")
onTextEdited: SettingsData.set("launchPrefix", text) onTextEdited: SettingsData.set("launchPrefix", text)
} }
TerminalPickerRow {}
} }
SettingsCard { SettingsCard {

View File

@@ -1697,8 +1697,11 @@ Item {
required property int index required property int index
readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid) readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid)
readonly property bool isTransient: !!modelData.transient
readonly property bool canExpand: modelData.canExpand !== false
readonly property bool canDelete: modelData.canDelete !== false
readonly property bool isExpanded: networkTab.expandedVpnUuid === modelData.uuid readonly property bool isExpanded: networkTab.expandedVpnUuid === modelData.uuid
readonly property var configData: isExpanded ? VPNService.editConfig : null readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null
width: parent.width width: parent.width
height: isExpanded ? 56 + vpnExpandedContent.height : 56 height: isExpanded ? 56 + vpnExpandedContent.height : 56
@@ -1745,7 +1748,7 @@ Item {
Column { Column {
spacing: 2 spacing: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4 width: parent.width - 20 - ((canExpand ? 28 : 0) + (canDelete ? 28 : 0)) - Theme.spacingS * 4
StyledText { StyledText {
text: modelData.name text: modelData.name
@@ -1775,6 +1778,7 @@ Item {
radius: 14 radius: 14
color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent" color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: canExpand
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -1805,6 +1809,7 @@ Item {
radius: 14 radius: 14
color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent" color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: canDelete
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -1835,7 +1840,7 @@ Item {
id: vpnExpandedContent id: vpnExpandedContent
width: parent.width width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: isExpanded visible: !isTransient && isExpanded
Rectangle { Rectangle {
width: parent.width width: parent.width

View File

@@ -38,7 +38,7 @@ StyledRect {
property bool meetsRequirements: requiresDms ? PluginService.checkPluginCompatibility(requiresDms) : true property bool meetsRequirements: requiresDms ? PluginService.checkPluginCompatibility(requiresDms) : true
Connections { Connections {
target: SystemUpdateService target: ShellVersionService
function onSemverVersionChanged() { function onSemverVersionChanged() {
root.meetsRequirementsChanged(); root.meetsRequirementsChanged();
} }

View File

@@ -194,7 +194,7 @@ FocusScope {
} }
Connections { Connections {
target: SystemUpdateService target: ShellVersionService
function onSemverVersionChanged() { function onSemverVersionChanged() {
incompatWarning.refresh(); incompatWarning.refresh();
} }

View File

@@ -1,11 +1,53 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.Settings.Widgets import qs.Modules.Settings.Widgets
Item { Item {
id: root id: root
readonly property var intervalOptions: [
{
label: I18n.tr("Every 15 minutes"),
seconds: 900
},
{
label: I18n.tr("Every 30 minutes"),
seconds: 1800
},
{
label: I18n.tr("Every hour"),
seconds: 3600
},
{
label: I18n.tr("Every 4 hours"),
seconds: 14400
},
{
label: I18n.tr("Once a day"),
seconds: 86400
}
]
function intervalLabelFor(seconds) {
for (const opt of intervalOptions) {
if (opt.seconds === seconds) {
return opt.label;
}
}
return intervalOptions[1].label;
}
function intervalSecondsFor(label) {
for (const opt of intervalOptions) {
if (opt.label === label) {
return opt.seconds;
}
}
return 1800;
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -25,18 +67,60 @@ Item {
title: I18n.tr("System Updater") title: I18n.tr("System Updater")
settingKey: "systemUpdater" settingKey: "systemUpdater"
SettingsToggleRow { StyledText {
text: I18n.tr("Hide Updater Widget", "When updater widget is used, then hide it if no update found") width: parent.width - Theme.spacingM * 2
description: I18n.tr("When updater widget is used, then hide it if no update found") anchors.left: parent.left
checked: SettingsData.updaterHideWidget anchors.leftMargin: Theme.spacingM
onToggled: checked => { visible: SystemUpdateService.backends.length > 0
SettingsData.set("updaterHideWidget", checked); text: {
const names = (SystemUpdateService.backends || []).map(b => b.displayName).join(", ");
return I18n.tr("Detected backends: %1").arg(names);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
SettingsDropdownRow {
text: I18n.tr("Check interval")
description: I18n.tr("How often the server polls for new updates.")
options: root.intervalOptions.map(o => o.label)
currentValue: root.intervalLabelFor(SettingsData.updaterIntervalSeconds)
onValueChanged: label => {
const secs = root.intervalSecondsFor(label);
SettingsData.set("updaterIntervalSeconds", secs);
SystemUpdateService.setInterval(secs);
} }
} }
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Use Custom Command") text: I18n.tr("Include Flatpak updates")
description: I18n.tr("Use custom command for update your system") description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
checked: SettingsData.updaterIncludeFlatpak
onToggled: checked => SettingsData.set("updaterIncludeFlatpak", checked)
}
SettingsToggleRow {
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")
checked: SettingsData.updaterAllowAUR
onToggled: checked => SettingsData.set("updaterAllowAUR", checked)
}
TerminalPickerRow {}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Advanced")
settingKey: "systemUpdaterAdvanced"
SettingsToggleRow {
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 checked: SettingsData.updaterUseCustomCommand
onToggled: checked => { onToggled: checked => {
if (!checked) { if (!checked) {
@@ -49,11 +133,32 @@ Item {
} }
} }
Rectangle {
width: parent.width - Theme.spacingM * 2
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
visible: SettingsData.updaterUseCustomCommand
height: warnText.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
StyledText {
id: warnText
anchors.fill: parent
anchors.margins: Theme.spacingS
text: I18n.tr("Custom command and terminal params are split on whitespace; paths with spaces will break.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
wrapMode: Text.WordWrap
}
}
FocusScope { FocusScope {
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingM * 2
height: customCommandColumn.implicitHeight height: customCommandColumn.implicitHeight
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
visible: SettingsData.updaterUseCustomCommand
Column { Column {
id: customCommandColumn id: customCommandColumn
@@ -61,7 +166,7 @@ Item {
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
text: I18n.tr("System update custom command") text: I18n.tr("Custom update command")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
@@ -69,7 +174,7 @@ Item {
DankTextField { DankTextField {
id: updaterCustomCommand id: updaterCustomCommand
width: parent.width width: parent.width
placeholderText: "myPkgMngr --sysupdate" placeholderText: "topgrade --no-retry"
backgroundColor: Theme.surfaceContainerHighest backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: Theme.outlineMedium normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary focusedBorderColor: Theme.primary
@@ -98,6 +203,7 @@ Item {
height: terminalParamsColumn.implicitHeight height: terminalParamsColumn.implicitHeight
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
visible: SettingsData.updaterUseCustomCommand
Column { Column {
id: terminalParamsColumn id: terminalParamsColumn
@@ -105,7 +211,7 @@ Item {
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
text: I18n.tr("Terminal custom additional parameters") text: I18n.tr("Terminal additional parameters")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
@@ -113,7 +219,7 @@ Item {
DankTextField { DankTextField {
id: updaterTerminalCustomClass id: updaterTerminalCustomClass
width: parent.width width: parent.width
placeholderText: "-T udpClass" placeholderText: "-T updater"
backgroundColor: Theme.surfaceContainerHighest backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: Theme.outlineMedium normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary focusedBorderColor: Theme.primary

View File

@@ -2645,6 +2645,18 @@ Item {
onToggled: checked => SettingsData.set("matugenTemplateVesktop", checked) onToggled: checked => SettingsData.set("matugenTemplateVesktop", checked)
} }
SettingsToggleRow {
tab: "theme"
tags: ["matugen", "vencord", "discord", "template"]
settingKey: "matugenTemplateVencord"
text: "vencord"
description: getTemplateDescription("vencord", "")
descriptionColor: getTemplateDescriptionColor("vencord")
visible: SettingsData.runDmsMatugenTemplates
checked: SettingsData.matugenTemplateVencord
onToggled: checked => SettingsData.set("matugenTemplateVencord", checked)
}
SettingsToggleRow { SettingsToggleRow {
tab: "theme" tab: "theme"
tags: ["matugen", "equibop", "discord", "template"] tags: ["matugen", "equibop", "discord", "template"]

View File

@@ -0,0 +1,31 @@
import QtQuick
import qs.Common
SettingsDropdownRow {
id: root
readonly property string autoLabel: I18n.tr("Auto")
text: I18n.tr("Terminal")
settingKey: "terminalOverride"
options: {
const opts = [autoLabel];
const installed = SessionData.installedTerminals || [];
const list = installed.length > 0 ? installed : SessionData.terminalOptions;
for (const t of list) {
opts.push(t);
}
if (SessionData.terminalOverride && !opts.includes(SessionData.terminalOverride)) {
opts.push(SessionData.terminalOverride);
}
return opts;
}
currentValue: SessionData.terminalOverride.length > 0 ? SessionData.terminalOverride : autoLabel
onValueChanged: label => {
const next = label === autoLabel ? "" : label;
SessionData.set("terminalOverride", next);
}
}

View File

@@ -246,7 +246,8 @@ Item {
"text": I18n.tr("System Update"), "text": I18n.tr("System Update"),
"description": I18n.tr("Check for system updates"), "description": I18n.tr("Check for system updates"),
"icon": "update", "icon": "update",
"enabled": SystemUpdateService.distributionSupported "enabled": SystemUpdateService.sysupdateAvailable,
"warning": SystemUpdateService.sysupdateAvailable ? undefined : I18n.tr("Requires DMS server with sysupdate capability")
}, },
{ {
"id": "powerMenuButton", "id": "powerMenuButton",
@@ -430,7 +431,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "hideWhenIdle"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -579,6 +580,17 @@ Item {
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
} }
function handleHideWhenIdleChanged(sectionId, widgetIndex, enabled) {
var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
return;
}
var newWidget = cloneWidgetData(widgets[widgetIndex]);
newWidget.hideWhenIdle = enabled;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
}
function handleDiskUsageModeChanged(sectionId, widgetIndex, mode) { function handleDiskUsageModeChanged(sectionId, widgetIndex, mode) {
var widgets = getWidgetsForSection(sectionId).slice(); var widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length) { if (widgetIndex < 0 || widgetIndex >= widgets.length) {
@@ -714,6 +726,8 @@ Item {
item.barShowOverflowBadge = widget.barShowOverflowBadge; item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined) if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion; item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
if (widget.hideWhenIdle !== undefined)
item.hideWhenIdle = widget.hideWhenIdle;
} }
widgets.push(item); widgets.push(item);
}); });
@@ -1003,6 +1017,9 @@ Item {
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
} }
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
}
} }
} }
@@ -1070,6 +1087,9 @@ Item {
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
} }
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
}
} }
} }
@@ -1137,6 +1157,9 @@ Item {
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
} }
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
}
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import qs.Services
Column { Column {
id: root id: root
readonly property var log: Log.scoped("WidgetsTabSection")
property var items: [] property var items: []
property var allWidgets: [] property var allWidgets: []
@@ -33,6 +34,7 @@ Column {
signal showInGbChanged(string sectionId, int widgetIndex, bool enabled) signal showInGbChanged(string sectionId, int widgetIndex, bool enabled)
signal diskUsageModeChanged(string sectionId, int widgetIndex, int mode) signal diskUsageModeChanged(string sectionId, int widgetIndex, int mode)
signal overflowSettingChanged(string sectionId, int widgetIndex, string settingName, var value) signal overflowSettingChanged(string sectionId, int widgetIndex, string settingName, var value)
signal hideWhenIdleChanged(string sectionId, int widgetIndex, bool enabled)
function cloneWidgetData(widget) { function cloneWidgetData(widget) {
var result = { var result = {
@@ -335,6 +337,25 @@ Column {
} }
} }
DankActionButton {
id: hideWhenIdleButton
buttonSize: 28
visible: modelData.id === "systemUpdate"
iconName: "visibility_off"
iconSize: 16
iconColor: (modelData.hideWhenIdle === true) ? Theme.primary : Theme.outline
onClicked: {
root.hideWhenIdleChanged(root.sectionId, index, modelData.hideWhenIdle !== true);
}
onEntered: {
const tooltipText = modelData.hideWhenIdle === true ? "Hide when no updates: ON" : "Hide when no updates: OFF";
sharedTooltip.show(tooltipText, hideWhenIdleButton, 0, 0, "bottom");
}
onExited: {
sharedTooltip.hide();
}
}
DankActionButton { DankActionButton {
id: memMenuButton id: memMenuButton
visible: modelData.id === "memUsage" visible: modelData.id === "memUsage"
@@ -1706,11 +1727,11 @@ Column {
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: { onOpened: {
console.log("Privacy context menu opened"); log.debug("Privacy context menu opened");
} }
onClosed: { onClosed: {
console.log("Privacy Center context menu closed"); log.debug("Privacy Center context menu closed");
} }
background: Rectangle { background: Rectangle {

View File

@@ -1,329 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: systemUpdatePopout
layerNamespace: "dms:system-update"
property var parentWidget: null
property var triggerScreen: null
Ref {
service: SystemUpdateService
}
popupWidth: 400
popupHeight: 500
triggerWidth: 55
positioning: ""
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: close()
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
if (SystemUpdateService.updateCount === 0 && !SystemUpdateService.isChecking) {
SystemUpdateService.checkForUpdates();
}
}
}
content: Component {
Rectangle {
id: updaterPanel
color: "transparent"
Column {
width: parent.width - Theme.spacingL * 2
height: parent.height - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingL
Item {
width: parent.width
height: 40
StyledText {
text: I18n.tr("System Updates")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: {
if (SystemUpdateService.isChecking)
return I18n.tr("Checking...");
if (SystemUpdateService.hasError)
return I18n.tr("Error");
if (SystemUpdateService.updateCount === 0)
return I18n.tr("Up to date");
return SystemUpdateService.updateCount === 1
? I18n.tr("%1 update").arg(SystemUpdateService.updateCount)
: I18n.tr("%1 updates").arg(SystemUpdateService.updateCount);
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (SystemUpdateService.hasError)
return Theme.error;
return Theme.surfaceText;
}
}
DankActionButton {
id: checkForUpdatesButton
buttonSize: 28
iconName: "refresh"
iconSize: 18
z: 15
iconColor: Theme.surfaceText
enabled: !SystemUpdateService.isChecking
opacity: enabled ? 1.0 : 0.5
onClicked: {
SystemUpdateService.checkForUpdates();
}
RotationAnimation {
target: checkForUpdatesButton
property: "rotation"
from: 0
to: 360
duration: 1000
running: SystemUpdateService.isChecking
loops: Animation.Infinite
onRunningChanged: {
if (!running) {
checkForUpdatesButton.rotation = 0;
}
}
}
}
}
}
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingL;
usedHeight += 48 + Theme.spacingL;
return parent.height - usedHeight;
}
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 0
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.rightMargin: 0
StyledText {
id: statusText
width: parent.width
text: {
if (SystemUpdateService.hasError) {
return I18n.tr("Failed to check for updates:\n%1").arg(SystemUpdateService.errorMessage);
}
if (!SystemUpdateService.helperAvailable) {
return I18n.tr("No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.");
}
if (SystemUpdateService.isChecking) {
return I18n.tr("Checking for updates...");
}
if (SystemUpdateService.updateCount === 0) {
return I18n.tr("Your system is up to date!");
}
return SystemUpdateService.updateCount === 1
? I18n.tr("Found %1 package to update:").arg(SystemUpdateService.updateCount)
: I18n.tr("Found %1 packages to update:").arg(SystemUpdateService.updateCount);
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (SystemUpdateService.hasError)
return Theme.errorText;
return Theme.surfaceText;
}
wrapMode: Text.WordWrap
visible: SystemUpdateService.updateCount === 0 || SystemUpdateService.hasError || SystemUpdateService.isChecking
}
DankListView {
id: packagesList
width: parent.width
height: parent.height - (SystemUpdateService.updateCount === 0 || SystemUpdateService.hasError || SystemUpdateService.isChecking ? statusText.height + Theme.spacingM : 0)
visible: SystemUpdateService.updateCount > 0 && !SystemUpdateService.isChecking && !SystemUpdateService.hasError
clip: true
spacing: Theme.spacingXS
model: SystemUpdateService.availableUpdates
delegate: Rectangle {
width: ListView.view.width - Theme.spacingM
height: 48
radius: Theme.cornerRadius
color: packageMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent"
border.color: Theme.outlineLight
border.width: 0
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - Theme.spacingM
spacing: 2
StyledText {
width: parent.width
text: modelData.name || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: `${modelData.currentVersion} ${modelData.newVersion}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
MouseArea {
id: packageMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
Row {
width: parent.width
height: 48
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: updateMouseArea.containsMouse ? Theme.primaryHover : Theme.secondaryHover
opacity: SystemUpdateService.updateCount > 0 ? 1.0 : 0.5
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "system_update_alt"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Update All")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: updateMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: SystemUpdateService.updateCount > 0
onClicked: {
SystemUpdateService.runUpdates();
systemUpdatePopout.close();
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.errorPressed : Theme.secondaryHover
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "close"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
systemUpdatePopout.close();
}
}
}
}
}
}
}
}

View File

@@ -7,6 +7,7 @@ import qs.Widgets
import qs.Services import qs.Services
Variants { Variants {
readonly property var log: Log.scoped("WallpaperBackground")
model: { model: {
if (SessionData.isGreeterMode) { if (SessionData.isGreeterMode) {
return Quickshell.screens; return Quickshell.screens;
@@ -103,7 +104,7 @@ Variants {
function _recheckScreenScale() { function _recheckScreenScale() {
const newScale = CompositorService.getScreenScale(modelData); const newScale = CompositorService.getScreenScale(modelData);
if (newScale !== root.screenScale) { if (newScale !== root.screenScale) {
console.info("WallpaperBackground: screen scale corrected for", modelData.name + ":", root.screenScale, "->", newScale); log.info("screen scale corrected for", modelData.name + ":", root.screenScale, "->", newScale);
root.screenScale = newScale; root.screenScale = newScale;
} }
} }

View File

@@ -68,6 +68,20 @@ Scope {
hideSpotlight(); hideSpotlight();
} }
onIsClosingChanged: {
if (!isClosing) {
closeTimer.stop();
return;
}
closeTimer.restart();
}
Timer {
id: closeTimer
interval: Theme.expressiveDurations.fast
onTriggered: niriOverviewScope.resetState()
}
Loader { Loader {
id: niriOverlayLoader id: niriOverlayLoader
active: overlayActive || isClosing active: overlayActive || isClosing
@@ -128,7 +142,7 @@ Scope {
WindowBlur { WindowBlur {
targetWindow: overlayWindow targetWindow: overlayWindow
readonly property real s: Math.min(1, spotlightContainer.scale) readonly property real s: Math.min(1, spotlightContainer.scale)
readonly property bool active: spotlightContainer.visible && spotlightContainer.opacity > 0 readonly property bool active: overlayWindow.shouldShowSpotlight && spotlightContainer.opacity > 0
blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5 blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5
blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5 blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5
blurWidth: active ? spotlightContainer.width * s : 0 blurWidth: active ? spotlightContainer.width * s : 0
@@ -256,16 +270,10 @@ Scope {
layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0) layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0)
Behavior on scale { Behavior on scale {
id: scaleAnimation
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.fast duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
niriOverviewScope.resetState();
}
} }
} }

View File

@@ -8,6 +8,7 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("OverviewWidget")
required property var panelWindow required property var panelWindow
required property bool overviewOpen required property bool overviewOpen
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
@@ -276,7 +277,7 @@ Item {
} }
return result; return result;
} catch (e) { } catch (e) {
console.error("OverviewWidget filter error:", e); log.error("OverviewWidget filter error:", e);
return []; return [];
} }
} }

View File

@@ -4,9 +4,11 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("AppSearchService")
property var applications: [] property var applications: []
property var _cachedCategories: null property var _cachedCategories: null
@@ -811,7 +813,7 @@ Singleton {
}); });
isPersistent = false; isPersistent = false;
} catch (e) { } catch (e) {
console.warn("AppSearchService: Error creating temporary plugin instance", pluginId, ":", e); log.warn("Error creating temporary plugin instance", pluginId, ":", e);
return []; return [];
} }
} }
@@ -831,7 +833,7 @@ Singleton {
instance.destroy(); instance.destroy();
} }
} catch (e) { } catch (e) {
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e); log.warn("Error getting items from plugin", pluginId, ":", e);
if (!isPersistent) if (!isPersistent)
instance.destroy(); instance.destroy();
} }
@@ -857,7 +859,7 @@ Singleton {
}); });
isPersistent = false; isPersistent = false;
} catch (e) { } catch (e) {
console.warn("AppSearchService: Error creating temporary plugin instance for execution", pluginId, ":", e); log.warn("Error creating temporary plugin instance for execution", pluginId, ":", e);
return false; return false;
} }
} }
@@ -877,7 +879,7 @@ Singleton {
instance.destroy(); instance.destroy();
} }
} catch (e) { } catch (e) {
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e); log.warn("Error executing item from plugin", pluginId, ":", e);
if (!isPersistent) if (!isPersistent)
instance.destroy(); instance.destroy();
} }
@@ -949,7 +951,7 @@ Singleton {
try { try {
return instance.getCategories() || []; return instance.getCategories() || [];
} catch (e) { } catch (e) {
console.warn("AppSearchService: Error getting categories from plugin", pluginId, ":", e); log.warn("Error getting categories from plugin", pluginId, ":", e);
return []; return [];
} }
} }
@@ -968,7 +970,7 @@ Singleton {
try { try {
instance.setCategory(categoryId); instance.setCategory(categoryId);
} catch (e) { } catch (e) {
console.warn("AppSearchService: Error setting category on plugin", pluginId, ":", e); log.warn("Error setting category on plugin", pluginId, ":", e);
} }
} }

View File

@@ -11,6 +11,7 @@ import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("AudioService")
readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource readonly property PwNode source: Pipewire.defaultAudioSource
@@ -143,7 +144,7 @@ Singleton {
function setDeviceAlias(nodeName, customAlias) { function setDeviceAlias(nodeName, customAlias) {
if (!nodeName) { if (!nodeName) {
console.error("AudioService: Cannot set alias - nodeName is empty"); log.error("Cannot set alias - nodeName is empty");
return false; return false;
} }
@@ -189,8 +190,8 @@ EOFCONFIG
Proc.runCommand("writeWireplumberConfig", ["sh", "-c", shellCmd], (output, exitCode) => { Proc.runCommand("writeWireplumberConfig", ["sh", "-c", shellCmd], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.error("AudioService: Failed to write WirePlumber config. Exit code:", exitCode); log.error("Failed to write WirePlumber config. Exit code:", exitCode);
console.error("AudioService: Error output:", output); log.error("Error output:", output);
ToastService.showError(I18n.tr("Failed to save audio config"), output || ""); ToastService.showError(I18n.tr("Failed to save audio config"), output || "");
return; return;
} }
@@ -305,7 +306,7 @@ EOFCONFIG
ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated")); ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated"));
wireplumberReloadCompleted(true); wireplumberReloadCompleted(true);
} else { } else {
console.error("AudioService: Failed to restart WirePlumber:", output); log.error("Failed to restart WirePlumber:", output);
ToastService.showError(I18n.tr("Failed to restart audio system"), output); ToastService.showError(I18n.tr("Failed to restart audio system"), output);
wireplumberReloadCompleted(false); wireplumberReloadCompleted(false);
} }
@@ -317,7 +318,7 @@ EOFCONFIG
Proc.runCommand("readWireplumberConfig", ["cat", configPath], (output, exitCode) => { Proc.runCommand("readWireplumberConfig", ["cat", configPath], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.log("AudioService: No existing WirePlumber config found"); log.debug("No existing WirePlumber config found");
return; return;
} }
@@ -340,7 +341,7 @@ EOFCONFIG
if (Object.keys(aliases).length > 0) { if (Object.keys(aliases).length > 0) {
deviceAliases = aliases; deviceAliases = aliases;
console.log("AudioService: Loaded", Object.keys(aliases).length, "device aliases"); log.debug("Loaded", Object.keys(aliases).length, "device aliases");
} }
}, 0); }, 0);
} }
@@ -394,13 +395,13 @@ EOFCONFIG
Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => { Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => {
if (exitCode === 0 && output.trim()) { if (exitCode === 0 && output.trim()) {
currentSoundTheme = output.trim(); currentSoundTheme = output.trim();
console.log("AudioService: Current system sound theme:", currentSoundTheme); log.debug("Current system sound theme:", currentSoundTheme);
if (SettingsData.useSystemSoundTheme) { if (SettingsData.useSystemSoundTheme) {
discoverSoundFiles(currentSoundTheme); discoverSoundFiles(currentSoundTheme);
} }
} else { } else {
currentSoundTheme = ""; currentSoundTheme = "";
console.log("AudioService: No system sound theme found"); log.debug("No system sound theme found");
} }
}, 0); }, 0);
} }
@@ -510,22 +511,22 @@ EOFCONFIG
const themeLower = currentSoundTheme.toLowerCase(); const themeLower = currentSoundTheme.toLowerCase();
if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) { if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) {
const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav"); const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav");
console.log("AudioService: Using bundled sound (special condition) for", soundEvent, ":", bundledPath); log.debug("Using bundled sound (special condition) for", soundEvent, ":", bundledPath);
return bundledPath; return bundledPath;
} }
if (SettingsData.useSystemSoundTheme && soundFilePaths[soundEvent]) { if (SettingsData.useSystemSoundTheme && soundFilePaths[soundEvent]) {
console.log("AudioService: Using system sound for", soundEvent, ":", soundFilePaths[soundEvent]); log.debug("Using system sound for", soundEvent, ":", soundFilePaths[soundEvent]);
return soundFilePaths[soundEvent]; return soundFilePaths[soundEvent];
} }
const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav"); const bundledPath = Qt.resolvedUrl(soundMap[soundEvent] || "../assets/sounds/freedesktop/message.wav");
console.log("AudioService: Using bundled sound for", soundEvent, ":", bundledPath); log.debug("Using bundled sound for", soundEvent, ":", bundledPath);
return bundledPath; return bundledPath;
} }
function reloadSounds() { function reloadSounds() {
console.log("AudioService: Reloading sounds, useSystemSoundTheme:", SettingsData.useSystemSoundTheme, "currentSoundTheme:", currentSoundTheme); log.debug("Reloading sounds, useSystemSoundTheme:", SettingsData.useSystemSoundTheme, "currentSoundTheme:", currentSoundTheme);
if (SettingsData.useSystemSoundTheme && currentSoundTheme) { if (SettingsData.useSystemSoundTheme && currentSoundTheme) {
discoverSoundFiles(currentSoundTheme); discoverSoundFiles(currentSoundTheme);
} else { } else {
@@ -549,7 +550,7 @@ EOFCONFIG
MediaDevices { MediaDevices {
id: devices id: devices
Component.onCompleted: { Component.onCompleted: {
console.log("AudioService: MediaDevices initialized, default output:", defaultAudioOutput?.description) log.debug("MediaDevices initialized, default output:", defaultAudioOutput?.description)
} }
} }
`, root, "AudioService.MediaDevices"); `, root, "AudioService.MediaDevices");
@@ -560,7 +561,7 @@ EOFCONFIG
Connections { Connections {
target: root.mediaDevices target: root.mediaDevices
function onDefaultAudioOutputChanged() { function onDefaultAudioOutputChanged() {
console.log("AudioService: Default audio output changed, recreating sound players") log.debug("Default audio output changed, recreating sound players")
root.destroySoundPlayers() root.destroySoundPlayers()
root.createSoundPlayers() root.createSoundPlayers()
} }
@@ -568,7 +569,7 @@ EOFCONFIG
`, root, "AudioService.MediaDevicesConnections"); `, root, "AudioService.MediaDevicesConnections");
} }
} catch (e) { } catch (e) {
console.log("AudioService: MediaDevices not available, using default audio output"); log.debug("MediaDevices not available, using default audio output");
mediaDevices = null; mediaDevices = null;
} }
} }
@@ -682,7 +683,7 @@ EOFCONFIG
} }
`, root, "AudioService.LoginSound"); `, root, "AudioService.LoginSound");
} catch (e) { } catch (e) {
console.warn("AudioService: Error creating sound players:", e); log.warn("Error creating sound players:", e);
} }
} }

View File

@@ -1,5 +1,4 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
@@ -19,206 +18,217 @@ Singleton {
readonly property bool enhancedPairingAvailable: DMSService.dmsAvailable && DMSService.apiVersion >= 9 && DMSService.capabilities.includes("bluetooth") readonly property bool enhancedPairingAvailable: DMSService.dmsAvailable && DMSService.apiVersion >= 9 && DMSService.capabilities.includes("bluetooth")
readonly property bool connected: { readonly property bool connected: {
if (!adapter || !adapter.devices) { if (!adapter || !adapter.devices) {
return false return false;
} }
let isConnected = false let isConnected = false;
adapter.devices.values.forEach(dev => { if (dev.connected) isConnected = true }) adapter.devices.values.forEach(dev => {
return isConnected if (dev.connected)
isConnected = true;
});
return isConnected;
} }
readonly property var pairedDevices: { readonly property var pairedDevices: {
if (!adapter || !adapter.devices) { if (!adapter || !adapter.devices) {
return [] return [];
} }
return adapter.devices.values.filter(dev => { return adapter.devices.values.filter(dev => {
return dev && (dev.paired || dev.trusted) return dev && (dev.paired || dev.trusted);
}) });
} }
readonly property var allDevicesWithBattery: { readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices) { if (!adapter || !adapter.devices) {
return [] return [];
} }
return adapter.devices.values.filter(dev => { return adapter.devices.values.filter(dev => {
return dev && dev.batteryAvailable && dev.battery > 0 return dev && dev.batteryAvailable && dev.battery > 0;
}) });
} }
function sortDevices(devices) { function sortDevices(devices) {
return devices.sort((a, b) => { return devices.sort((a, b) => {
const aName = a.name || a.deviceName || "" const aName = a.name || a.deviceName || "";
const bName = b.name || b.deviceName || "" const bName = b.name || b.deviceName || "";
const aAddr = a.address || "" const aAddr = a.address || "";
const bAddr = b.address || "" const bAddr = b.address || "";
const aHasRealName = aName.includes(" ") && aName.length > 3 const aHasRealName = aName.includes(" ") && aName.length > 3;
const bHasRealName = bName.includes(" ") && bName.length > 3 const bHasRealName = bName.includes(" ") && bName.length > 3;
if (aHasRealName && !bHasRealName) return -1 if (aHasRealName && !bHasRealName)
if (!aHasRealName && bHasRealName) return 1 return -1;
if (!aHasRealName && bHasRealName)
return 1;
if (aHasRealName && bHasRealName) { if (aHasRealName && bHasRealName) {
return aName.localeCompare(bName) return aName.localeCompare(bName);
} }
return aAddr.localeCompare(bAddr) return aAddr.localeCompare(bAddr);
}) });
} }
function getDeviceIcon(device) { function getDeviceIcon(device) {
if (!device) { if (!device) {
return "bluetooth" return "bluetooth";
} }
const name = (device.name || device.deviceName || "").toLowerCase() const name = (device.name || device.deviceName || "").toLowerCase();
const icon = (device.icon || "").toLowerCase() const icon = (device.icon || "").toLowerCase();
const audioKeywords = ["headset", "audio", "headphone", "airpod", "arctis"] const audioKeywords = ["headset", "audio", "headphone", "airpod", "arctis"];
if (audioKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) { if (audioKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) {
return "headset" return "headset";
} }
if (icon.includes("mouse") || name.includes("mouse")) { if (icon.includes("mouse") || name.includes("mouse")) {
return "mouse" return "mouse";
} }
if (icon.includes("keyboard") || name.includes("keyboard")) { if (icon.includes("keyboard") || name.includes("keyboard")) {
return "keyboard" return "keyboard";
} }
const phoneKeywords = ["phone", "iphone", "android", "samsung"] const phoneKeywords = ["phone", "iphone", "android", "samsung"];
if (phoneKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) { if (phoneKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) {
return "smartphone" return "smartphone";
} }
if (icon.includes("watch") || name.includes("watch")) { if (icon.includes("watch") || name.includes("watch")) {
return "watch" return "watch";
} }
if (icon.includes("speaker") || name.includes("speaker")) { if (icon.includes("speaker") || name.includes("speaker")) {
return "speaker" return "speaker";
} }
if (icon.includes("display") || name.includes("tv")) { if (icon.includes("display") || name.includes("tv")) {
return "tv" return "tv";
} }
return "bluetooth" return "bluetooth";
} }
function canConnect(device) { function canConnect(device) {
if (!device) { if (!device) {
return false return false;
} }
return !device.paired && !device.pairing && !device.blocked return !device.paired && !device.pairing && !device.blocked;
} }
function getSignalStrength(device) { function getSignalStrength(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
return "Unknown" return "Unknown";
} }
const signal = device.signalStrength const signal = device.signalStrength;
if (signal >= 80) { if (signal >= 80) {
return "Excellent" return "Excellent";
} }
if (signal >= 60) { if (signal >= 60) {
return "Good" return "Good";
} }
if (signal >= 40) { if (signal >= 40) {
return "Fair" return "Fair";
} }
if (signal >= 20) { if (signal >= 20) {
return "Poor" return "Poor";
} }
return "Very Poor" return "Very Poor";
} }
function getSignalIcon(device) { function getSignalIcon(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
return "signal_cellular_null" return "signal_cellular_null";
} }
const signal = device.signalStrength const signal = device.signalStrength;
if (signal >= 80) { if (signal >= 80) {
return "signal_cellular_4_bar" return "signal_cellular_4_bar";
} }
if (signal >= 60) { if (signal >= 60) {
return "signal_cellular_3_bar" return "signal_cellular_3_bar";
} }
if (signal >= 40) { if (signal >= 40) {
return "signal_cellular_2_bar" return "signal_cellular_2_bar";
} }
if (signal >= 20) { if (signal >= 20) {
return "signal_cellular_1_bar" return "signal_cellular_1_bar";
} }
return "signal_cellular_0_bar" return "signal_cellular_0_bar";
} }
function isDeviceBusy(device) { function isDeviceBusy(device) {
if (!device) { if (!device) {
return false return false;
} }
return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting;
} }
function connectDeviceWithTrust(device) { function connectDeviceWithTrust(device) {
if (!device) { if (!device) {
return return;
} }
device.trusted = true device.trusted = true;
device.connect() device.connect();
} }
function pairDevice(device, callback) { function pairDevice(device, callback) {
if (!device) { if (!device) {
if (callback) callback({error: "Invalid device"}) if (callback)
return callback({
error: "Invalid device"
});
return;
} }
// The DMS backend actually implements a bluez agent, so we can pair anything // The DMS backend actually implements a bluez agent, so we can pair anything
if (enhancedPairingAvailable) { if (enhancedPairingAvailable) {
const devicePath = getDevicePath(device) const devicePath = getDevicePath(device);
DMSService.bluetoothPair(devicePath, callback) DMSService.bluetoothPair(devicePath, callback);
return return;
} }
// Quickshell does not implement a bluez agent, so we can try to pair but only with devices that don't require a passcode // Quickshell does not implement a bluez agent, so we can try to pair but only with devices that don't require a passcode
device.trusted = true device.trusted = true;
device.connect() device.connect();
if (callback) callback({success: true}) if (callback)
callback({
success: true
});
} }
function getCardName(device) { function getCardName(device) {
if (!device) { if (!device) {
return "" return "";
} }
return `bluez_card.${device.address.replace(/:/g, "_")}` return `bluez_card.${device.address.replace(/:/g, "_")}`;
} }
function getDevicePath(device) { function getDevicePath(device) {
if (!device || !device.address) { if (!device || !device.address) {
return "" return "";
} }
const adapterPath = adapter ? "/org/bluez/hci0" : "/org/bluez/hci0" const adapterPath = adapter ? "/org/bluez/hci0" : "/org/bluez/hci0";
return `${adapterPath}/dev_${device.address.replace(/:/g, "_")}` return `${adapterPath}/dev_${device.address.replace(/:/g, "_")}`;
} }
function isAudioDevice(device) { function isAudioDevice(device) {
if (!device) { if (!device) {
return false return false;
} }
const icon = getDeviceIcon(device) const icon = getDeviceIcon(device);
return icon === "headset" || icon === "speaker" return icon === "headset" || icon === "speaker";
} }
function getCodecInfo(codecName) { function getCodecInfo(codecName) {
const codec = codecName.replace(/-/g, "_").toUpperCase() const codec = codecName.replace(/-/g, "_").toUpperCase();
const codecMap = { const codecMap = {
"LDAC": { "LDAC": {
@@ -261,77 +271,77 @@ Singleton {
"description": "Basic speech codec • Legacy compatibility", "description": "Basic speech codec • Legacy compatibility",
"qualityColor": "#9E9E9E" "qualityColor": "#9E9E9E"
} }
} };
return codecMap[codec] || { return codecMap[codec] || {
"name": codecName, "name": codecName,
"description": "Unknown codec", "description": "Unknown codec",
"qualityColor": "#9E9E9E" "qualityColor": "#9E9E9E"
} };
} }
property var deviceCodecs: ({}) property var deviceCodecs: ({})
function updateDeviceCodec(deviceAddress, codec) { function updateDeviceCodec(deviceAddress, codec) {
deviceCodecs[deviceAddress] = codec deviceCodecs[deviceAddress] = codec;
deviceCodecsChanged() deviceCodecsChanged();
} }
function refreshDeviceCodec(device) { function refreshDeviceCodec(device) {
if (!device || !device.connected || !isAudioDevice(device)) { if (!device || !device.connected || !isAudioDevice(device)) {
return return;
} }
const cardName = getCardName(device) const cardName = getCardName(device);
codecQueryProcess.cardName = cardName codecQueryProcess.cardName = cardName;
codecQueryProcess.deviceAddress = device.address codecQueryProcess.deviceAddress = device.address;
codecQueryProcess.availableCodecs = [] codecQueryProcess.availableCodecs = [];
codecQueryProcess.parsingTargetCard = false codecQueryProcess.parsingTargetCard = false;
codecQueryProcess.detectedCodec = "" codecQueryProcess.detectedCodec = "";
codecQueryProcess.running = true codecQueryProcess.running = true;
} }
function getCurrentCodec(device, callback) { function getCurrentCodec(device, callback) {
if (!device || !device.connected || !isAudioDevice(device)) { if (!device || !device.connected || !isAudioDevice(device)) {
callback("") callback("");
return return;
} }
const cardName = getCardName(device) const cardName = getCardName(device);
codecQueryProcess.cardName = cardName codecQueryProcess.cardName = cardName;
codecQueryProcess.callback = callback codecQueryProcess.callback = callback;
codecQueryProcess.availableCodecs = [] codecQueryProcess.availableCodecs = [];
codecQueryProcess.parsingTargetCard = false codecQueryProcess.parsingTargetCard = false;
codecQueryProcess.detectedCodec = "" codecQueryProcess.detectedCodec = "";
codecQueryProcess.running = true codecQueryProcess.running = true;
} }
function getAvailableCodecs(device, callback) { function getAvailableCodecs(device, callback) {
if (!device || !device.connected || !isAudioDevice(device)) { if (!device || !device.connected || !isAudioDevice(device)) {
callback([], "") callback([], "");
return return;
} }
const cardName = getCardName(device) const cardName = getCardName(device);
codecFullQueryProcess.cardName = cardName codecFullQueryProcess.cardName = cardName;
codecFullQueryProcess.callback = callback codecFullQueryProcess.callback = callback;
codecFullQueryProcess.availableCodecs = [] codecFullQueryProcess.availableCodecs = [];
codecFullQueryProcess.parsingTargetCard = false codecFullQueryProcess.parsingTargetCard = false;
codecFullQueryProcess.detectedCodec = "" codecFullQueryProcess.detectedCodec = "";
codecFullQueryProcess.running = true codecFullQueryProcess.running = true;
} }
function switchCodec(device, profileName, callback) { function switchCodec(device, profileName, callback) {
if (!device || !isAudioDevice(device)) { if (!device || !isAudioDevice(device)) {
callback(false, "Invalid device") callback(false, "Invalid device");
return return;
} }
const cardName = getCardName(device) const cardName = getCardName(device);
codecSwitchProcess.cardName = cardName codecSwitchProcess.cardName = cardName;
codecSwitchProcess.profile = profileName codecSwitchProcess.profile = profileName;
codecSwitchProcess.callback = callback codecSwitchProcess.callback = callback;
codecSwitchProcess.running = true codecSwitchProcess.running = true;
} }
Process { Process {
@@ -349,67 +359,67 @@ Singleton {
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode === 0 && detectedCodec) { if (exitCode === 0 && detectedCodec) {
if (deviceAddress) { if (deviceAddress) {
root.updateDeviceCodec(deviceAddress, detectedCodec) root.updateDeviceCodec(deviceAddress, detectedCodec);
} }
if (callback) { if (callback) {
callback(detectedCodec) callback(detectedCodec);
} }
} else if (callback) { } else if (callback) {
callback("") callback("");
} }
parsingTargetCard = false parsingTargetCard = false;
detectedCodec = "" detectedCodec = "";
availableCodecs = [] availableCodecs = [];
deviceAddress = "" deviceAddress = "";
callback = null callback = null;
} }
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: data => { onRead: data => {
let line = data.trim() let line = data.trim();
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) { if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
codecQueryProcess.parsingTargetCard = true codecQueryProcess.parsingTargetCard = true;
return return;
} }
if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) { if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
codecQueryProcess.parsingTargetCard = false codecQueryProcess.parsingTargetCard = false;
return return;
} }
if (codecQueryProcess.parsingTargetCard) { if (codecQueryProcess.parsingTargetCard) {
if (line.startsWith("Active Profile:")) { if (line.startsWith("Active Profile:")) {
let profile = line.split(": ")[1] || "" let profile = line.split(": ")[1] || "";
let activeCodec = codecQueryProcess.availableCodecs.find(c => { let activeCodec = codecQueryProcess.availableCodecs.find(c => {
return c.profile === profile return c.profile === profile;
}) });
if (activeCodec) { if (activeCodec) {
codecQueryProcess.detectedCodec = activeCodec.name codecQueryProcess.detectedCodec = activeCodec.name;
} }
return return;
} }
if (line.includes("codec") && line.includes("available: yes")) { if (line.includes("codec") && line.includes("available: yes")) {
let parts = line.split(": ") let parts = line.split(": ");
if (parts.length >= 2) { if (parts.length >= 2) {
let profile = parts[0].trim() let profile = parts[0].trim();
let description = parts[1] let description = parts[1];
let codecMatch = description.match(/codec ([^\)\s]+)/i) let codecMatch = description.match(/codec ([^\)\s]+)/i);
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN" let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
let codecInfo = root.getCodecInfo(codecName) let codecInfo = root.getCodecInfo(codecName);
if (codecInfo && !codecQueryProcess.availableCodecs.some(c => { if (codecInfo && !codecQueryProcess.availableCodecs.some(c => {
return c.profile === profile return c.profile === profile;
})) { })) {
let newCodecs = codecQueryProcess.availableCodecs.slice() let newCodecs = codecQueryProcess.availableCodecs.slice();
newCodecs.push({ newCodecs.push({
"name": codecInfo.name, "name": codecInfo.name,
"profile": profile, "profile": profile,
"description": codecInfo.description, "description": codecInfo.description,
"qualityColor": codecInfo.qualityColor "qualityColor": codecInfo.qualityColor
}) });
codecQueryProcess.availableCodecs = newCodecs codecQueryProcess.availableCodecs = newCodecs;
} }
} }
} }
@@ -431,59 +441,59 @@ Singleton {
onExited: function (exitCode, exitStatus) { onExited: function (exitCode, exitStatus) {
if (callback) { if (callback) {
callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "") callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "");
} }
parsingTargetCard = false parsingTargetCard = false;
detectedCodec = "" detectedCodec = "";
availableCodecs = [] availableCodecs = [];
callback = null callback = null;
} }
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: data => { onRead: data => {
let line = data.trim() let line = data.trim();
if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) { if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) {
codecFullQueryProcess.parsingTargetCard = true codecFullQueryProcess.parsingTargetCard = true;
return return;
} }
if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) { if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) {
codecFullQueryProcess.parsingTargetCard = false codecFullQueryProcess.parsingTargetCard = false;
return return;
} }
if (codecFullQueryProcess.parsingTargetCard) { if (codecFullQueryProcess.parsingTargetCard) {
if (line.startsWith("Active Profile:")) { if (line.startsWith("Active Profile:")) {
let profile = line.split(": ")[1] || "" let profile = line.split(": ")[1] || "";
let activeCodec = codecFullQueryProcess.availableCodecs.find(c => { let activeCodec = codecFullQueryProcess.availableCodecs.find(c => {
return c.profile === profile return c.profile === profile;
}) });
if (activeCodec) { if (activeCodec) {
codecFullQueryProcess.detectedCodec = activeCodec.name codecFullQueryProcess.detectedCodec = activeCodec.name;
} }
return return;
} }
if (line.includes("codec") && line.includes("available: yes")) { if (line.includes("codec") && line.includes("available: yes")) {
let parts = line.split(": ") let parts = line.split(": ");
if (parts.length >= 2) { if (parts.length >= 2) {
let profile = parts[0].trim() let profile = parts[0].trim();
let description = parts[1] let description = parts[1];
let codecMatch = description.match(/codec ([^\)\s]+)/i) let codecMatch = description.match(/codec ([^\)\s]+)/i);
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN" let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
let codecInfo = root.getCodecInfo(codecName) let codecInfo = root.getCodecInfo(codecName);
if (codecInfo && !codecFullQueryProcess.availableCodecs.some(c => { if (codecInfo && !codecFullQueryProcess.availableCodecs.some(c => {
return c.profile === profile return c.profile === profile;
})) { })) {
let newCodecs = codecFullQueryProcess.availableCodecs.slice() let newCodecs = codecFullQueryProcess.availableCodecs.slice();
newCodecs.push({ newCodecs.push({
"name": codecInfo.name, "name": codecInfo.name,
"profile": profile, "profile": profile,
"description": codecInfo.description, "description": codecInfo.description,
"qualityColor": codecInfo.qualityColor "qualityColor": codecInfo.qualityColor
}) });
codecFullQueryProcess.availableCodecs = newCodecs codecFullQueryProcess.availableCodecs = newCodecs;
} }
} }
} }
@@ -503,21 +513,21 @@ Singleton {
onExited: function (exitCode, exitStatus) { onExited: function (exitCode, exitStatus) {
if (callback) { if (callback) {
callback(exitCode === 0, exitCode === 0 ? "Codec switched successfully" : "Failed to switch codec") callback(exitCode === 0, exitCode === 0 ? "Codec switched successfully" : "Failed to switch codec");
} }
// If successful, refresh the codec for this device // If successful, refresh the codec for this device
if (exitCode === 0) { if (exitCode === 0) {
if (root.adapter && root.adapter.devices) { if (root.adapter && root.adapter.devices) {
root.adapter.devices.values.forEach(device => { root.adapter.devices.values.forEach(device => {
if (device && root.getCardName(device) === cardName) { if (device && root.getCardName(device) === cardName) {
Qt.callLater(() => root.refreshDeviceCodec(device)) Qt.callLater(() => root.refreshDeviceCodec(device));
} }
}) });
} }
} }
callback = null callback = null;
} }
} }
} }

View File

@@ -6,9 +6,11 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland // ! Import is needed despite what qmlls says import Quickshell.Wayland // ! Import is needed despite what qmlls says
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("BlurService")
property bool quickshellSupported: false property bool quickshellSupported: false
property bool compositorSupported: false property bool compositorSupported: false
@@ -52,7 +54,7 @@ Singleton {
targetWindow.BackgroundEffect.blurRegion = region; targetWindow.BackgroundEffect.blurRegion = region;
return region; return region;
} catch (e) { } catch (e) {
console.warn("BlurService: Failed to create blur region:", e); log.warn("Failed to create blur region:", e);
return null; return null;
} }
} }
@@ -84,15 +86,15 @@ Singleton {
onStreamFinished: { onStreamFinished: {
root.compositorSupported = text.trim() === "supported"; root.compositorSupported = text.trim() === "supported";
if (root.compositorSupported) if (root.compositorSupported)
console.info("BlurService: Compositor supports ext-background-effect-v1"); log.info("Compositor supports ext-background-effect-v1");
else else
console.info("BlurService: Compositor does not support ext-background-effect-v1"); log.info("Compositor does not support ext-background-effect-v1");
} }
} }
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) if (exitCode !== 0)
console.warn("BlurService: blur probe failed with code:", exitCode); log.warn("blur probe failed with code:", exitCode);
} }
} }
@@ -104,10 +106,10 @@ Singleton {
`, root, "BlurAvailabilityTest"); `, root, "BlurAvailabilityTest");
test.destroy(); test.destroy();
quickshellSupported = true; quickshellSupported = true;
console.info("BlurService: Quickshell blur support available"); log.info("Quickshell blur support available");
blurProbe.running = true; blurProbe.running = true;
} catch (e) { } catch (e) {
console.info("BlurService: BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell."); log.info("BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell.");
} }
} }
} }

View File

@@ -19,68 +19,69 @@ Singleton {
function checkKhalAvailability() { function checkKhalAvailability() {
if (!khalCheckProcess.running) if (!khalCheckProcess.running)
khalCheckProcess.running = true khalCheckProcess.running = true;
} }
function detectKhalDateFormat() { function detectKhalDateFormat() {
if (!khalFormatProcess.running) if (!khalFormatProcess.running)
khalFormatProcess.running = true khalFormatProcess.running = true;
} }
function parseKhalDateFormat(formatExample) { function parseKhalDateFormat(formatExample) {
let qtFormat = formatExample.replace("12", "MM").replace("21", "dd").replace("2013", "yyyy") let qtFormat = formatExample.replace("12", "MM").replace("21", "dd").replace("2013", "yyyy");
return { format: qtFormat, parser: null } return {
format: qtFormat,
parser: null
};
} }
function loadCurrentMonth() { function loadCurrentMonth() {
if (!root.khalAvailable) if (!root.khalAvailable)
return return;
let today = new Date();
let today = new Date() let firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1) let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
// Add padding // Add padding
let startDate = new Date(firstDay) let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7) startDate.setDate(startDate.getDate() - firstDay.getDay() - 7);
let endDate = new Date(lastDay) let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7) endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7);
loadEvents(startDate, endDate) loadEvents(startDate, endDate);
} }
function loadEvents(startDate, endDate) { function loadEvents(startDate, endDate) {
if (!root.khalAvailable) { if (!root.khalAvailable) {
return return;
} }
if (eventsProcess.running) { if (eventsProcess.running) {
return return;
} }
// Store last requested date range for refresh timer // Store last requested date range for refresh timer
root.lastStartDate = startDate root.lastStartDate = startDate;
root.lastEndDate = endDate root.lastEndDate = endDate;
root.isLoading = true root.isLoading = true;
// Format dates for khal using detected format // Format dates for khal using detected format
let startDateStr = Qt.formatDate(startDate, root.khalDateFormat) let startDateStr = Qt.formatDate(startDate, root.khalDateFormat);
let endDateStr = Qt.formatDate(endDate, root.khalDateFormat) let endDateStr = Qt.formatDate(endDate, root.khalDateFormat);
eventsProcess.requestStartDate = startDate eventsProcess.requestStartDate = startDate;
eventsProcess.requestEndDate = endDate eventsProcess.requestEndDate = endDate;
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr] eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr];
eventsProcess.running = true eventsProcess.running = true;
} }
function getEventsForDate(date) { function getEventsForDate(date) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd") let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
return root.eventsByDate[dateKey] || [] return root.eventsByDate[dateKey] || [];
} }
function hasEventsForDate(date) { function hasEventsForDate(date) {
let events = getEventsForDate(date) let events = getEventsForDate(date);
return events.length > 0 return events.length > 0;
} }
// Initialize on component completion // Initialize on component completion
Component.onCompleted: { Component.onCompleted: {
detectKhalDateFormat() detectKhalDateFormat();
} }
// Process for detecting khal date format // Process for detecting khal date format
@@ -91,22 +92,22 @@ Singleton {
running: false running: false
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
checkKhalAvailability() checkKhalAvailability();
} }
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
let lines = text.split('\n') let lines = text.split('\n');
for (let line of lines) { for (let line of lines) {
if (line.startsWith('dateformat:')) { if (line.startsWith('dateformat:')) {
let formatExample = line.substring(line.indexOf(':') + 1).trim() let formatExample = line.substring(line.indexOf(':') + 1).trim();
let formatInfo = parseKhalDateFormat(formatExample) let formatInfo = parseKhalDateFormat(formatExample);
root.khalDateFormat = formatInfo.format root.khalDateFormat = formatInfo.format;
break break;
} }
} }
checkKhalAvailability() checkKhalAvailability();
} }
} }
} }
@@ -118,9 +119,9 @@ Singleton {
command: ["khal", "list", "today"] command: ["khal", "list", "today"]
running: false running: false
onExited: exitCode => { onExited: exitCode => {
root.khalAvailable = (exitCode === 0) root.khalAvailable = (exitCode === 0);
if (exitCode === 0) { if (exitCode === 0) {
loadCurrentMonth() loadCurrentMonth();
} }
} }
} }
@@ -135,100 +136,96 @@ Singleton {
running: false running: false
onExited: exitCode => { onExited: exitCode => {
root.isLoading = false root.isLoading = false;
if (exitCode !== 0) { if (exitCode !== 0) {
root.lastError = "Failed to load events (exit code: " + exitCode + ")" root.lastError = "Failed to load events (exit code: " + exitCode + ")";
return return;
} }
try { try {
let newEventsByDate = {} let newEventsByDate = {};
let lines = eventsProcess.rawOutput.split('\n') let lines = eventsProcess.rawOutput.split('\n');
for (let line of lines) { for (let line of lines) {
line = line.trim() line = line.trim();
if (!line || line === "[]") if (!line || line === "[]")
continue continue;
// Parse JSON line // Parse JSON line
let dayEvents = JSON.parse(line) let dayEvents = JSON.parse(line);
// Process each event in this day's array // Process each event in this day's array
for (let event of dayEvents) { for (let event of dayEvents) {
if (!event.title) if (!event.title)
continue continue;
// Parse start and end dates using detected format // Parse start and end dates using detected format
let startDate, endDate let startDate, endDate;
if (event['start-date']) { if (event['start-date']) {
startDate = Date.fromLocaleString(I18n.locale(), event['start-date'], root.khalDateFormat) startDate = Date.fromLocaleString(I18n.locale(), event['start-date'], root.khalDateFormat);
} else { } else {
startDate = new Date() startDate = new Date();
} }
if (event['end-date']) { if (event['end-date']) {
endDate = Date.fromLocaleString(I18n.locale(), event['end-date'], root.khalDateFormat) endDate = Date.fromLocaleString(I18n.locale(), event['end-date'], root.khalDateFormat);
} else { } else {
endDate = new Date(startDate) endDate = new Date(startDate);
} }
// Create start/end times // Create start/end times
let startTime = new Date(startDate) let startTime = new Date(startDate);
let endTime = new Date(endDate) let endTime = new Date(endDate);
if (event['start-time'] if (event['start-time'] && event['all-day'] !== "True") {
&& event['all-day'] !== "True") {
// Parse time if available and not all-day // Parse time if available and not all-day
let timeStr = event['start-time'] let timeStr = event['start-time'];
if (timeStr) { if (timeStr) {
// Match time with optional seconds and AM/PM // Match time with optional seconds and AM/PM
let timeParts = timeStr.match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i) let timeParts = timeStr.match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
if (timeParts) { if (timeParts) {
let hours = parseInt(timeParts[1]) let hours = parseInt(timeParts[1]);
let minutes = parseInt(timeParts[2]) let minutes = parseInt(timeParts[2]);
// Handle AM/PM conversion if present // Handle AM/PM conversion if present
if (timeParts[3]) { if (timeParts[3]) {
let period = timeParts[3].toUpperCase() let period = timeParts[3].toUpperCase();
if (period === 'PM' && hours !== 12) { if (period === 'PM' && hours !== 12) {
hours += 12 hours += 12;
} else if (period === 'AM' && hours === 12) { } else if (period === 'AM' && hours === 12) {
hours = 0 hours = 0;
} }
} }
startTime.setHours(hours, minutes) startTime.setHours(hours, minutes);
if (event['end-time']) { if (event['end-time']) {
let endTimeParts = event['end-time'].match( let endTimeParts = event['end-time'].match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i)
if (endTimeParts) { if (endTimeParts) {
let endHours = parseInt(endTimeParts[1]) let endHours = parseInt(endTimeParts[1]);
let endMinutes = parseInt(endTimeParts[2]) let endMinutes = parseInt(endTimeParts[2]);
// Handle AM/PM conversion if present // Handle AM/PM conversion if present
if (endTimeParts[3]) { if (endTimeParts[3]) {
let endPeriod = endTimeParts[3].toUpperCase() let endPeriod = endTimeParts[3].toUpperCase();
if (endPeriod === 'PM' && endHours !== 12) { if (endPeriod === 'PM' && endHours !== 12) {
endHours += 12 endHours += 12;
} else if (endPeriod === 'AM' && endHours === 12) { } else if (endPeriod === 'AM' && endHours === 12) {
endHours = 0 endHours = 0;
} }
} }
endTime.setHours(endHours, endMinutes) endTime.setHours(endHours, endMinutes);
} }
} else { } else {
// Default to 1 hour duration on same day // Default to 1 hour duration on same day
endTime = new Date(startTime) endTime = new Date(startTime);
endTime.setHours( endTime.setHours(startTime.getHours() + 1);
startTime.getHours() + 1)
} }
} }
} }
} }
// Create unique ID for this event (to track multi-day events) // Create unique ID for this event (to track multi-day events)
let eventId = event.title + "_" + event['start-date'] let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time'] || 'allday');
+ "_" + (event['start-time'] || 'allday')
// Create event object template // Create event object template
let extractedUrl = "" let extractedUrl = "";
if (!event.url && event.description) { if (!event.url && event.description) {
let urlMatch = event.description.match(/https?:\/\/[^\s]+/) let urlMatch = event.description.match(/https?:\/\/[^\s]+/);
if (urlMatch) { if (urlMatch) {
extractedUrl = urlMatch[0] extractedUrl = urlMatch[0];
} }
} }
let eventTemplate = { let eventTemplate = {
@@ -242,75 +239,71 @@ Singleton {
"calendar": "", "calendar": "",
"color": "", "color": "",
"allDay": event['all-day'] === "True", "allDay": event['all-day'] === "True",
"isMultiDay": startDate.toDateString( "isMultiDay": startDate.toDateString() !== endDate.toDateString()
) !== endDate.toDateString() };
}
// Add event to each day it spans // Add event to each day it spans
let currentDate = new Date(startDate) let currentDate = new Date(startDate);
while (currentDate <= endDate) { while (currentDate <= endDate) {
let dateKey = Qt.formatDate(currentDate, let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd");
"yyyy-MM-dd")
if (!newEventsByDate[dateKey]) if (!newEventsByDate[dateKey])
newEventsByDate[dateKey] = [] newEventsByDate[dateKey] = [];
// Check if this exact event is already added to this date (prevent duplicates) // Check if this exact event is already added to this date (prevent duplicates)
let existingEvent = newEventsByDate[dateKey].find( let existingEvent = newEventsByDate[dateKey].find(e => {
e => { return e.id === eventId;
return e.id === eventId });
})
if (existingEvent) { if (existingEvent) {
// Move to next day without adding duplicate // Move to next day without adding duplicate
currentDate.setDate(currentDate.getDate() + 1) currentDate.setDate(currentDate.getDate() + 1);
continue continue;
} }
// Create a copy of the event for this date // Create a copy of the event for this date
let dayEvent = Object.assign({}, eventTemplate) let dayEvent = Object.assign({}, eventTemplate);
// For multi-day events, adjust the display time for this specific day // For multi-day events, adjust the display time for this specific day
if (currentDate.getTime() === startDate.getTime()) { if (currentDate.getTime() === startDate.getTime()) {
// First day - use original start time // First day - use original start time
dayEvent.start = new Date(startTime) dayEvent.start = new Date(startTime);
} else { } else {
// Subsequent days - start at beginning of day for all-day events // Subsequent days - start at beginning of day for all-day events
dayEvent.start = new Date(currentDate) dayEvent.start = new Date(currentDate);
if (!dayEvent.allDay) if (!dayEvent.allDay)
dayEvent.start.setHours(0, 0, 0, 0) dayEvent.start.setHours(0, 0, 0, 0);
} }
if (currentDate.getTime() === endDate.getTime()) { if (currentDate.getTime() === endDate.getTime()) {
// Last day - use original end time // Last day - use original end time
dayEvent.end = new Date(endTime) dayEvent.end = new Date(endTime);
} else { } else {
// Earlier days - end at end of day for all-day events // Earlier days - end at end of day for all-day events
dayEvent.end = new Date(currentDate) dayEvent.end = new Date(currentDate);
if (!dayEvent.allDay) if (!dayEvent.allDay)
dayEvent.end.setHours(23, 59, 59, 999) dayEvent.end.setHours(23, 59, 59, 999);
} }
newEventsByDate[dateKey].push(dayEvent) newEventsByDate[dateKey].push(dayEvent);
// Move to next day // Move to next day
currentDate.setDate(currentDate.getDate() + 1) currentDate.setDate(currentDate.getDate() + 1);
} }
} }
} }
// Sort events by start time within each date // Sort events by start time within each date
for (let dateKey in newEventsByDate) { for (let dateKey in newEventsByDate) {
newEventsByDate[dateKey].sort((a, b) => { newEventsByDate[dateKey].sort((a, b) => {
return a.start.getTime( return a.start.getTime() - b.start.getTime();
) - b.start.getTime() });
})
} }
root.eventsByDate = newEventsByDate root.eventsByDate = newEventsByDate;
root.lastError = "" root.lastError = "";
} catch (error) { } catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString() root.lastError = "Failed to parse events JSON: " + error.toString();
root.eventsByDate = {} root.eventsByDate = {};
} }
// Reset for next run // Reset for next run
eventsProcess.rawOutput = "" eventsProcess.rawOutput = "";
} }
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: data => { onRead: data => {
eventsProcess.rawOutput += data + "\n" eventsProcess.rawOutput += data + "\n";
} }
} }
} }

View File

@@ -6,9 +6,11 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("ChangelogService")
readonly property string currentVersion: "1.4" readonly property string currentVersion: "1.4"
readonly property bool changelogEnabled: false readonly property bool changelogEnabled: false
@@ -101,7 +103,7 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("ChangelogService: Failed to create changelog marker"); log.warn("Failed to create changelog marker");
} }
} }
} }

View File

@@ -5,9 +5,11 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("ClipboardService")
readonly property int longTextThreshold: 200 readonly property int longTextThreshold: 200
@@ -78,7 +80,7 @@ Singleton {
} }
DMSService.sendRequest("clipboard.getHistory", null, function (response) { DMSService.sendRequest("clipboard.getHistory", null, function (response) {
if (response.error) { if (response.error) {
console.warn("ClipboardService: Failed to get history:", response.error); log.warn("Failed to get history:", response.error);
return; return;
} }
internalEntries = response.result || []; internalEntries = response.result || [];
@@ -144,7 +146,7 @@ Singleton {
"id": entry.id "id": entry.id
}, function (response) { }, function (response) {
if (response.error) { if (response.error) {
console.warn("ClipboardService: Failed to delete entry:", response.error); log.warn("Failed to delete entry:", response.error);
return; return;
} }
internalEntries = internalEntries.filter(e => e.id !== entry.id); internalEntries = internalEntries.filter(e => e.id !== entry.id);
@@ -169,7 +171,7 @@ Singleton {
"id": entry.id "id": entry.id
}, function (response) { }, function (response) {
if (response.error) { if (response.error) {
console.warn("ClipboardService: Failed to delete entry:", response.error); log.warn("Failed to delete entry:", response.error);
return; return;
} }
internalEntries = internalEntries.filter(e => e.id !== entry.id); internalEntries = internalEntries.filter(e => e.id !== entry.id);
@@ -223,7 +225,7 @@ Singleton {
const savedCount = pinnedCount; const savedCount = pinnedCount;
DMSService.sendRequest("clipboard.clearHistory", null, function (response) { DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) { if (response.error) {
console.warn("ClipboardService: Failed to clear history:", response.error); log.warn("Failed to clear history:", response.error);
return; return;
} }
refresh(); refresh();

View File

@@ -7,9 +7,11 @@ import Quickshell.I3
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("CompositorService")
property bool isHyprland: false property bool isHyprland: false
property bool isNiri: false property bool isNiri: false
@@ -52,7 +54,7 @@ Singleton {
randrScales = scales; randrScales = scales;
} }
} catch (e) { } catch (e) {
console.warn("CompositorService: failed to parse randr data:", e); log.warn("failed to parse randr data:", e);
} }
} }
randrReady = true; randrReady = true;
@@ -379,9 +381,7 @@ Singleton {
const focusedWin = NiriService.windows.find(nw => nw.is_focused); const focusedWin = NiriService.windows.find(nw => nw.is_focused);
if (!focusedWin) if (!focusedWin)
return []; return [];
const screenWsIds = new Set( const screenWsIds = new Set(NiriService.allWorkspaces.filter(ws => ws.output === screenName).map(ws => ws.id));
NiriService.allWorkspaces.filter(ws => ws.output === screenName).map(ws => ws.id)
);
return screenWsIds.has(focusedWin.workspace_id) ? toplevels : []; return screenWsIds.has(focusedWin.workspace_id) ? toplevels : [];
} }
return NiriService.filterCurrentDisplay(toplevels, screenName); return NiriService.filterCurrentDisplay(toplevels, screenName);
@@ -454,7 +454,7 @@ Singleton {
} }
} }
} catch (e) { } catch (e) {
console.warn("CompositorService: workspace snapshot failed:", e); log.warn("workspace snapshot failed:", e);
} }
if (currentWorkspaceId === null) if (currentWorkspaceId === null)
@@ -498,7 +498,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "hyprland"; compositor = "hyprland";
console.info("CompositorService: Detected Hyprland"); log.info("Detected Hyprland");
return; return;
} }
@@ -513,7 +513,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "niri"; compositor = "niri";
console.info("CompositorService: Detected Niri with socket:", niriSocket); log.info("Detected Niri with socket:", niriSocket);
NiriService.generateNiriBlurrule(); NiriService.generateNiriBlurrule();
} }
}, 0); }, 0);
@@ -531,7 +531,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "sway"; compositor = "sway";
console.info("CompositorService: Detected Sway with socket:", swaySocket); log.info("Detected Sway with socket:", swaySocket);
} }
}, 0); }, 0);
return; return;
@@ -548,7 +548,7 @@ Singleton {
isMiracle = true; isMiracle = true;
isLabwc = false; isLabwc = false;
compositor = "miracle"; compositor = "miracle";
console.info("CompositorService: Detected Miracle WM with socket:", miracleSocket); log.info("Detected Miracle WM with socket:", miracleSocket);
} }
}, 0); }, 0);
return; return;
@@ -565,7 +565,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "scroll"; compositor = "scroll";
console.info("CompositorService: Detected Scroll with socket:", scrollSocket); log.info("Detected Scroll with socket:", scrollSocket);
} }
}, 0); }, 0);
return; return;
@@ -580,7 +580,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = true; isLabwc = true;
compositor = "labwc"; compositor = "labwc";
console.info("CompositorService: Detected LabWC with PID:", labwcPid); log.info("Detected LabWC with PID:", labwcPid);
return; return;
} }
@@ -595,7 +595,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "unknown"; compositor = "unknown";
console.warn("CompositorService: No compositor detected"); log.warn("No compositor detected");
} }
} }
@@ -618,7 +618,7 @@ Singleton {
isMiracle = false; isMiracle = false;
isLabwc = false; isLabwc = false;
compositor = "dwl"; compositor = "dwl";
console.info("CompositorService: Detected DWL via DMS capability"); log.info("Detected DWL via DMS capability");
} }
} }
@@ -638,7 +638,7 @@ Singleton {
if (isLabwc) { if (isLabwc) {
Quickshell.execDetached(["dms", "dpms", "off"]); Quickshell.execDetached(["dms", "dpms", "off"]);
} }
console.warn("CompositorService: Cannot power off monitors, unknown compositor"); log.warn("Cannot power off monitors, unknown compositor");
} }
function powerOnMonitors() { function powerOnMonitors() {
@@ -657,12 +657,12 @@ Singleton {
if (isLabwc) { if (isLabwc) {
Quickshell.execDetached(["dms", "dpms", "on"]); Quickshell.execDetached(["dms", "dpms", "on"]);
} }
console.warn("CompositorService: Cannot power on monitors, unknown compositor"); log.warn("Cannot power on monitors, unknown compositor");
} }
function _dwlPowerOffMonitors() { function _dwlPowerOffMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) { if (!Quickshell.screens || Quickshell.screens.length === 0) {
console.warn("CompositorService: No screens available for DWL power off"); log.warn("No screens available for DWL power off");
return; return;
} }
@@ -676,7 +676,7 @@ Singleton {
function _dwlPowerOnMonitors() { function _dwlPowerOnMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) { if (!Quickshell.screens || Quickshell.screens.length === 0) {
console.warn("CompositorService: No screens available for DWL power on"); log.warn("No screens available for DWL power on");
return; return;
} }

Some files were not shown because too many files have changed in this diff Show More