mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 02:22:06 -04:00
Compare commits
19 Commits
dc5636bed5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1cbd0994f | ||
|
|
c81645bacb | ||
|
|
cdc4ca7e1f | ||
|
|
7d92842ff2 | ||
|
|
d8bf3bdfe8 | ||
|
|
23ed795e85 | ||
|
|
2877c63c97 | ||
|
|
86096db26b | ||
|
|
f76724f7cd | ||
|
|
3b96c6ab22 | ||
|
|
1467f5dba9 | ||
|
|
baaa30c94e | ||
|
|
24a3cd5a3d | ||
|
|
65151dbfd7 | ||
|
|
7bd9574868 | ||
|
|
a4cfdf4a59 | ||
|
|
fd651dc943 | ||
|
|
919b09fc96 | ||
|
|
aeb3fdd637 |
@@ -20,3 +20,11 @@ repos:
|
||||
language: system
|
||||
files: ^core/.*\.(go|mod|sum)$
|
||||
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/)
|
||||
|
||||
@@ -26,6 +26,17 @@ var runCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
daemon, _ := cmd.Flags().GetBool("daemon")
|
||||
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 {
|
||||
runShellDaemon(session)
|
||||
} else {
|
||||
@@ -527,5 +538,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
randrCmd,
|
||||
blurCmd,
|
||||
trashCmd,
|
||||
systemCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -267,6 +269,8 @@ func runSetupDmsConfig(name string) error {
|
||||
func runSetup() error {
|
||||
fmt.Println("=== DMS Configuration Setup ===")
|
||||
|
||||
ensureInputGroup()
|
||||
|
||||
wm, wmSelected := promptCompositor()
|
||||
terminal, terminalSelected := promptTerminal()
|
||||
useSystemd := promptSystemd()
|
||||
@@ -340,6 +344,37 @@ func runSetup() error {
|
||||
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) {
|
||||
fmt.Println("Select compositor:")
|
||||
fmt.Println("1) Niri")
|
||||
|
||||
277
core/cmd/dms/commands_system.go
Normal file
277
core/cmd/dms/commands_system.go
Normal 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
|
||||
}
|
||||
@@ -15,6 +15,8 @@ func init() {
|
||||
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("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")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
|
||||
@@ -15,6 +15,8 @@ func init() {
|
||||
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("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")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
|
||||
@@ -80,6 +80,16 @@ func getRuntimeDir() string {
|
||||
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 {
|
||||
_, err := exec.LookPath("systemd-run")
|
||||
return err == nil
|
||||
@@ -216,6 +226,8 @@ func runShellInteractive(session bool) {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||
}
|
||||
|
||||
cmd.Env = appendLogEnv(cmd.Env)
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -459,6 +471,8 @@ func runShellDaemon(session bool) {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening /dev/null: %v", err)
|
||||
|
||||
42
core/go.mod
42
core/go.mod
@@ -6,11 +6,11 @@ toolchain go1.26.1
|
||||
|
||||
require (
|
||||
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/bubbletea v1.3.10
|
||||
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/godbus/dbus/v5 v5.2.2
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||
@@ -20,28 +20,27 @@ require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
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
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/image v0.39.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // 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/fogleman/gg v1.3.0 // 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/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/klauspost/cpuid/v2 v2.3.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/stretchr/objx v0.5.3 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // 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/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/term v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // 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/lucasb-eyer/go-colorful v1.3.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
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/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/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/text v0.36.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
||||
102
core/go.sum
102
core/go.sum
@@ -1,14 +1,14 @@
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
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.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||
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/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/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.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/chroma/v2 v2.24.0 h1:zrg+k0tAaVbM8whaT2hR5DOUqAdopsDaH998EGi6Llk=
|
||||
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.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
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/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
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.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
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/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
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/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
||||
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
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/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/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
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/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
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/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.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
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/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
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/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/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
|
||||
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
|
||||
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/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||
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-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
|
||||
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-20260424211911-732291493fb8/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
|
||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
|
||||
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-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
|
||||
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/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
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/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/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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
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/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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.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.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
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/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
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/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
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.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
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/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=
|
||||
@@ -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/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||
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.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
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/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
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.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
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-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.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
cblog "github.com/charmbracelet/log"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
// 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 (
|
||||
logger *Logger
|
||||
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 {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
@@ -86,7 +108,7 @@ func GetLogger() *Logger {
|
||||
SetString(" DEBUG").
|
||||
Foreground(lipgloss.Color("4"))
|
||||
|
||||
base := cblog.New(os.Stderr)
|
||||
base := cblog.New(logStderr)
|
||||
base.SetStyles(styles)
|
||||
base.SetReportTimestamp(false)
|
||||
|
||||
@@ -98,10 +120,85 @@ func GetLogger() *Logger {
|
||||
base.SetPrefix(" go")
|
||||
|
||||
logger = &Logger{base}
|
||||
|
||||
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
|
||||
_ = SetLogFile(path)
|
||||
}
|
||||
})
|
||||
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
|
||||
|
||||
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) }
|
||||
|
||||
@@ -60,6 +60,7 @@ var templateRegistry = []TemplateDef{
|
||||
{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: "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: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
||||
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
|
||||
|
||||
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
|
||||
|
||||
func InitializeManager() (*Manager, error) {
|
||||
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()
|
||||
|
||||
@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
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"
|
||||
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
@@ -202,6 +203,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
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 {
|
||||
case "ping":
|
||||
models.Respond(conn, req.ID, "pong")
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"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/trayrecovery"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
@@ -75,6 +76,7 @@ var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
var trayRecoveryManager *trayrecovery.Manager
|
||||
var locationManager *location.Manager
|
||||
var sysUpdateManager *sysupdate.Manager
|
||||
var geoClientInstance geolocation.Client
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
@@ -421,6 +423,19 @@ func InitializeLocationManager(geoClient geolocation.Client) error {
|
||||
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) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -506,6 +521,10 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
if sysUpdateManager != nil {
|
||||
caps = append(caps, "sysupdate")
|
||||
}
|
||||
|
||||
return Capabilities{Capabilities: caps}
|
||||
}
|
||||
|
||||
@@ -576,6 +595,10 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
|
||||
if sysUpdateManager != nil {
|
||||
caps = append(caps, "sysupdate")
|
||||
}
|
||||
|
||||
return ServerInfo{
|
||||
APIVersion: APIVersion,
|
||||
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 {
|
||||
wg.Add(1)
|
||||
dbusChan := dbusManager.SubscribeSignals(dbusClientID)
|
||||
@@ -1348,6 +1403,9 @@ func cleanupManagers() {
|
||||
if locationManager != nil {
|
||||
locationManager.Close()
|
||||
}
|
||||
if sysUpdateManager != nil {
|
||||
sysUpdateManager.Close()
|
||||
}
|
||||
if geoClientInstance != nil {
|
||||
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.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
||||
|
||||
|
||||
96
core/internal/server/sysupdate/backend.go
Normal file
96
core/internal/server/sysupdate/backend.go
Normal 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
|
||||
}
|
||||
79
core/internal/server/sysupdate/backend_apt.go
Normal file
79
core/internal/server/sysupdate/backend_apt.go
Normal 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
|
||||
}
|
||||
72
core/internal/server/sysupdate/backend_apt_test.go
Normal file
72
core/internal/server/sysupdate/backend_apt_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
core/internal/server/sysupdate/backend_dnf.go
Normal file
125
core/internal/server/sysupdate/backend_dnf.go
Normal 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
|
||||
}
|
||||
80
core/internal/server/sysupdate/backend_dnf_test.go
Normal file
80
core/internal/server/sysupdate/backend_dnf_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
169
core/internal/server/sysupdate/backend_flatpak.go
Normal file
169
core/internal/server/sysupdate/backend_flatpak.go
Normal 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
|
||||
}
|
||||
150
core/internal/server/sysupdate/backend_flatpak_test.go
Normal file
150
core/internal/server/sysupdate/backend_flatpak_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
258
core/internal/server/sysupdate/backend_pacman.go
Normal file
258
core/internal/server/sysupdate/backend_pacman.go
Normal 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
|
||||
}
|
||||
114
core/internal/server/sysupdate/backend_pacman_test.go
Normal file
114
core/internal/server/sysupdate/backend_pacman_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
core/internal/server/sysupdate/backend_rpmostree.go
Normal file
125
core/internal/server/sysupdate/backend_rpmostree.go
Normal 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})
|
||||
}
|
||||
104
core/internal/server/sysupdate/backend_rpmostree_test.go
Normal file
104
core/internal/server/sysupdate/backend_rpmostree_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
core/internal/server/sysupdate/backend_zypper.go
Normal file
83
core/internal/server/sysupdate/backend_zypper.go
Normal 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})
|
||||
}
|
||||
80
core/internal/server/sysupdate/backend_zypper_test.go
Normal file
80
core/internal/server/sysupdate/backend_zypper_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
125
core/internal/server/sysupdate/executor.go
Normal file
125
core/internal/server/sysupdate/executor.go
Normal 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}
|
||||
}
|
||||
}
|
||||
55
core/internal/server/sysupdate/handlers.go
Normal file
55
core/internal/server/sysupdate/handlers.go
Normal 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())
|
||||
}
|
||||
506
core/internal/server/sysupdate/manager.go
Normal file
506
core/internal/server/sysupdate/manager.go
Normal 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
|
||||
}
|
||||
86
core/internal/server/sysupdate/types.go
Normal file
86
core/internal/server/sysupdate/types.go
Normal 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
|
||||
}
|
||||
@@ -111,7 +111,7 @@
|
||||
inherit version;
|
||||
pname = "dms-shell";
|
||||
src = ./core;
|
||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||
vendorHash = "sha256-kPu3MLqhLaCaBpCwIP8JXep0J/Z45kxDFOEY8JvcWdU=";
|
||||
|
||||
subPackages = [ "cmd/dms" ];
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("CacheData")
|
||||
|
||||
readonly property int cacheConfigVersion: 1
|
||||
|
||||
@@ -131,7 +133,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CacheData: Failed to parse cache:", e.message);
|
||||
log.warn("Failed to parse cache:", e.message);
|
||||
} finally {
|
||||
_loading = false;
|
||||
}
|
||||
@@ -149,7 +151,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function migrateFromUndefinedToV1(cache) {
|
||||
console.info("CacheData: Migrating configuration from undefined to version 1");
|
||||
log.info("Migrating configuration from undefined to version 1");
|
||||
}
|
||||
|
||||
function cleanupUnusedKeys() {
|
||||
@@ -164,7 +166,7 @@ Singleton {
|
||||
|
||||
for (const key in cache) {
|
||||
if (!validKeys.includes(key)) {
|
||||
console.log("CacheData: Removing unused key:", key);
|
||||
log.debug("Removing unused key:", key);
|
||||
delete cache[key];
|
||||
needsSave = true;
|
||||
}
|
||||
@@ -174,7 +176,7 @@ Singleton {
|
||||
cacheFile.setText(JSON.stringify(cache, null, 2));
|
||||
}
|
||||
} 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())
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.warn("CacheData: Failed to parse launcher cache:", e.message);
|
||||
log.warn("Failed to parse launcher cache:", e.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -220,7 +222,7 @@ Singleton {
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
if (!isGreeterMode) {
|
||||
console.info("CacheData: No cache file found, starting fresh");
|
||||
log.info("No cache file found, starting fresh");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("I18n")
|
||||
|
||||
property string _resolvedLocale: "en"
|
||||
|
||||
@@ -54,15 +56,15 @@ Singleton {
|
||||
try {
|
||||
root.translations = JSON.parse(text());
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -105,14 +107,14 @@ Singleton {
|
||||
_selectedPath = fileUrl;
|
||||
translationsLoaded = false;
|
||||
translations = ({});
|
||||
console.info(`I18n: Using locale '${localeTag}' from ${fileUrl}`);
|
||||
log.info(`I18n: Using locale '${localeTag}' from ${fileUrl}`);
|
||||
}
|
||||
|
||||
function _fallbackToEnglish() {
|
||||
_selectedPath = "";
|
||||
translationsLoaded = false;
|
||||
translations = ({});
|
||||
console.warn("I18n: Falling back to built-in English strings");
|
||||
log.warn("Falling back to built-in English strings");
|
||||
}
|
||||
|
||||
function tr(term, context) {
|
||||
|
||||
@@ -3,9 +3,11 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("Proc")
|
||||
|
||||
readonly property int noTimeout: -1
|
||||
property int defaultDebounceMs: 50
|
||||
@@ -112,7 +114,7 @@ Singleton {
|
||||
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
|
||||
entry.callback(safeOutput, safeExitCode);
|
||||
} 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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import "settings/SessionStore.js" as Store
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SessionData")
|
||||
|
||||
readonly property int sessionConfigVersion: 3
|
||||
|
||||
@@ -30,9 +31,36 @@ Singleton {
|
||||
property bool isLightMode: false
|
||||
property bool doNotDisturb: false
|
||||
property real doNotDisturbUntil: 0
|
||||
property string terminalOverride: ""
|
||||
property bool isSwitchingMode: false
|
||||
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 {
|
||||
id: dndExpireTimer
|
||||
repeat: false
|
||||
@@ -230,7 +258,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -310,7 +338,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -525,7 +553,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
log.warn("Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -622,7 +650,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
log.warn("Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -653,7 +681,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
log.warn("Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -684,7 +712,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
log.warn("Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -715,7 +743,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!screen) {
|
||||
console.warn("SessionData: Screen not found");
|
||||
log.warn("Screen not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import "settings/SettingsStore.js" as Store
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SettingsData")
|
||||
|
||||
readonly property int settingsConfigVersion: 5
|
||||
|
||||
@@ -493,6 +494,7 @@ Singleton {
|
||||
property bool matugenTemplatePywalfox: true
|
||||
property bool matugenTemplateZenBrowser: true
|
||||
property bool matugenTemplateVesktop: true
|
||||
property bool matugenTemplateVencord: true
|
||||
property bool matugenTemplateEquibop: true
|
||||
property bool matugenTemplateGhostty: true
|
||||
property bool matugenTemplateKitty: true
|
||||
@@ -640,6 +642,9 @@ Singleton {
|
||||
property bool updaterUseCustomCommand: false
|
||||
property string updaterCustomCommand: ""
|
||||
property string updaterTerminalAdditionalParams: ""
|
||||
property int updaterIntervalSeconds: 1800
|
||||
property bool updaterIncludeFlatpak: true
|
||||
property bool updaterAllowAUR: true
|
||||
|
||||
property string displayNameMode: "system"
|
||||
property var screenPreferences: ({})
|
||||
@@ -1291,7 +1296,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
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));
|
||||
applyStoredTheme();
|
||||
} finally {
|
||||
@@ -1312,12 +1317,12 @@ Singleton {
|
||||
if (_isReadOnly) {
|
||||
_hasUnsavedChanges = _checkForUnsavedChanges();
|
||||
if (!wasReadOnly)
|
||||
console.info("SettingsData: settings.json is now read-only");
|
||||
log.info("settings.json is now read-only");
|
||||
} else {
|
||||
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
|
||||
_hasUnsavedChanges = false;
|
||||
if (wasReadOnly)
|
||||
console.info("SettingsData: settings.json is now writable");
|
||||
log.info("settings.json is now writable");
|
||||
if (_pendingMigration)
|
||||
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
|
||||
}
|
||||
@@ -1371,7 +1376,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
const msg = e.message || String(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();
|
||||
}
|
||||
}
|
||||
@@ -1388,7 +1393,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_pluginParseError = true;
|
||||
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));
|
||||
pluginSettings = {};
|
||||
} finally {
|
||||
@@ -2791,7 +2796,7 @@ Singleton {
|
||||
} catch (e) {
|
||||
_parseError = true;
|
||||
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));
|
||||
} finally {
|
||||
_loading = false;
|
||||
@@ -2826,7 +2831,7 @@ Singleton {
|
||||
if (!isGreeterMode) {
|
||||
const msg = String(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import "StockThemes.js" as StockThemes
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("Theme")
|
||||
|
||||
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"
|
||||
@@ -148,7 +149,7 @@ Singleton {
|
||||
}
|
||||
|
||||
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 iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
|
||||
@@ -376,7 +377,7 @@ Singleton {
|
||||
"use": true
|
||||
}, response => {
|
||||
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) {
|
||||
@@ -389,13 +390,13 @@ Singleton {
|
||||
"longitude": SessionData.longitude
|
||||
}, locationResponse => {
|
||||
if (locationResponse?.error) {
|
||||
console.warn("Theme automation: Failed to set location", locationResponse.error);
|
||||
log.warn("Theme automation: Failed to set location", locationResponse.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (workerRunning) {
|
||||
console.info("Theme: Worker already running, queueing request");
|
||||
log.info("Worker already running, queueing request");
|
||||
pendingThemeRequest = {
|
||||
kind,
|
||||
value,
|
||||
@@ -1542,7 +1543,7 @@ Singleton {
|
||||
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) {
|
||||
NiriService.suppressNextToast();
|
||||
@@ -1557,7 +1558,7 @@ Singleton {
|
||||
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
|
||||
};
|
||||
|
||||
console.log("Theme: Starting matugen worker");
|
||||
log.debug("Starting matugen worker");
|
||||
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,];
|
||||
@@ -1581,7 +1582,7 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const skipTemplates = [];
|
||||
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 {
|
||||
if (!SettingsData.matugenTemplateGtk)
|
||||
skipTemplates.push("gtk");
|
||||
@@ -1603,6 +1604,8 @@ Singleton {
|
||||
skipTemplates.push("zenbrowser");
|
||||
if (!SettingsData.matugenTemplateVesktop)
|
||||
skipTemplates.push("vesktop");
|
||||
if (!SettingsData.matugenTemplateVencord)
|
||||
skipTemplates.push("vencord");
|
||||
if (!SettingsData.matugenTemplateEquibop)
|
||||
skipTemplates.push("equibop");
|
||||
if (!SettingsData.matugenTemplateGhostty)
|
||||
@@ -1715,7 +1718,7 @@ Singleton {
|
||||
}
|
||||
|
||||
if (!darkTheme || !darkTheme.primary) {
|
||||
console.warn("Theme data not available for:", currentTheme);
|
||||
log.warn("Theme data not available for:", currentTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1953,10 +1956,10 @@ Singleton {
|
||||
id: systemThemeGenerator
|
||||
running: false
|
||||
stdout: SplitParser {
|
||||
onRead: data => console.info("Theme worker:", data)
|
||||
onRead: data => log.info("Theme worker:", data)
|
||||
}
|
||||
stderr: SplitParser {
|
||||
onRead: data => console.warn("Theme worker:", data)
|
||||
onRead: data => log.warn("Theme worker:", data)
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
@@ -1965,18 +1968,18 @@ Singleton {
|
||||
|
||||
switch (exitCode) {
|
||||
case 0:
|
||||
console.info("Theme: Matugen worker completed successfully");
|
||||
log.info("Matugen worker completed successfully");
|
||||
root.matugenCompleted(currentMode, "success");
|
||||
break;
|
||||
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");
|
||||
break;
|
||||
default:
|
||||
if (typeof ToastService !== "undefined") {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1985,7 +1988,7 @@ Singleton {
|
||||
|
||||
const req = pendingThemeRequest;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2039,7 +2042,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Theme: Failed to parse dynamic colors:", e);
|
||||
log.error("Failed to parse dynamic colors:", e);
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.wallpaperErrorStatus = "error";
|
||||
ToastService.showError("Dynamic colors parse error: " + e.message);
|
||||
@@ -2059,11 +2062,11 @@ Singleton {
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
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;
|
||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
||||
if (!isGreeterMode && matugenAvailable && rawWallpaperPath) {
|
||||
console.log("Theme: Matugen available, triggering immediate regeneration");
|
||||
log.debug("Matugen available, triggering immediate regeneration");
|
||||
generateSystemThemesFromCurrentTheme();
|
||||
}
|
||||
}
|
||||
@@ -2187,7 +2190,7 @@ Singleton {
|
||||
"endMinute": endMinute
|
||||
}, response => {
|
||||
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 (SessionData.nightModeUseIPLocation) {
|
||||
console.warn("Theme automation: Waiting for IP location from backend");
|
||||
log.warn("Theme automation: Waiting for IP location from backend");
|
||||
} else {
|
||||
console.warn("Theme automation: Location mode requires coordinates");
|
||||
log.warn("Theme automation: Location mode requires coordinates");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2364,7 +2367,7 @@ Singleton {
|
||||
"use": true
|
||||
}, response => {
|
||||
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;
|
||||
@@ -2378,7 +2381,7 @@ Singleton {
|
||||
"longitude": SessionData.longitude
|
||||
}, locResp => {
|
||||
if (locResp?.error) {
|
||||
console.warn("Theme automation: Failed to set location", locResp.error);
|
||||
log.warn("Theme automation: Failed to set location", locResp.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ var SPEC = {
|
||||
isLightMode: { def: false },
|
||||
doNotDisturb: { def: false },
|
||||
doNotDisturbUntil: { def: 0 },
|
||||
terminalOverride: { def: "" },
|
||||
|
||||
wallpaperPath: { def: "" },
|
||||
perMonitorWallpaper: { def: false },
|
||||
|
||||
@@ -302,6 +302,7 @@ var SPEC = {
|
||||
matugenTemplatePywalfox: { def: true },
|
||||
matugenTemplateZenBrowser: { def: true },
|
||||
matugenTemplateVesktop: { def: true },
|
||||
matugenTemplateVencord: { def: true },
|
||||
matugenTemplateEquibop: { def: true },
|
||||
matugenTemplateGhostty: { def: true },
|
||||
matugenTemplateKitty: { def: true },
|
||||
@@ -428,6 +429,9 @@ var SPEC = {
|
||||
updaterUseCustomCommand: { def: false },
|
||||
updaterCustomCommand: { def: "" },
|
||||
updaterTerminalAdditionalParams: { def: "" },
|
||||
updaterIntervalSeconds: { def: 1800 },
|
||||
updaterIncludeFlatpak: { def: true },
|
||||
updaterAllowAUR: { def: true },
|
||||
|
||||
displayNameMode: { def: "system" },
|
||||
screenPreferences: { def: {} },
|
||||
|
||||
@@ -27,6 +27,7 @@ import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DMSShell")
|
||||
|
||||
property bool osdSurfacesLoaded: true
|
||||
property int pendingOsdResumeReloads: 0
|
||||
@@ -54,7 +55,7 @@ Item {
|
||||
item.popoutService = PopoutService;
|
||||
}
|
||||
item.pluginId = pluginId;
|
||||
console.info("Daemon plugin loaded:", pluginId);
|
||||
log.info("Daemon plugin loaded:", pluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +94,7 @@ Item {
|
||||
}
|
||||
|
||||
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: {
|
||||
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;
|
||||
}
|
||||
|
||||
console.log("FilePicker: Launching", cmd);
|
||||
log.debug("FilePicker: Launching", cmd);
|
||||
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", cmd]
|
||||
@@ -805,10 +806,10 @@ Item {
|
||||
}
|
||||
|
||||
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) {
|
||||
console.warn("DMSShell: Invalid app picker request data");
|
||||
log.warn("Invalid app picker request data");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -895,7 +896,12 @@ Item {
|
||||
|
||||
SystemUpdatePopout {
|
||||
id: systemUpdatePopout
|
||||
onPopoutClosed: PopoutService.unloadSystemUpdate()
|
||||
onPopoutClosed: {
|
||||
if (systemUpdatePopout._reopenAfterUpgrade) {
|
||||
return;
|
||||
}
|
||||
PopoutService.unloadSystemUpdate();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.systemUpdatePopout = systemUpdatePopout;
|
||||
@@ -1092,12 +1098,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: powerProfileWatcherLoader
|
||||
active: SettingsData.osdPowerProfileEnabled
|
||||
source: "Services/PowerProfileWatcher.qml"
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: hyprlandOverviewLoader
|
||||
active: CompositorService.isHyprland
|
||||
|
||||
@@ -9,6 +9,7 @@ import qs.Modules.Settings.DisplayConfig
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DMSShellIPC")
|
||||
|
||||
required property var powerMenuModalLoader
|
||||
required property var processListModalLoader
|
||||
@@ -861,7 +862,7 @@ Item {
|
||||
|
||||
function set(key: string, value: string): string {
|
||||
if (!(key in SettingsData)) {
|
||||
console.warn("Cannot set property, not found:", key);
|
||||
log.warn("Cannot set property, not found:", key);
|
||||
return "SETTINGS_INVALID_KEY";
|
||||
}
|
||||
|
||||
@@ -894,12 +895,12 @@ Item {
|
||||
throw "Unsupported type";
|
||||
}
|
||||
|
||||
console.warn("Setting:", key, value);
|
||||
log.warn("Setting:", key, value);
|
||||
SettingsData[key] = value;
|
||||
SettingsData.saveSettings();
|
||||
return "SETTINGS_SET_SUCCESS";
|
||||
} catch (e) {
|
||||
console.warn("Failed to set property:", key, "error:", e);
|
||||
log.warn("Failed to set property:", key, "error:", e);
|
||||
return "SETTINGS_SET_FAILURE";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
@@ -7,6 +6,7 @@ import qs.Services
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("AppPickerModal")
|
||||
|
||||
property string title: I18n.tr("Select Application")
|
||||
property string targetData: ""
|
||||
@@ -30,52 +30,52 @@ DankModal {
|
||||
onBackgroundClicked: close()
|
||||
|
||||
onDialogClosed: {
|
||||
searchQuery = ""
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive = false
|
||||
searchQuery = "";
|
||||
selectedIndex = 0;
|
||||
keyboardNavigationActive = false;
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
searchQuery = ""
|
||||
updateApplicationList()
|
||||
selectedIndex = 0
|
||||
searchQuery = "";
|
||||
updateApplicationList();
|
||||
selectedIndex = 0;
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.text = ""
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
contentLoader.item.searchField.text = "";
|
||||
contentLoader.item.searchField.forceActiveFocus();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function updateApplicationList() {
|
||||
applicationsModel.clear()
|
||||
const apps = AppSearchService.applications
|
||||
const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {}
|
||||
let filteredApps = []
|
||||
applicationsModel.clear();
|
||||
const apps = AppSearchService.applications;
|
||||
const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {};
|
||||
let filteredApps = [];
|
||||
|
||||
for (const app of apps) {
|
||||
if (!app || !app.categories) continue
|
||||
|
||||
let matchesCategory = categoryFilter.length === 0
|
||||
if (!app || !app.categories)
|
||||
continue;
|
||||
let matchesCategory = categoryFilter.length === 0;
|
||||
|
||||
if (categoryFilter.length > 0) {
|
||||
try {
|
||||
for (const cat of app.categories) {
|
||||
if (categoryFilter.includes(cat)) {
|
||||
matchesCategory = true
|
||||
break
|
||||
matchesCategory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("AppPicker: Error iterating categories for", app.name, ":", e)
|
||||
continue
|
||||
log.warn("AppPicker: Error iterating categories for", app.name, ":", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesCategory) {
|
||||
const name = app.name || ""
|
||||
const lowerName = name.toLowerCase()
|
||||
const lowerQuery = searchQuery.toLowerCase()
|
||||
const name = app.name || "";
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerQuery = searchQuery.toLowerCase();
|
||||
|
||||
if (searchQuery === "" || lowerName.includes(lowerQuery)) {
|
||||
filteredApps.push({
|
||||
@@ -84,21 +84,21 @@ DankModal {
|
||||
exec: app.exec || app.execString || "",
|
||||
startupClass: app.startupWMClass || "",
|
||||
appData: app
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredApps.sort((a, b) => {
|
||||
const aId = a.appData.id || a.appData.execString || a.appData.exec || ""
|
||||
const bId = b.appData.id || b.appData.execString || b.appData.exec || ""
|
||||
const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0
|
||||
const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0
|
||||
const aId = a.appData.id || a.appData.execString || a.appData.exec || "";
|
||||
const bId = b.appData.id || b.appData.execString || b.appData.exec || "";
|
||||
const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0;
|
||||
const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0;
|
||||
if (aUsage !== bUsage) {
|
||||
return bUsage - aUsage
|
||||
return bUsage - aUsage;
|
||||
}
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
return (a.name || "").localeCompare(b.name || "");
|
||||
});
|
||||
|
||||
filteredApps.forEach(app => {
|
||||
applicationsModel.append({
|
||||
@@ -107,10 +107,10 @@ DankModal {
|
||||
exec: app.exec,
|
||||
startupClass: app.startupClass,
|
||||
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()
|
||||
@@ -129,56 +129,57 @@ DankModal {
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (applicationsModel.count === 0) return
|
||||
if (applicationsModel.count === 0)
|
||||
return;
|
||||
|
||||
// Toggle view mode with Tab key
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
root.viewMode = root.viewMode === "grid" ? "list" : "grid"
|
||||
event.accepted = true
|
||||
return
|
||||
root.viewMode = root.viewMode === "grid" ? "list" : "grid";
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.viewMode === "grid") {
|
||||
if (event.key === Qt.Key_Left) {
|
||||
root.keyboardNavigationActive = true
|
||||
root.selectedIndex = Math.max(0, root.selectedIndex - 1)
|
||||
event.accepted = true
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = Math.max(0, root.selectedIndex - 1);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Right) {
|
||||
root.keyboardNavigationActive = true
|
||||
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
|
||||
event.accepted = true
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.keyboardNavigationActive = true
|
||||
root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns)
|
||||
event.accepted = true
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.keyboardNavigationActive = true
|
||||
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns)
|
||||
event.accepted = true
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns);
|
||||
event.accepted = true;
|
||||
}
|
||||
} else {
|
||||
if (event.key === Qt.Key_Up) {
|
||||
root.keyboardNavigationActive = true
|
||||
root.selectedIndex = Math.max(0, root.selectedIndex - 1)
|
||||
event.accepted = true
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = Math.max(0, root.selectedIndex - 1);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.keyboardNavigationActive = true
|
||||
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
|
||||
event.accepted = true
|
||||
root.keyboardNavigationActive = true;
|
||||
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) {
|
||||
const app = applicationsModel.get(root.selectedIndex)
|
||||
launchApplication(app)
|
||||
const app = applicationsModel.get(root.selectedIndex);
|
||||
launchApplication(app);
|
||||
}
|
||||
event.accepted = true
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +218,7 @@ DankModal {
|
||||
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"
|
||||
onClicked: {
|
||||
root.viewMode = "list"
|
||||
root.viewMode = "list";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ DankModal {
|
||||
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"
|
||||
onClicked: {
|
||||
root.viewMode = "grid"
|
||||
root.viewMode = "grid";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,42 +258,42 @@ DankModal {
|
||||
keyForwardTargets: [appContent]
|
||||
|
||||
onTextEdited: {
|
||||
root.searchQuery = text
|
||||
root.searchQuery = text;
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
event.accepted = true
|
||||
return
|
||||
root.close();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
|
||||
const hasText = text.length > 0
|
||||
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key);
|
||||
const hasText = text.length > 0;
|
||||
|
||||
if (isEnterKey && hasText) {
|
||||
if (root.keyboardNavigationActive && applicationsModel.count > 0) {
|
||||
const app = applicationsModel.get(root.selectedIndex)
|
||||
launchApplication(app)
|
||||
const app = applicationsModel.get(root.selectedIndex);
|
||||
launchApplication(app);
|
||||
} else if (applicationsModel.count > 0) {
|
||||
const app = applicationsModel.get(0)
|
||||
launchApplication(app)
|
||||
const app = applicationsModel.get(0);
|
||||
launchApplication(app);
|
||||
}
|
||||
event.accepted = true
|
||||
return
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
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 isEmptyEnter = isEnterKey && !hasText
|
||||
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 isEmptyEnter = isEnterKey && !hasText;
|
||||
|
||||
event.accepted = !(isNavigationKey || isEmptyEnter)
|
||||
event.accepted = !(isNavigationKey || isEmptyEnter);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (!root.shouldBeVisible) {
|
||||
searchField.focus = false
|
||||
searchField.focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,12 +304,12 @@ DankModal {
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: {
|
||||
let usedHeight = 40 + Theme.spacingS
|
||||
usedHeight += 52 + Theme.spacingS
|
||||
let usedHeight = 40 + Theme.spacingS;
|
||||
usedHeight += 52 + Theme.spacingS;
|
||||
if (root.showTargetData) {
|
||||
usedHeight += 36 + Theme.spacingS
|
||||
usedHeight += 36 + Theme.spacingS;
|
||||
}
|
||||
return parent.height - usedHeight
|
||||
return parent.height - usedHeight;
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
@@ -320,14 +321,14 @@ DankModal {
|
||||
property int itemSpacing: Theme.spacingS
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count) return
|
||||
|
||||
const itemY = index * (itemHeight + itemSpacing)
|
||||
const itemBottom = itemY + itemHeight
|
||||
if (index < 0 || index >= count)
|
||||
return;
|
||||
const itemY = index * (itemHeight + itemSpacing);
|
||||
const itemBottom = itemY + itemHeight;
|
||||
if (itemY < contentY) {
|
||||
contentY = itemY
|
||||
contentY = itemY;
|
||||
} else if (itemBottom > contentY + height) {
|
||||
contentY = itemBottom - height
|
||||
contentY = itemBottom - height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,9 +344,9 @@ DankModal {
|
||||
spacing: itemSpacing
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
root.selectedIndex = currentIndex
|
||||
root.selectedIndex = currentIndex;
|
||||
if (root.keyboardNavigationActive) {
|
||||
ensureVisible(currentIndex)
|
||||
ensureVisible(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,11 +361,11 @@ DankModal {
|
||||
hoverUpdatesSelection: true
|
||||
|
||||
onItemClicked: (idx, modelData) => {
|
||||
launchApplication(modelData)
|
||||
launchApplication(modelData);
|
||||
}
|
||||
|
||||
onKeyboardNavigationReset: {
|
||||
root.keyboardNavigationActive = false
|
||||
root.keyboardNavigationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,14 +374,14 @@ DankModal {
|
||||
id: appGrid
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count) return
|
||||
|
||||
const itemY = Math.floor(index / root.gridColumns) * cellHeight
|
||||
const itemBottom = itemY + cellHeight
|
||||
if (index < 0 || index >= count)
|
||||
return;
|
||||
const itemY = Math.floor(index / root.gridColumns) * cellHeight;
|
||||
const itemBottom = itemY + cellHeight;
|
||||
if (itemY < contentY) {
|
||||
contentY = itemY
|
||||
contentY = itemY;
|
||||
} else if (itemBottom > contentY + height) {
|
||||
contentY = itemBottom - height
|
||||
contentY = itemBottom - height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,9 +398,9 @@ DankModal {
|
||||
currentIndex: root.selectedIndex
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
root.selectedIndex = currentIndex
|
||||
root.selectedIndex = currentIndex;
|
||||
if (root.keyboardNavigationActive) {
|
||||
ensureVisible(currentIndex)
|
||||
ensureVisible(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,11 +414,11 @@ DankModal {
|
||||
hoverUpdatesSelection: true
|
||||
|
||||
onItemClicked: (idx, modelData) => {
|
||||
launchApplication(modelData)
|
||||
launchApplication(modelData);
|
||||
}
|
||||
|
||||
onKeyboardNavigationReset: {
|
||||
root.keyboardNavigationActive = false
|
||||
root.keyboardNavigationActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,22 +450,22 @@ DankModal {
|
||||
}
|
||||
|
||||
function launchApplication(app) {
|
||||
if (!app) return
|
||||
|
||||
root.applicationSelected(app, root.targetData)
|
||||
if (!app)
|
||||
return;
|
||||
root.applicationSelected(app, root.targetData);
|
||||
|
||||
if (usageHistoryKey && app.appId) {
|
||||
const usageHistory = SettingsData[usageHistoryKey] || {}
|
||||
const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0
|
||||
const usageHistory = SettingsData[usageHistoryKey] || {};
|
||||
const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0;
|
||||
usageHistory[app.appId] = {
|
||||
count: currentCount + 1,
|
||||
lastUsed: Date.now(),
|
||||
name: app.name
|
||||
}
|
||||
SettingsData.set(usageHistoryKey, usageHistory)
|
||||
};
|
||||
SettingsData.set(usageHistoryKey, usageHistory);
|
||||
}
|
||||
|
||||
root.close()
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("BluetoothPairingModal")
|
||||
|
||||
layerNamespace: "dms:bluetooth-pairing"
|
||||
|
||||
@@ -24,7 +25,7 @@ DankModal {
|
||||
property string passkeyInput: ""
|
||||
|
||||
function show(pairingData) {
|
||||
console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
|
||||
log.debug("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
|
||||
token = pairingData.token || "";
|
||||
deviceName = pairingData.deviceName || "";
|
||||
deviceAddress = pairingData.deviceAddr || "";
|
||||
@@ -33,7 +34,7 @@ DankModal {
|
||||
pinInput = "";
|
||||
passkeyInput = "";
|
||||
|
||||
console.log("BluetoothPairingModal: Calling open()");
|
||||
log.debug("Calling open()");
|
||||
open();
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item) {
|
||||
|
||||
@@ -2,9 +2,11 @@ import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Services
|
||||
|
||||
AppPickerModal {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("BrowserPickerModal")
|
||||
|
||||
property string url: ""
|
||||
|
||||
@@ -17,35 +19,44 @@ AppPickerModal {
|
||||
showTargetData: true
|
||||
|
||||
function shellEscape(str) {
|
||||
return "'" + str.replace(/'/g, "'\\''") + "'"
|
||||
return "'" + str.replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
onApplicationSelected: (app, url) => {
|
||||
if (!app) return
|
||||
if (!app)
|
||||
return;
|
||||
let cmd = app.exec || "";
|
||||
const escapedUrl = shellEscape(url);
|
||||
|
||||
let cmd = app.exec || ""
|
||||
const escapedUrl = shellEscape(url)
|
||||
|
||||
let hasField = false
|
||||
if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUrl); hasField = true }
|
||||
else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUrl); hasField = true }
|
||||
else if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedUrl); hasField = true }
|
||||
else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedUrl); hasField = true }
|
||||
|
||||
cmd = cmd.replace(/%[ikc]/g, "")
|
||||
|
||||
if (!hasField) {
|
||||
cmd += " " + escapedUrl
|
||||
let hasField = false;
|
||||
if (cmd.includes("%u")) {
|
||||
cmd = cmd.replace("%u", escapedUrl);
|
||||
hasField = true;
|
||||
} else if (cmd.includes("%U")) {
|
||||
cmd = cmd.replace("%U", escapedUrl);
|
||||
hasField = true;
|
||||
} else if (cmd.includes("%f")) {
|
||||
cmd = cmd.replace("%f", escapedUrl);
|
||||
hasField = true;
|
||||
} else if (cmd.includes("%F")) {
|
||||
cmd = cmd.replace("%F", escapedUrl);
|
||||
hasField = true;
|
||||
}
|
||||
|
||||
console.log("BrowserPicker: Launching", cmd)
|
||||
cmd = cmd.replace(/%[ikc]/g, "");
|
||||
|
||||
if (!hasField) {
|
||||
cmd += " " + escapedUrl;
|
||||
}
|
||||
|
||||
log.debug("BrowserPicker: Launching", cmd);
|
||||
|
||||
Quickshell.execDetached({
|
||||
command: ["sh", "-c", cmd]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onViewModeChanged: {
|
||||
SettingsData.set("browserPickerViewMode", viewMode)
|
||||
SettingsData.set("browserPickerViewMode", viewMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: thumbnail
|
||||
readonly property var log: Log.scoped("ClipboardThumbnail")
|
||||
|
||||
required property var entry
|
||||
required property string entryType
|
||||
@@ -52,7 +53,7 @@ Item {
|
||||
modal.activeImageLoads--;
|
||||
}
|
||||
if (response.error) {
|
||||
console.warn("ClipboardThumbnail: Failed to load image:", entry.id);
|
||||
log.warn("Failed to load image:", entry.id);
|
||||
return;
|
||||
}
|
||||
const data = response.result?.data;
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankModal")
|
||||
|
||||
property string layerNamespace: "dms:modal"
|
||||
property alias content: contentLoader.sourceComponent
|
||||
@@ -246,10 +247,10 @@ Item {
|
||||
return WlrLayershell.Overlay;
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
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;
|
||||
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;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
|
||||
@@ -9,6 +9,7 @@ import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankColorPickerModal")
|
||||
|
||||
layerNamespace: "dms:color-picker"
|
||||
|
||||
@@ -50,17 +51,17 @@ DankModal {
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleInstant() {
|
||||
if (shouldBeVisible) {
|
||||
hideInstant();
|
||||
hideInstant();
|
||||
} else {
|
||||
show();
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ DankModal {
|
||||
hideInstant();
|
||||
Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("dms color pick exited with code:", exitCode);
|
||||
log.warn("dms color pick exited with code:", exitCode);
|
||||
root.show();
|
||||
return;
|
||||
}
|
||||
@@ -120,11 +121,11 @@ DankModal {
|
||||
if (result.hex) {
|
||||
applyPickedColor(result.hex);
|
||||
} 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();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse dms color pick JSON:", e);
|
||||
log.warn("Failed to parse dms color pick JSON:", e);
|
||||
root.show();
|
||||
}
|
||||
}, 0, Proc.noTimeout);
|
||||
@@ -142,39 +143,39 @@ DankModal {
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
root.show();
|
||||
return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
|
||||
}
|
||||
function open(): string {
|
||||
root.show();
|
||||
return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function openColor(color: string): string {
|
||||
root.selectedColor = Qt.color(color);
|
||||
root.currentColor = Qt.color(color);
|
||||
root.updateFromColor(Qt.color(color));
|
||||
return open();
|
||||
}
|
||||
function openColor(color: string): string {
|
||||
root.selectedColor = Qt.color(color);
|
||||
root.currentColor = Qt.color(color);
|
||||
root.updateFromColor(Qt.color(color));
|
||||
return open();
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
root.hide();
|
||||
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
|
||||
}
|
||||
function close(): string {
|
||||
root.hide();
|
||||
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function closeInstant(): string {
|
||||
root.hideInstant();
|
||||
return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS";
|
||||
}
|
||||
function closeInstant(): string {
|
||||
root.hideInstant();
|
||||
return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
root.toggle();
|
||||
return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS";
|
||||
}
|
||||
function toggle(): string {
|
||||
root.toggle();
|
||||
return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggleInstant(): string {
|
||||
root.toggleInstant();
|
||||
return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS";
|
||||
}
|
||||
function toggleInstant(): string {
|
||||
root.toggleInstant();
|
||||
return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS";
|
||||
}
|
||||
|
||||
target: "color-picker"
|
||||
target: "color-picker"
|
||||
}
|
||||
|
||||
content: Component {
|
||||
|
||||
@@ -1881,7 +1881,7 @@ Item {
|
||||
function openTerminal(path) {
|
||||
if (!path)
|
||||
return;
|
||||
var terminal = Quickshell.env("TERMINAL") || "xterm";
|
||||
var terminal = SessionData.resolveTerminal() || "xterm";
|
||||
Quickshell.execDetached({
|
||||
command: [terminal],
|
||||
workingDirectory: path
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DankLauncherV2Modal")
|
||||
|
||||
visible: false
|
||||
|
||||
@@ -323,10 +324,10 @@ Item {
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
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;
|
||||
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;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("GreeterDoctorPage")
|
||||
|
||||
property bool isRunning: false
|
||||
property bool hasRun: false
|
||||
@@ -228,9 +230,7 @@ Item {
|
||||
text: {
|
||||
if (root.errorCount === 0)
|
||||
return I18n.tr("All checks passed", "greeter doctor page success");
|
||||
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);
|
||||
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);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
||||
@@ -412,7 +412,7 @@ Item {
|
||||
else
|
||||
root.selectedFilter = "ok";
|
||||
} catch (e) {
|
||||
console.error("GreeterDoctorPage: Failed to parse doctor output:", e);
|
||||
log.error("Failed to parse doctor output:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
|
||||
FloatingWindow {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("GreeterModal")
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property int currentPage: 0
|
||||
@@ -105,7 +106,7 @@ FloatingWindow {
|
||||
root.cheatsheetData = JSON.parse(trimmed);
|
||||
root.cheatsheetLoaded = true;
|
||||
} catch (e) {
|
||||
console.warn("Greeter: Failed to parse cheatsheet:", e);
|
||||
log.warn("Greeter: Failed to parse cheatsheet:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import qs.Widgets
|
||||
|
||||
FloatingWindow {
|
||||
id: processListModal
|
||||
readonly property var log: Log.scoped("ProcessListModal")
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property int currentTab: 0
|
||||
@@ -22,7 +23,7 @@ FloatingWindow {
|
||||
|
||||
function show() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
log.warn("dgop is not available");
|
||||
return;
|
||||
}
|
||||
visible = true;
|
||||
@@ -36,7 +37,7 @@ FloatingWindow {
|
||||
|
||||
function toggle() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
log.warn("dgop is not available");
|
||||
return;
|
||||
}
|
||||
visible = !visible;
|
||||
@@ -44,7 +45,7 @@ FloatingWindow {
|
||||
|
||||
function focusOrToggle() {
|
||||
if (!DgopService.dgopAvailable) {
|
||||
console.warn("ProcessListModal: dgop is not available");
|
||||
log.warn("dgop is not available");
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
|
||||
@@ -164,7 +164,8 @@ Rectangle {
|
||||
"id": "updater",
|
||||
"text": I18n.tr("System Updater"),
|
||||
"icon": "refresh",
|
||||
"tabIndex": 20
|
||||
"tabIndex": 20,
|
||||
"updaterOnly": true
|
||||
},
|
||||
{
|
||||
"id": "desktop_widgets",
|
||||
@@ -340,6 +341,8 @@ Rectangle {
|
||||
return false;
|
||||
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
|
||||
return false;
|
||||
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
|
||||
FloatingWindow {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("WorkspaceRenameModal")
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
@@ -39,7 +40,7 @@ FloatingWindow {
|
||||
} else if (CompositorService.isHyprland) {
|
||||
HyprlandService.renameWorkspace(name);
|
||||
} else {
|
||||
console.warn("WorkspaceRenameModal: rename not supported for this compositor");
|
||||
log.warn("rename not supported for this compositor");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../utils/layout.js" as LayoutUtils
|
||||
|
||||
Column {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DragDropGrid")
|
||||
|
||||
property bool editMode: false
|
||||
property string expandedSection: ""
|
||||
@@ -988,7 +989,7 @@ Column {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
|
||||
log.warn("stale plugin component for", pluginId, "- reloading");
|
||||
PluginService.reloadPlugin(pluginId);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -6,6 +6,7 @@ import "../utils/widgets.js" as WidgetUtils
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("WidgetModel")
|
||||
|
||||
property var vpnBuiltinInstance: null
|
||||
property var cupsBuiltinInstance: null
|
||||
@@ -26,7 +27,7 @@ QtObject {
|
||||
const widgets = SettingsData.controlCenterWidgets || [];
|
||||
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +56,7 @@ QtObject {
|
||||
const widgets = SettingsData.controlCenterWidgets || [];
|
||||
const hasCupsWidget = widgets.some(w => w.id === "builtin_cups");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Services
|
||||
|
||||
CompoundPill {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("ColorPickerPill")
|
||||
|
||||
property var colorPickerModal: null
|
||||
|
||||
@@ -14,14 +16,14 @@ CompoundPill {
|
||||
secondaryText: I18n.tr("Choose a color")
|
||||
|
||||
onToggled: {
|
||||
console.log("ColorPickerPill toggled, modal:", colorPickerModal);
|
||||
log.debug("ColorPickerPill toggled, modal:", colorPickerModal);
|
||||
if (colorPickerModal) {
|
||||
colorPickerModal.show();
|
||||
}
|
||||
}
|
||||
|
||||
onExpandClicked: {
|
||||
console.log("ColorPickerPill expandClicked, modal:", colorPickerModal);
|
||||
log.debug("ColorPickerPill expandClicked, modal:", colorPickerModal);
|
||||
if (colorPickerModal) {
|
||||
colorPickerModal.show();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: barWindow
|
||||
readonly property var log: Log.scoped("DankBarWindow")
|
||||
|
||||
required property var rootWindow
|
||||
required property var barConfig
|
||||
@@ -164,7 +165,7 @@ PanelWindow {
|
||||
barWindow.BackgroundEffect.blurRegion = region;
|
||||
barWindow.blurRegion = region;
|
||||
} 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 {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
console.info("DankBar: Plugin loaded:", pluginId);
|
||||
log.info("DankBar: Plugin loaded:", pluginId);
|
||||
SettingsData.widgetDataChanged();
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
console.info("DankBar: Plugin unloaded:", pluginId);
|
||||
log.info("DankBar: Plugin unloaded:", pluginId);
|
||||
SettingsData.widgetDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
461
quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml
Normal file
461
quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("AppsDock")
|
||||
|
||||
enableBackgroundHover: false
|
||||
enableCursor: false
|
||||
@@ -550,9 +551,9 @@ BasePill {
|
||||
showBadge: root.showOverflowBadge
|
||||
z: 10
|
||||
onClicked: {
|
||||
console.log("Overflow button clicked! Current state:", root.overflowExpanded);
|
||||
log.debug("Overflow button clicked! Current state:", root.overflowExpanded);
|
||||
root.overflowExpanded = !root.overflowExpanded;
|
||||
console.log("New state:", root.overflowExpanded);
|
||||
log.debug("New state:", root.overflowExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
id: battery
|
||||
readonly property var log: Log.scoped("Battery")
|
||||
|
||||
property bool batteryPopupVisible: false
|
||||
property var popoutTarget: null
|
||||
@@ -130,13 +131,13 @@ BasePill {
|
||||
// Check if this is a touchpad
|
||||
if (delta !== 120 && delta !== -120) {
|
||||
touchpadAccumulator += delta;
|
||||
console.info("Acc: "+touchpadAccumulator);
|
||||
log.info("Acc: " + touchpadAccumulator);
|
||||
if (Math.abs(touchpadAccumulator) < 500)
|
||||
return;
|
||||
delta = touchpadAccumulator;
|
||||
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
|
||||
if (typeof PowerProfiles === "undefined") {
|
||||
@@ -149,11 +150,14 @@ BasePill {
|
||||
var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
|
||||
|
||||
// Step once based on mouse wheel direction
|
||||
if (delta > 0) index += 1;
|
||||
else index -= 1;
|
||||
if (delta > 0)
|
||||
index += 1;
|
||||
else
|
||||
index -= 1;
|
||||
|
||||
// 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
|
||||
PowerProfiles.profile = profiles[index];
|
||||
|
||||
@@ -10,6 +10,36 @@ BasePill {
|
||||
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
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: {
|
||||
if (!activePlayer?.identity)
|
||||
return false;
|
||||
@@ -212,15 +242,15 @@ BasePill {
|
||||
height: 24
|
||||
radius: 12
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
|
||||
color: root._isPlaying ? Theme.primary : Theme.primaryHover
|
||||
visible: root.playerAvailable
|
||||
opacity: activePlayer ? 1 : 0.3
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
||||
name: root._isPlaying ? "pause" : "play_arrow"
|
||||
size: 14
|
||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
||||
color: root._isPlaying ? Theme.background : Theme.primary
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
property string displayText: {
|
||||
if (!activePlayer || !activePlayer.trackTitle) {
|
||||
if (!activePlayer || !root._stableTitle)
|
||||
return "";
|
||||
}
|
||||
|
||||
const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track");
|
||||
const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || "");
|
||||
const title = isWebMedia ? root._stableTitle : (root._stableTitle || "Unknown Track");
|
||||
const subtitle = isWebMedia ? (root._stableArtist || cachedIdentity) : (root._stableArtist || "");
|
||||
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
||||
}
|
||||
|
||||
@@ -444,15 +472,15 @@ BasePill {
|
||||
height: 24
|
||||
radius: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
|
||||
color: root._isPlaying ? Theme.primary : Theme.primaryHover
|
||||
visible: root.playerAvailable
|
||||
opacity: activePlayer ? 1 : 0.3
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
||||
name: root._isPlaying ? "pause" : "play_arrow"
|
||||
size: 14
|
||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
||||
color: root._isPlaying ? Theme.background : Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -7,41 +7,33 @@ import qs.Widgets
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
property var widgetData: null
|
||||
property bool isActive: false
|
||||
|
||||
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
|
||||
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
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "hidden_horizontal"
|
||||
when: root.shouldHide && !isVerticalOrientation
|
||||
PropertyChanges {
|
||||
target: root
|
||||
width: 0
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hidden_vertical"
|
||||
when: root.shouldHide && isVerticalOrientation
|
||||
PropertyChanges {
|
||||
target: root
|
||||
height: 0
|
||||
}
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
NumberAnimation {
|
||||
properties: "width,height"
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
|
||||
@@ -6,6 +6,7 @@ import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("CalendarOverviewCard")
|
||||
|
||||
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
|
||||
|
||||
@@ -521,7 +522,7 @@ Rectangle {
|
||||
onClicked: {
|
||||
if (modelData.url && modelData.url !== "") {
|
||||
if (Qt.openUrlExternally(modelData.url) === false) {
|
||||
console.warn("Failed to open URL: " + modelData.url);
|
||||
log.warn("Failed to open URL: " + modelData.url);
|
||||
} else {
|
||||
root.closeDash();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("WeatherTab")
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
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()));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Weather Date Sync Error:", e);
|
||||
log.warn("Weather Date Sync Error:", e);
|
||||
}
|
||||
|
||||
syncing = false;
|
||||
|
||||
@@ -5,9 +5,11 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "GreetdEnv.js" as GreetdEnv
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
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 sessionConfigPath: greetCfgDir + "/session.json"
|
||||
@@ -42,7 +44,7 @@ Singleton {
|
||||
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false;
|
||||
}
|
||||
} 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)
|
||||
saveMemory();
|
||||
} 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());
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "GreetdEnv.js" as GreetdEnv
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("GreetdSettings")
|
||||
|
||||
readonly property string configPath: {
|
||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||
@@ -81,8 +83,7 @@ Singleton {
|
||||
|
||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||
registryThemeVariants = settings.registryThemeVariants !== undefined ?
|
||||
settings.registryThemeVariants : ({});
|
||||
registryThemeVariants = settings.registryThemeVariants !== undefined ? settings.registryThemeVariants : ({});
|
||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||
@@ -142,7 +143,7 @@ Singleton {
|
||||
Theme.applyGreeterTheme(currentThemeName);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse greetd settings:", e);
|
||||
log.warn("Failed to parse greetd settings:", e);
|
||||
} finally {
|
||||
settingsLoaded = true;
|
||||
}
|
||||
@@ -192,7 +193,7 @@ Singleton {
|
||||
parseSettings(settingsFile.text());
|
||||
}
|
||||
onLoadFailed: error => {
|
||||
console.warn("Failed to load greetd settings:", error);
|
||||
log.warn("Failed to load greetd settings:", error);
|
||||
root.parseSettings("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: keyboard_controller
|
||||
readonly property var log: Log.scoped("KeyboardController")
|
||||
|
||||
// reference on the TextInput
|
||||
property Item target
|
||||
@@ -14,21 +16,20 @@ Item {
|
||||
|
||||
function show() {
|
||||
if (!isKeyboardActive && keyboard === null) {
|
||||
keyboard = keyboardComponent.createObject(
|
||||
keyboard_controller.rootObject)
|
||||
keyboard.target = keyboard_controller.target
|
||||
keyboard.dismissed.connect(hide)
|
||||
isKeyboardActive = true
|
||||
keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
|
||||
keyboard.target = keyboard_controller.target;
|
||||
keyboard.dismissed.connect(hide);
|
||||
isKeyboardActive = true;
|
||||
} else
|
||||
console.log("The keyboard is already shown")
|
||||
log.debug("The keyboard is already shown");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (isKeyboardActive && keyboard !== null) {
|
||||
keyboard.destroy()
|
||||
isKeyboardActive = false
|
||||
keyboard.destroy();
|
||||
isKeyboardActive = false;
|
||||
} else
|
||||
console.log("The keyboard is already hidden")
|
||||
log.debug("The keyboard is already hidden");
|
||||
}
|
||||
|
||||
// private
|
||||
|
||||
@@ -14,6 +14,7 @@ import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("LockScreenContent")
|
||||
|
||||
function encodeFileUrl(path) {
|
||||
if (!path)
|
||||
@@ -95,9 +96,9 @@ Item {
|
||||
if (SessionService.loginctlAvailable && DMSService.apiVersion >= 2) {
|
||||
DMSService.sendRequest("loginctl.lockerReady", null, resp => {
|
||||
if (resp?.error)
|
||||
console.warn("lockerReady failed:", resp.error);
|
||||
log.warn("lockerReady failed:", resp.error);
|
||||
else
|
||||
console.log("lockerReady sent (afterAnimating/afterRendering)");
|
||||
log.debug("lockerReady sent (afterAnimating/afterRendering)");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -803,7 +804,7 @@ Item {
|
||||
}
|
||||
|
||||
if (pam.passwd.active) {
|
||||
console.log("PAM is active, ignoring input");
|
||||
log.debug("PAM is active, ignoring input");
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
@@ -1622,7 +1623,7 @@ Item {
|
||||
buttonSize: 40
|
||||
onClicked: {
|
||||
if (demoMode) {
|
||||
console.log("Demo: Power Menu");
|
||||
log.debug("Demo: Power Menu");
|
||||
} else {
|
||||
powerMenu.show();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("LockScreenDemo")
|
||||
|
||||
property bool demoActive: false
|
||||
|
||||
@@ -25,12 +27,12 @@ PanelWindow {
|
||||
color: "transparent"
|
||||
|
||||
function showDemo(): void {
|
||||
console.log("Showing lock screen demo");
|
||||
log.debug("Showing lock screen demo");
|
||||
demoActive = true;
|
||||
}
|
||||
|
||||
function hideDemo(): void {
|
||||
console.log("Hiding lock screen demo");
|
||||
log.debug("Hiding lock screen demo");
|
||||
demoActive = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("VideoScreensaver")
|
||||
|
||||
required property string screenName
|
||||
property bool active: false
|
||||
@@ -53,7 +54,7 @@ Item {
|
||||
|
||||
onExited: exitCode => {
|
||||
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"));
|
||||
root.dismiss();
|
||||
}
|
||||
@@ -98,14 +99,14 @@ Item {
|
||||
`, background, "VideoScreensaver.VideoPlayer");
|
||||
|
||||
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);
|
||||
root.dismiss();
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn("VideoScreensaver: Failed to create video player:", e);
|
||||
log.warn("Failed to create video player:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ DankOSD {
|
||||
}
|
||||
|
||||
property bool _pendingShow: false
|
||||
property string _displayTitle: ""
|
||||
property string _displayArtist: ""
|
||||
property string _displayAlbum: ""
|
||||
|
||||
Timer {
|
||||
id: iconDebounce
|
||||
@@ -105,6 +108,12 @@ DankOSD {
|
||||
return;
|
||||
if (!SettingsData.osdMediaPlaybackEnabled)
|
||||
return;
|
||||
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||
return;
|
||||
|
||||
root._displayTitle = player.trackTitle || "";
|
||||
root._displayArtist = player.trackArtist || "";
|
||||
root._displayAlbum = player.trackAlbum || "";
|
||||
|
||||
root.updatePlaybackIcon();
|
||||
TrackArtService.loadArtwork(player.trackArtUrl);
|
||||
@@ -254,7 +263,7 @@ DankOSD {
|
||||
StyledText {
|
||||
id: topText
|
||||
width: parent.width
|
||||
text: player ? `${player.trackTitle || I18n.tr("Unknown Title")}` : ""
|
||||
text: player ? (root._displayTitle || I18n.tr("Unknown Title")) : ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
@@ -265,7 +274,7 @@ DankOSD {
|
||||
StyledText {
|
||||
id: bottomText
|
||||
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.weight: Font.Light
|
||||
color: Theme.surfaceText
|
||||
|
||||
@@ -407,6 +407,8 @@ Item {
|
||||
item.widgetWidth = Qt.binding(() => contentLoader.width);
|
||||
if (item.widgetHeight !== undefined)
|
||||
item.widgetHeight = Qt.binding(() => contentLoader.height);
|
||||
if (item.screen !== undefined)
|
||||
item.screen = Qt.binding(() => root.screen);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("PluginSettings")
|
||||
|
||||
required property string pluginId
|
||||
property var pluginService: null
|
||||
@@ -131,7 +133,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
if (!hasPermission) {
|
||||
console.warn("PluginSettings: Plugin", pluginId, "does not have settings_write permission");
|
||||
log.warn("Plugin", pluginId, "does not have settings_write permission");
|
||||
return;
|
||||
}
|
||||
if (pluginService.savePluginData) {
|
||||
|
||||
@@ -301,10 +301,10 @@ Item {
|
||||
clip: true
|
||||
spacing: 2
|
||||
|
||||
add: root.searchText.length > 0 ? ListViewTransitions.add : null
|
||||
remove: root.searchText.length > 0 ? ListViewTransitions.remove : null
|
||||
displaced: root.searchText.length > 0 ? ListViewTransitions.displaced : null
|
||||
move: root.searchText.length > 0 ? ListViewTransitions.move : null
|
||||
add: null
|
||||
remove: null
|
||||
displaced: null
|
||||
move: null
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.cachedProcesses
|
||||
|
||||
@@ -189,10 +189,10 @@ Item {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!SystemUpdateService.shellVersion && !DMSService.cliVersion)
|
||||
if (!ShellVersionService.shellVersion && !DMSService.cliVersion)
|
||||
return "dms";
|
||||
|
||||
let version = SystemUpdateService.shellVersion || "";
|
||||
let version = ShellVersionService.shellVersion || "";
|
||||
let cliVersion = DMSService.cliVersion || "";
|
||||
|
||||
// Debian/Ubuntu/OpenSUSE git format: 1.0.3+git2264.c5c5ce84
|
||||
@@ -218,7 +218,7 @@ Item {
|
||||
|
||||
let baseVersion = extractBaseVersion(cliVersion);
|
||||
if (!baseVersion)
|
||||
baseVersion = extractBaseVersion(SystemUpdateService.semverVersion);
|
||||
baseVersion = extractBaseVersion(ShellVersionService.semverVersion);
|
||||
if (baseVersion) {
|
||||
return `dms (git) v${baseVersion}-${match[1]}`;
|
||||
}
|
||||
@@ -253,8 +253,8 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: SystemUpdateService.shellCodename.length > 0
|
||||
text: `"${SystemUpdateService.shellCodename}"`
|
||||
visible: ShellVersionService.shellCodename.length > 0
|
||||
text: `"${ShellVersionService.shellCodename}"`
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.italic: true
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -9,6 +9,7 @@ import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("DisplayConfigState")
|
||||
|
||||
readonly property bool hasOutputBackend: WlrOutputService.wlrOutputAvailable
|
||||
readonly property var wlrOutputs: WlrOutputService.outputs
|
||||
@@ -106,7 +107,7 @@ Singleton {
|
||||
function findMatchingProfile() {
|
||||
const profiles = validatedProfiles;
|
||||
|
||||
console.log("[Profile Match] Current outputs:", JSON.stringify(currentOutputSet));
|
||||
log.debug("[Profile Match] Current outputs:", JSON.stringify(currentOutputSet));
|
||||
|
||||
let bestMatch = "";
|
||||
let bestScore = -1;
|
||||
@@ -116,25 +117,25 @@ Singleton {
|
||||
const profile = profiles[profileId];
|
||||
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;
|
||||
for (const output of currentOutputSet) {
|
||||
if (!profileSet.has(output)) {
|
||||
console.log("[Profile Match] - Missing output:", output);
|
||||
log.debug("[Profile Match] - Missing output:", output);
|
||||
allCurrentPresent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allCurrentPresent) {
|
||||
console.log("[Profile Match] - SKIP: not all current outputs present");
|
||||
log.debug("[Profile Match] - SKIP: not all current outputs present");
|
||||
continue;
|
||||
}
|
||||
|
||||
const disconnectedCount = profile.outputSet.length - currentOutputSet.length;
|
||||
const score = currentOutputSet.length * 100 - disconnectedCount;
|
||||
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)) {
|
||||
bestScore = score;
|
||||
@@ -142,7 +143,7 @@ Singleton {
|
||||
bestUpdatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
console.log("[Profile Match] Best match:", bestMatch, "score:", bestScore);
|
||||
log.debug("[Profile Match] Best match:", bestMatch, "score:", bestScore);
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
|
||||
@@ -325,6 +325,8 @@ Item {
|
||||
placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')")
|
||||
onTextEdited: SettingsData.set("launchPrefix", text)
|
||||
}
|
||||
|
||||
TerminalPickerRow {}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -1697,8 +1697,11 @@ Item {
|
||||
required property int index
|
||||
|
||||
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 var configData: isExpanded ? VPNService.editConfig : null
|
||||
readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null
|
||||
|
||||
width: parent.width
|
||||
height: isExpanded ? 56 + vpnExpandedContent.height : 56
|
||||
@@ -1745,7 +1748,7 @@ Item {
|
||||
Column {
|
||||
spacing: 2
|
||||
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 {
|
||||
text: modelData.name
|
||||
@@ -1775,6 +1778,7 @@ Item {
|
||||
radius: 14
|
||||
color: vpnExpandBtn.containsMouse ? Theme.surfacePressed : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: canExpand
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -1805,6 +1809,7 @@ Item {
|
||||
radius: 14
|
||||
color: vpnDeleteBtn.containsMouse ? Theme.errorHover : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: canDelete
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -1835,7 +1840,7 @@ Item {
|
||||
id: vpnExpandedContent
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: isExpanded
|
||||
visible: !isTransient && isExpanded
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
|
||||
@@ -38,7 +38,7 @@ StyledRect {
|
||||
property bool meetsRequirements: requiresDms ? PluginService.checkPluginCompatibility(requiresDms) : true
|
||||
|
||||
Connections {
|
||||
target: SystemUpdateService
|
||||
target: ShellVersionService
|
||||
function onSemverVersionChanged() {
|
||||
root.meetsRequirementsChanged();
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ FocusScope {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SystemUpdateService
|
||||
target: ShellVersionService
|
||||
function onSemverVersionChanged() {
|
||||
incompatWarning.refresh();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
@@ -25,18 +67,60 @@ Item {
|
||||
title: I18n.tr("System Updater")
|
||||
settingKey: "systemUpdater"
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Hide Updater Widget", "When updater widget is used, then hide it if no update found")
|
||||
description: I18n.tr("When updater widget is used, then hide it if no update found")
|
||||
checked: SettingsData.updaterHideWidget
|
||||
onToggled: checked => {
|
||||
SettingsData.set("updaterHideWidget", checked);
|
||||
StyledText {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
visible: SystemUpdateService.backends.length > 0
|
||||
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 {
|
||||
text: I18n.tr("Use Custom Command")
|
||||
description: I18n.tr("Use custom command for update your system")
|
||||
text: I18n.tr("Include Flatpak updates")
|
||||
description: I18n.tr("Apply Flatpak updates alongside system updates when running 'Update All'.")
|
||||
visible: (SystemUpdateService.backends || []).some(b => b.repo === "flatpak")
|
||||
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
|
||||
onToggled: 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 {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: customCommandColumn.implicitHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
visible: SettingsData.updaterUseCustomCommand
|
||||
|
||||
Column {
|
||||
id: customCommandColumn
|
||||
@@ -61,7 +166,7 @@ Item {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System update custom command")
|
||||
text: I18n.tr("Custom update command")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
@@ -69,7 +174,7 @@ Item {
|
||||
DankTextField {
|
||||
id: updaterCustomCommand
|
||||
width: parent.width
|
||||
placeholderText: "myPkgMngr --sysupdate"
|
||||
placeholderText: "topgrade --no-retry"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
@@ -98,6 +203,7 @@ Item {
|
||||
height: terminalParamsColumn.implicitHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
visible: SettingsData.updaterUseCustomCommand
|
||||
|
||||
Column {
|
||||
id: terminalParamsColumn
|
||||
@@ -105,7 +211,7 @@ Item {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Terminal custom additional parameters")
|
||||
text: I18n.tr("Terminal additional parameters")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
@@ -113,7 +219,7 @@ Item {
|
||||
DankTextField {
|
||||
id: updaterTerminalCustomClass
|
||||
width: parent.width
|
||||
placeholderText: "-T udpClass"
|
||||
placeholderText: "-T updater"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
@@ -2645,6 +2645,18 @@ Item {
|
||||
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 {
|
||||
tab: "theme"
|
||||
tags: ["matugen", "equibop", "discord", "template"]
|
||||
|
||||
31
quickshell/Modules/Settings/Widgets/TerminalPickerRow.qml
Normal file
31
quickshell/Modules/Settings/Widgets/TerminalPickerRow.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,8 @@ Item {
|
||||
"text": I18n.tr("System Update"),
|
||||
"description": I18n.tr("Check for system updates"),
|
||||
"icon": "update",
|
||||
"enabled": SystemUpdateService.distributionSupported
|
||||
"enabled": SystemUpdateService.sysupdateAvailable,
|
||||
"warning": SystemUpdateService.sysupdateAvailable ? undefined : I18n.tr("Requires DMS server with sysupdate capability")
|
||||
},
|
||||
{
|
||||
"id": "powerMenuButton",
|
||||
@@ -430,7 +431,7 @@ Item {
|
||||
"id": widget.id,
|
||||
"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++) {
|
||||
if (widget[keys[i]] !== undefined)
|
||||
result[keys[i]] = widget[keys[i]];
|
||||
@@ -579,6 +580,17 @@ Item {
|
||||
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) {
|
||||
var widgets = getWidgetsForSection(sectionId).slice();
|
||||
if (widgetIndex < 0 || widgetIndex >= widgets.length) {
|
||||
@@ -714,6 +726,8 @@ Item {
|
||||
item.barShowOverflowBadge = widget.barShowOverflowBadge;
|
||||
if (widget.trayUseInlineExpansion !== undefined)
|
||||
item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
|
||||
if (widget.hideWhenIdle !== undefined)
|
||||
item.hideWhenIdle = widget.hideWhenIdle;
|
||||
}
|
||||
widgets.push(item);
|
||||
});
|
||||
@@ -1003,6 +1017,9 @@ Item {
|
||||
onOverflowSettingChanged: (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) => {
|
||||
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) => {
|
||||
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
|
||||
}
|
||||
onHideWhenIdleChanged: (sectionId, widgetIndex, enabled) => {
|
||||
widgetsTab.handleHideWhenIdleChanged(sectionId, widgetIndex, enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import qs.Services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("WidgetsTabSection")
|
||||
|
||||
property var items: []
|
||||
property var allWidgets: []
|
||||
@@ -33,6 +34,7 @@ Column {
|
||||
signal showInGbChanged(string sectionId, int widgetIndex, bool enabled)
|
||||
signal diskUsageModeChanged(string sectionId, int widgetIndex, int mode)
|
||||
signal overflowSettingChanged(string sectionId, int widgetIndex, string settingName, var value)
|
||||
signal hideWhenIdleChanged(string sectionId, int widgetIndex, bool enabled)
|
||||
|
||||
function cloneWidgetData(widget) {
|
||||
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 {
|
||||
id: memMenuButton
|
||||
visible: modelData.id === "memUsage"
|
||||
@@ -1706,11 +1727,11 @@ Column {
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
onOpened: {
|
||||
console.log("Privacy context menu opened");
|
||||
log.debug("Privacy context menu opened");
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
console.log("Privacy Center context menu closed");
|
||||
log.debug("Privacy Center context menu closed");
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
Variants {
|
||||
readonly property var log: Log.scoped("WallpaperBackground")
|
||||
model: {
|
||||
if (SessionData.isGreeterMode) {
|
||||
return Quickshell.screens;
|
||||
@@ -103,7 +104,7 @@ Variants {
|
||||
function _recheckScreenScale() {
|
||||
const newScale = CompositorService.getScreenScale(modelData);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,20 @@ Scope {
|
||||
hideSpotlight();
|
||||
}
|
||||
|
||||
onIsClosingChanged: {
|
||||
if (!isClosing) {
|
||||
closeTimer.stop();
|
||||
return;
|
||||
}
|
||||
closeTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Theme.expressiveDurations.fast
|
||||
onTriggered: niriOverviewScope.resetState()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: niriOverlayLoader
|
||||
active: overlayActive || isClosing
|
||||
@@ -128,7 +142,7 @@ Scope {
|
||||
WindowBlur {
|
||||
targetWindow: overlayWindow
|
||||
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
|
||||
blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5
|
||||
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)
|
||||
|
||||
Behavior on scale {
|
||||
id: scaleAnimation
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
onRunningChanged: {
|
||||
if (running || !spotlightContainer.animatingOut)
|
||||
return;
|
||||
niriOverviewScope.resetState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("OverviewWidget")
|
||||
required property var panelWindow
|
||||
required property bool overviewOpen
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
|
||||
@@ -276,7 +277,7 @@ Item {
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error("OverviewWidget filter error:", e);
|
||||
log.error("OverviewWidget filter error:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("AppSearchService")
|
||||
|
||||
property var applications: []
|
||||
property var _cachedCategories: null
|
||||
@@ -811,7 +813,7 @@ Singleton {
|
||||
});
|
||||
isPersistent = false;
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error creating temporary plugin instance", pluginId, ":", e);
|
||||
log.warn("Error creating temporary plugin instance", pluginId, ":", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -831,7 +833,7 @@ Singleton {
|
||||
instance.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e);
|
||||
log.warn("Error getting items from plugin", pluginId, ":", e);
|
||||
if (!isPersistent)
|
||||
instance.destroy();
|
||||
}
|
||||
@@ -857,7 +859,7 @@ Singleton {
|
||||
});
|
||||
isPersistent = false;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -877,7 +879,7 @@ Singleton {
|
||||
instance.destroy();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e);
|
||||
log.warn("Error executing item from plugin", pluginId, ":", e);
|
||||
if (!isPersistent)
|
||||
instance.destroy();
|
||||
}
|
||||
@@ -949,7 +951,7 @@ Singleton {
|
||||
try {
|
||||
return instance.getCategories() || [];
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error getting categories from plugin", pluginId, ":", e);
|
||||
log.warn("Error getting categories from plugin", pluginId, ":", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -968,7 +970,7 @@ Singleton {
|
||||
try {
|
||||
instance.setCategory(categoryId);
|
||||
} catch (e) {
|
||||
console.warn("AppSearchService: Error setting category on plugin", pluginId, ":", e);
|
||||
log.warn("Error setting category on plugin", pluginId, ":", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("AudioService")
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
@@ -143,7 +144,7 @@ Singleton {
|
||||
|
||||
function setDeviceAlias(nodeName, customAlias) {
|
||||
if (!nodeName) {
|
||||
console.error("AudioService: Cannot set alias - nodeName is empty");
|
||||
log.error("Cannot set alias - nodeName is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -189,8 +190,8 @@ EOFCONFIG
|
||||
|
||||
Proc.runCommand("writeWireplumberConfig", ["sh", "-c", shellCmd], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.error("AudioService: Failed to write WirePlumber config. Exit code:", exitCode);
|
||||
console.error("AudioService: Error output:", output);
|
||||
log.error("Failed to write WirePlumber config. Exit code:", exitCode);
|
||||
log.error("Error output:", output);
|
||||
ToastService.showError(I18n.tr("Failed to save audio config"), output || "");
|
||||
return;
|
||||
}
|
||||
@@ -305,7 +306,7 @@ EOFCONFIG
|
||||
ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated"));
|
||||
wireplumberReloadCompleted(true);
|
||||
} 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);
|
||||
wireplumberReloadCompleted(false);
|
||||
}
|
||||
@@ -317,7 +318,7 @@ EOFCONFIG
|
||||
|
||||
Proc.runCommand("readWireplumberConfig", ["cat", configPath], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.log("AudioService: No existing WirePlumber config found");
|
||||
log.debug("No existing WirePlumber config found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -340,7 +341,7 @@ EOFCONFIG
|
||||
|
||||
if (Object.keys(aliases).length > 0) {
|
||||
deviceAliases = aliases;
|
||||
console.log("AudioService: Loaded", Object.keys(aliases).length, "device aliases");
|
||||
log.debug("Loaded", Object.keys(aliases).length, "device aliases");
|
||||
}
|
||||
}, 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) => {
|
||||
if (exitCode === 0 && output.trim()) {
|
||||
currentSoundTheme = output.trim();
|
||||
console.log("AudioService: Current system sound theme:", currentSoundTheme);
|
||||
log.debug("Current system sound theme:", currentSoundTheme);
|
||||
if (SettingsData.useSystemSoundTheme) {
|
||||
discoverSoundFiles(currentSoundTheme);
|
||||
}
|
||||
} else {
|
||||
currentSoundTheme = "";
|
||||
console.log("AudioService: No system sound theme found");
|
||||
log.debug("No system sound theme found");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -510,22 +511,22 @@ EOFCONFIG
|
||||
const themeLower = currentSoundTheme.toLowerCase();
|
||||
if (SettingsData.useSystemSoundTheme && specialConditions[themeLower]?.includes(soundEvent)) {
|
||||
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;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
discoverSoundFiles(currentSoundTheme);
|
||||
} else {
|
||||
@@ -549,7 +550,7 @@ EOFCONFIG
|
||||
MediaDevices {
|
||||
id: devices
|
||||
Component.onCompleted: {
|
||||
console.log("AudioService: MediaDevices initialized, default output:", defaultAudioOutput?.description)
|
||||
log.debug("MediaDevices initialized, default output:", defaultAudioOutput?.description)
|
||||
}
|
||||
}
|
||||
`, root, "AudioService.MediaDevices");
|
||||
@@ -560,7 +561,7 @@ EOFCONFIG
|
||||
Connections {
|
||||
target: root.mediaDevices
|
||||
function onDefaultAudioOutputChanged() {
|
||||
console.log("AudioService: Default audio output changed, recreating sound players")
|
||||
log.debug("Default audio output changed, recreating sound players")
|
||||
root.destroySoundPlayers()
|
||||
root.createSoundPlayers()
|
||||
}
|
||||
@@ -568,7 +569,7 @@ EOFCONFIG
|
||||
`, root, "AudioService.MediaDevicesConnections");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("AudioService: MediaDevices not available, using default audio output");
|
||||
log.debug("MediaDevices not available, using default audio output");
|
||||
mediaDevices = null;
|
||||
}
|
||||
}
|
||||
@@ -682,7 +683,7 @@ EOFCONFIG
|
||||
}
|
||||
`, root, "AudioService.LoginSound");
|
||||
} catch (e) {
|
||||
console.warn("AudioService: Error creating sound players:", e);
|
||||
log.warn("Error creating sound players:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
@@ -19,206 +18,217 @@ Singleton {
|
||||
readonly property bool enhancedPairingAvailable: DMSService.dmsAvailable && DMSService.apiVersion >= 9 && DMSService.capabilities.includes("bluetooth")
|
||||
readonly property bool connected: {
|
||||
if (!adapter || !adapter.devices) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
let isConnected = false
|
||||
adapter.devices.values.forEach(dev => { if (dev.connected) isConnected = true })
|
||||
return isConnected
|
||||
let isConnected = false;
|
||||
adapter.devices.values.forEach(dev => {
|
||||
if (dev.connected)
|
||||
isConnected = true;
|
||||
});
|
||||
return isConnected;
|
||||
}
|
||||
readonly property var pairedDevices: {
|
||||
if (!adapter || !adapter.devices) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev && (dev.paired || dev.trusted)
|
||||
})
|
||||
return dev && (dev.paired || dev.trusted);
|
||||
});
|
||||
}
|
||||
readonly property var allDevicesWithBattery: {
|
||||
if (!adapter || !adapter.devices) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev && dev.batteryAvailable && dev.battery > 0
|
||||
})
|
||||
return dev && dev.batteryAvailable && dev.battery > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function sortDevices(devices) {
|
||||
return devices.sort((a, b) => {
|
||||
const aName = a.name || a.deviceName || ""
|
||||
const bName = b.name || b.deviceName || ""
|
||||
const aAddr = a.address || ""
|
||||
const bAddr = b.address || ""
|
||||
const aName = a.name || a.deviceName || "";
|
||||
const bName = b.name || b.deviceName || "";
|
||||
const aAddr = a.address || "";
|
||||
const bAddr = b.address || "";
|
||||
|
||||
const aHasRealName = aName.includes(" ") && aName.length > 3
|
||||
const bHasRealName = bName.includes(" ") && bName.length > 3
|
||||
const aHasRealName = aName.includes(" ") && aName.length > 3;
|
||||
const bHasRealName = bName.includes(" ") && bName.length > 3;
|
||||
|
||||
if (aHasRealName && !bHasRealName) return -1
|
||||
if (!aHasRealName && bHasRealName) return 1
|
||||
if (aHasRealName && !bHasRealName)
|
||||
return -1;
|
||||
if (!aHasRealName && bHasRealName)
|
||||
return 1;
|
||||
|
||||
if (aHasRealName && bHasRealName) {
|
||||
return aName.localeCompare(bName)
|
||||
}
|
||||
if (aHasRealName && bHasRealName) {
|
||||
return aName.localeCompare(bName);
|
||||
}
|
||||
|
||||
return aAddr.localeCompare(bAddr)
|
||||
})
|
||||
return aAddr.localeCompare(bAddr);
|
||||
});
|
||||
}
|
||||
|
||||
function getDeviceIcon(device) {
|
||||
if (!device) {
|
||||
return "bluetooth"
|
||||
return "bluetooth";
|
||||
}
|
||||
|
||||
const name = (device.name || device.deviceName || "").toLowerCase()
|
||||
const icon = (device.icon || "").toLowerCase()
|
||||
const name = (device.name || device.deviceName || "").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))) {
|
||||
return "headset"
|
||||
return "headset";
|
||||
}
|
||||
|
||||
if (icon.includes("mouse") || name.includes("mouse")) {
|
||||
return "mouse"
|
||||
return "mouse";
|
||||
}
|
||||
|
||||
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))) {
|
||||
return "smartphone"
|
||||
return "smartphone";
|
||||
}
|
||||
|
||||
if (icon.includes("watch") || name.includes("watch")) {
|
||||
return "watch"
|
||||
return "watch";
|
||||
}
|
||||
|
||||
if (icon.includes("speaker") || name.includes("speaker")) {
|
||||
return "speaker"
|
||||
return "speaker";
|
||||
}
|
||||
|
||||
if (icon.includes("display") || name.includes("tv")) {
|
||||
return "tv"
|
||||
return "tv";
|
||||
}
|
||||
|
||||
return "bluetooth"
|
||||
return "bluetooth";
|
||||
}
|
||||
|
||||
function canConnect(device) {
|
||||
if (!device) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return !device.paired && !device.pairing && !device.blocked
|
||||
return !device.paired && !device.pairing && !device.blocked;
|
||||
}
|
||||
|
||||
function getSignalStrength(device) {
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||
return "Unknown"
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const signal = device.signalStrength
|
||||
const signal = device.signalStrength;
|
||||
if (signal >= 80) {
|
||||
return "Excellent"
|
||||
return "Excellent";
|
||||
}
|
||||
if (signal >= 60) {
|
||||
return "Good"
|
||||
return "Good";
|
||||
}
|
||||
if (signal >= 40) {
|
||||
return "Fair"
|
||||
return "Fair";
|
||||
}
|
||||
if (signal >= 20) {
|
||||
return "Poor"
|
||||
return "Poor";
|
||||
}
|
||||
|
||||
return "Very Poor"
|
||||
return "Very Poor";
|
||||
}
|
||||
|
||||
function getSignalIcon(device) {
|
||||
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) {
|
||||
return "signal_cellular_4_bar"
|
||||
return "signal_cellular_4_bar";
|
||||
}
|
||||
if (signal >= 60) {
|
||||
return "signal_cellular_3_bar"
|
||||
return "signal_cellular_3_bar";
|
||||
}
|
||||
if (signal >= 40) {
|
||||
return "signal_cellular_2_bar"
|
||||
return "signal_cellular_2_bar";
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
if (!device) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
device.trusted = true
|
||||
device.connect()
|
||||
device.trusted = true;
|
||||
device.connect();
|
||||
}
|
||||
|
||||
function pairDevice(device, callback) {
|
||||
if (!device) {
|
||||
if (callback) callback({error: "Invalid device"})
|
||||
return
|
||||
if (callback)
|
||||
callback({
|
||||
error: "Invalid device"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// The DMS backend actually implements a bluez agent, so we can pair anything
|
||||
if (enhancedPairingAvailable) {
|
||||
const devicePath = getDevicePath(device)
|
||||
DMSService.bluetoothPair(devicePath, callback)
|
||||
return
|
||||
const devicePath = getDevicePath(device);
|
||||
DMSService.bluetoothPair(devicePath, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.connect()
|
||||
if (callback) callback({success: true})
|
||||
device.trusted = true;
|
||||
device.connect();
|
||||
if (callback)
|
||||
callback({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
|
||||
function getCardName(device) {
|
||||
if (!device) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
return `bluez_card.${device.address.replace(/:/g, "_")}`
|
||||
return `bluez_card.${device.address.replace(/:/g, "_")}`;
|
||||
}
|
||||
|
||||
function getDevicePath(device) {
|
||||
if (!device || !device.address) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
const adapterPath = adapter ? "/org/bluez/hci0" : "/org/bluez/hci0"
|
||||
return `${adapterPath}/dev_${device.address.replace(/:/g, "_")}`
|
||||
const adapterPath = adapter ? "/org/bluez/hci0" : "/org/bluez/hci0";
|
||||
return `${adapterPath}/dev_${device.address.replace(/:/g, "_")}`;
|
||||
}
|
||||
|
||||
function isAudioDevice(device) {
|
||||
if (!device) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
const icon = getDeviceIcon(device)
|
||||
return icon === "headset" || icon === "speaker"
|
||||
const icon = getDeviceIcon(device);
|
||||
return icon === "headset" || icon === "speaker";
|
||||
}
|
||||
|
||||
function getCodecInfo(codecName) {
|
||||
const codec = codecName.replace(/-/g, "_").toUpperCase()
|
||||
const codec = codecName.replace(/-/g, "_").toUpperCase();
|
||||
|
||||
const codecMap = {
|
||||
"LDAC": {
|
||||
@@ -261,77 +271,77 @@ Singleton {
|
||||
"description": "Basic speech codec • Legacy compatibility",
|
||||
"qualityColor": "#9E9E9E"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return codecMap[codec] || {
|
||||
"name": codecName,
|
||||
"description": "Unknown codec",
|
||||
"qualityColor": "#9E9E9E"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
property var deviceCodecs: ({})
|
||||
|
||||
function updateDeviceCodec(deviceAddress, codec) {
|
||||
deviceCodecs[deviceAddress] = codec
|
||||
deviceCodecsChanged()
|
||||
deviceCodecs[deviceAddress] = codec;
|
||||
deviceCodecsChanged();
|
||||
}
|
||||
|
||||
function refreshDeviceCodec(device) {
|
||||
if (!device || !device.connected || !isAudioDevice(device)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecQueryProcess.cardName = cardName
|
||||
codecQueryProcess.deviceAddress = device.address
|
||||
codecQueryProcess.availableCodecs = []
|
||||
codecQueryProcess.parsingTargetCard = false
|
||||
codecQueryProcess.detectedCodec = ""
|
||||
codecQueryProcess.running = true
|
||||
const cardName = getCardName(device);
|
||||
codecQueryProcess.cardName = cardName;
|
||||
codecQueryProcess.deviceAddress = device.address;
|
||||
codecQueryProcess.availableCodecs = [];
|
||||
codecQueryProcess.parsingTargetCard = false;
|
||||
codecQueryProcess.detectedCodec = "";
|
||||
codecQueryProcess.running = true;
|
||||
}
|
||||
|
||||
function getCurrentCodec(device, callback) {
|
||||
if (!device || !device.connected || !isAudioDevice(device)) {
|
||||
callback("")
|
||||
return
|
||||
callback("");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecQueryProcess.cardName = cardName
|
||||
codecQueryProcess.callback = callback
|
||||
codecQueryProcess.availableCodecs = []
|
||||
codecQueryProcess.parsingTargetCard = false
|
||||
codecQueryProcess.detectedCodec = ""
|
||||
codecQueryProcess.running = true
|
||||
const cardName = getCardName(device);
|
||||
codecQueryProcess.cardName = cardName;
|
||||
codecQueryProcess.callback = callback;
|
||||
codecQueryProcess.availableCodecs = [];
|
||||
codecQueryProcess.parsingTargetCard = false;
|
||||
codecQueryProcess.detectedCodec = "";
|
||||
codecQueryProcess.running = true;
|
||||
}
|
||||
|
||||
function getAvailableCodecs(device, callback) {
|
||||
if (!device || !device.connected || !isAudioDevice(device)) {
|
||||
callback([], "")
|
||||
return
|
||||
callback([], "");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecFullQueryProcess.cardName = cardName
|
||||
codecFullQueryProcess.callback = callback
|
||||
codecFullQueryProcess.availableCodecs = []
|
||||
codecFullQueryProcess.parsingTargetCard = false
|
||||
codecFullQueryProcess.detectedCodec = ""
|
||||
codecFullQueryProcess.running = true
|
||||
const cardName = getCardName(device);
|
||||
codecFullQueryProcess.cardName = cardName;
|
||||
codecFullQueryProcess.callback = callback;
|
||||
codecFullQueryProcess.availableCodecs = [];
|
||||
codecFullQueryProcess.parsingTargetCard = false;
|
||||
codecFullQueryProcess.detectedCodec = "";
|
||||
codecFullQueryProcess.running = true;
|
||||
}
|
||||
|
||||
function switchCodec(device, profileName, callback) {
|
||||
if (!device || !isAudioDevice(device)) {
|
||||
callback(false, "Invalid device")
|
||||
return
|
||||
callback(false, "Invalid device");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecSwitchProcess.cardName = cardName
|
||||
codecSwitchProcess.profile = profileName
|
||||
codecSwitchProcess.callback = callback
|
||||
codecSwitchProcess.running = true
|
||||
const cardName = getCardName(device);
|
||||
codecSwitchProcess.cardName = cardName;
|
||||
codecSwitchProcess.profile = profileName;
|
||||
codecSwitchProcess.callback = callback;
|
||||
codecSwitchProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -349,67 +359,67 @@ Singleton {
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 0 && detectedCodec) {
|
||||
if (deviceAddress) {
|
||||
root.updateDeviceCodec(deviceAddress, detectedCodec)
|
||||
root.updateDeviceCodec(deviceAddress, detectedCodec);
|
||||
}
|
||||
if (callback) {
|
||||
callback(detectedCodec)
|
||||
callback(detectedCodec);
|
||||
}
|
||||
} else if (callback) {
|
||||
callback("")
|
||||
callback("");
|
||||
}
|
||||
|
||||
parsingTargetCard = false
|
||||
detectedCodec = ""
|
||||
availableCodecs = []
|
||||
deviceAddress = ""
|
||||
callback = null
|
||||
parsingTargetCard = false;
|
||||
detectedCodec = "";
|
||||
availableCodecs = [];
|
||||
deviceAddress = "";
|
||||
callback = null;
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
let line = data.trim()
|
||||
let line = data.trim();
|
||||
|
||||
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
|
||||
codecQueryProcess.parsingTargetCard = true
|
||||
return
|
||||
codecQueryProcess.parsingTargetCard = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
|
||||
codecQueryProcess.parsingTargetCard = false
|
||||
return
|
||||
codecQueryProcess.parsingTargetCard = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (codecQueryProcess.parsingTargetCard) {
|
||||
if (line.startsWith("Active Profile:")) {
|
||||
let profile = line.split(": ")[1] || ""
|
||||
let profile = line.split(": ")[1] || "";
|
||||
let activeCodec = codecQueryProcess.availableCodecs.find(c => {
|
||||
return c.profile === profile
|
||||
})
|
||||
return c.profile === profile;
|
||||
});
|
||||
if (activeCodec) {
|
||||
codecQueryProcess.detectedCodec = activeCodec.name
|
||||
codecQueryProcess.detectedCodec = activeCodec.name;
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (line.includes("codec") && line.includes("available: yes")) {
|
||||
let parts = line.split(": ")
|
||||
let parts = line.split(": ");
|
||||
if (parts.length >= 2) {
|
||||
let profile = parts[0].trim()
|
||||
let description = parts[1]
|
||||
let codecMatch = description.match(/codec ([^\)\s]+)/i)
|
||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN"
|
||||
let codecInfo = root.getCodecInfo(codecName)
|
||||
let profile = parts[0].trim();
|
||||
let description = parts[1];
|
||||
let codecMatch = description.match(/codec ([^\)\s]+)/i);
|
||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
|
||||
let codecInfo = root.getCodecInfo(codecName);
|
||||
if (codecInfo && !codecQueryProcess.availableCodecs.some(c => {
|
||||
return c.profile === profile
|
||||
})) {
|
||||
let newCodecs = codecQueryProcess.availableCodecs.slice()
|
||||
return c.profile === profile;
|
||||
})) {
|
||||
let newCodecs = codecQueryProcess.availableCodecs.slice();
|
||||
newCodecs.push({
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
})
|
||||
codecQueryProcess.availableCodecs = newCodecs
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
});
|
||||
codecQueryProcess.availableCodecs = newCodecs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,59 +441,59 @@ Singleton {
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (callback) {
|
||||
callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "")
|
||||
callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "");
|
||||
}
|
||||
parsingTargetCard = false
|
||||
detectedCodec = ""
|
||||
availableCodecs = []
|
||||
callback = null
|
||||
parsingTargetCard = false;
|
||||
detectedCodec = "";
|
||||
availableCodecs = [];
|
||||
callback = null;
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
let line = data.trim()
|
||||
let line = data.trim();
|
||||
|
||||
if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) {
|
||||
codecFullQueryProcess.parsingTargetCard = true
|
||||
return
|
||||
codecFullQueryProcess.parsingTargetCard = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) {
|
||||
codecFullQueryProcess.parsingTargetCard = false
|
||||
return
|
||||
codecFullQueryProcess.parsingTargetCard = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (codecFullQueryProcess.parsingTargetCard) {
|
||||
if (line.startsWith("Active Profile:")) {
|
||||
let profile = line.split(": ")[1] || ""
|
||||
let profile = line.split(": ")[1] || "";
|
||||
let activeCodec = codecFullQueryProcess.availableCodecs.find(c => {
|
||||
return c.profile === profile
|
||||
})
|
||||
return c.profile === profile;
|
||||
});
|
||||
if (activeCodec) {
|
||||
codecFullQueryProcess.detectedCodec = activeCodec.name
|
||||
codecFullQueryProcess.detectedCodec = activeCodec.name;
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (line.includes("codec") && line.includes("available: yes")) {
|
||||
let parts = line.split(": ")
|
||||
let parts = line.split(": ");
|
||||
if (parts.length >= 2) {
|
||||
let profile = parts[0].trim()
|
||||
let description = parts[1]
|
||||
let codecMatch = description.match(/codec ([^\)\s]+)/i)
|
||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN"
|
||||
let codecInfo = root.getCodecInfo(codecName)
|
||||
let profile = parts[0].trim();
|
||||
let description = parts[1];
|
||||
let codecMatch = description.match(/codec ([^\)\s]+)/i);
|
||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN";
|
||||
let codecInfo = root.getCodecInfo(codecName);
|
||||
if (codecInfo && !codecFullQueryProcess.availableCodecs.some(c => {
|
||||
return c.profile === profile
|
||||
})) {
|
||||
let newCodecs = codecFullQueryProcess.availableCodecs.slice()
|
||||
return c.profile === profile;
|
||||
})) {
|
||||
let newCodecs = codecFullQueryProcess.availableCodecs.slice();
|
||||
newCodecs.push({
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
})
|
||||
codecFullQueryProcess.availableCodecs = newCodecs
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
});
|
||||
codecFullQueryProcess.availableCodecs = newCodecs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,21 +513,21 @@ Singleton {
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
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 (exitCode === 0) {
|
||||
if (root.adapter && root.adapter.devices) {
|
||||
root.adapter.devices.values.forEach(device => {
|
||||
if (device && root.getCardName(device) === cardName) {
|
||||
Qt.callLater(() => root.refreshDeviceCodec(device))
|
||||
}
|
||||
})
|
||||
if (device && root.getCardName(device) === cardName) {
|
||||
Qt.callLater(() => root.refreshDeviceCodec(device));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
callback = null
|
||||
callback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland // ! Import is needed despite what qmlls says
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("BlurService")
|
||||
|
||||
property bool quickshellSupported: false
|
||||
property bool compositorSupported: false
|
||||
@@ -52,7 +54,7 @@ Singleton {
|
||||
targetWindow.BackgroundEffect.blurRegion = region;
|
||||
return region;
|
||||
} catch (e) {
|
||||
console.warn("BlurService: Failed to create blur region:", e);
|
||||
log.warn("Failed to create blur region:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -84,15 +86,15 @@ Singleton {
|
||||
onStreamFinished: {
|
||||
root.compositorSupported = text.trim() === "supported";
|
||||
if (root.compositorSupported)
|
||||
console.info("BlurService: Compositor supports ext-background-effect-v1");
|
||||
log.info("Compositor supports ext-background-effect-v1");
|
||||
else
|
||||
console.info("BlurService: Compositor does not support ext-background-effect-v1");
|
||||
log.info("Compositor does not support ext-background-effect-v1");
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
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");
|
||||
test.destroy();
|
||||
quickshellSupported = true;
|
||||
console.info("BlurService: Quickshell blur support available");
|
||||
log.info("Quickshell blur support available");
|
||||
blurProbe.running = true;
|
||||
} 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,68 +19,69 @@ Singleton {
|
||||
|
||||
function checkKhalAvailability() {
|
||||
if (!khalCheckProcess.running)
|
||||
khalCheckProcess.running = true
|
||||
khalCheckProcess.running = true;
|
||||
}
|
||||
|
||||
function detectKhalDateFormat() {
|
||||
if (!khalFormatProcess.running)
|
||||
khalFormatProcess.running = true
|
||||
khalFormatProcess.running = true;
|
||||
}
|
||||
|
||||
function parseKhalDateFormat(formatExample) {
|
||||
let qtFormat = formatExample.replace("12", "MM").replace("21", "dd").replace("2013", "yyyy")
|
||||
return { format: qtFormat, parser: null }
|
||||
let qtFormat = formatExample.replace("12", "MM").replace("21", "dd").replace("2013", "yyyy");
|
||||
return {
|
||||
format: qtFormat,
|
||||
parser: null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function loadCurrentMonth() {
|
||||
if (!root.khalAvailable)
|
||||
return
|
||||
|
||||
let today = new Date()
|
||||
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
|
||||
return;
|
||||
let today = new Date();
|
||||
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
// Add padding
|
||||
let startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7)
|
||||
let endDate = new Date(lastDay)
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7)
|
||||
loadEvents(startDate, endDate)
|
||||
let startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7);
|
||||
let endDate = new Date(lastDay);
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7);
|
||||
loadEvents(startDate, endDate);
|
||||
}
|
||||
|
||||
function loadEvents(startDate, endDate) {
|
||||
if (!root.khalAvailable) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (eventsProcess.running) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
// Store last requested date range for refresh timer
|
||||
root.lastStartDate = startDate
|
||||
root.lastEndDate = endDate
|
||||
root.isLoading = true
|
||||
root.lastStartDate = startDate;
|
||||
root.lastEndDate = endDate;
|
||||
root.isLoading = true;
|
||||
// Format dates for khal using detected format
|
||||
let startDateStr = Qt.formatDate(startDate, root.khalDateFormat)
|
||||
let endDateStr = Qt.formatDate(endDate, root.khalDateFormat)
|
||||
eventsProcess.requestStartDate = startDate
|
||||
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.running = true
|
||||
let startDateStr = Qt.formatDate(startDate, root.khalDateFormat);
|
||||
let endDateStr = Qt.formatDate(endDate, root.khalDateFormat);
|
||||
eventsProcess.requestStartDate = startDate;
|
||||
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.running = true;
|
||||
}
|
||||
|
||||
function getEventsForDate(date) {
|
||||
let dateKey = Qt.formatDate(date, "yyyy-MM-dd")
|
||||
return root.eventsByDate[dateKey] || []
|
||||
let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
|
||||
return root.eventsByDate[dateKey] || [];
|
||||
}
|
||||
|
||||
function hasEventsForDate(date) {
|
||||
let events = getEventsForDate(date)
|
||||
return events.length > 0
|
||||
let events = getEventsForDate(date);
|
||||
return events.length > 0;
|
||||
}
|
||||
|
||||
// Initialize on component completion
|
||||
Component.onCompleted: {
|
||||
detectKhalDateFormat()
|
||||
detectKhalDateFormat();
|
||||
}
|
||||
|
||||
// Process for detecting khal date format
|
||||
@@ -91,22 +92,22 @@ Singleton {
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
checkKhalAvailability()
|
||||
checkKhalAvailability();
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let lines = text.split('\n')
|
||||
let lines = text.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('dateformat:')) {
|
||||
let formatExample = line.substring(line.indexOf(':') + 1).trim()
|
||||
let formatInfo = parseKhalDateFormat(formatExample)
|
||||
root.khalDateFormat = formatInfo.format
|
||||
break
|
||||
let formatExample = line.substring(line.indexOf(':') + 1).trim();
|
||||
let formatInfo = parseKhalDateFormat(formatExample);
|
||||
root.khalDateFormat = formatInfo.format;
|
||||
break;
|
||||
}
|
||||
}
|
||||
checkKhalAvailability()
|
||||
checkKhalAvailability();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,9 +119,9 @@ Singleton {
|
||||
command: ["khal", "list", "today"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.khalAvailable = (exitCode === 0)
|
||||
root.khalAvailable = (exitCode === 0);
|
||||
if (exitCode === 0) {
|
||||
loadCurrentMonth()
|
||||
loadCurrentMonth();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,100 +136,96 @@ Singleton {
|
||||
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.isLoading = false
|
||||
root.isLoading = false;
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = "Failed to load events (exit code: " + exitCode + ")"
|
||||
return
|
||||
root.lastError = "Failed to load events (exit code: " + exitCode + ")";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let newEventsByDate = {}
|
||||
let lines = eventsProcess.rawOutput.split('\n')
|
||||
let newEventsByDate = {};
|
||||
let lines = eventsProcess.rawOutput.split('\n');
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
line = line.trim();
|
||||
if (!line || line === "[]")
|
||||
continue
|
||||
continue;
|
||||
|
||||
// Parse JSON line
|
||||
let dayEvents = JSON.parse(line)
|
||||
let dayEvents = JSON.parse(line);
|
||||
// Process each event in this day's array
|
||||
for (let event of dayEvents) {
|
||||
if (!event.title)
|
||||
continue
|
||||
continue;
|
||||
|
||||
// Parse start and end dates using detected format
|
||||
let startDate, endDate
|
||||
let startDate, endDate;
|
||||
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 {
|
||||
startDate = new Date()
|
||||
startDate = new 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 {
|
||||
endDate = new Date(startDate)
|
||||
endDate = new Date(startDate);
|
||||
}
|
||||
// Create start/end times
|
||||
let startTime = new Date(startDate)
|
||||
let endTime = new Date(endDate)
|
||||
if (event['start-time']
|
||||
&& event['all-day'] !== "True") {
|
||||
let startTime = new Date(startDate);
|
||||
let endTime = new Date(endDate);
|
||||
if (event['start-time'] && event['all-day'] !== "True") {
|
||||
// Parse time if available and not all-day
|
||||
let timeStr = event['start-time']
|
||||
let timeStr = event['start-time'];
|
||||
if (timeStr) {
|
||||
// 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) {
|
||||
let hours = parseInt(timeParts[1])
|
||||
let minutes = parseInt(timeParts[2])
|
||||
let hours = parseInt(timeParts[1]);
|
||||
let minutes = parseInt(timeParts[2]);
|
||||
|
||||
// Handle AM/PM conversion if present
|
||||
if (timeParts[3]) {
|
||||
let period = timeParts[3].toUpperCase()
|
||||
let period = timeParts[3].toUpperCase();
|
||||
if (period === 'PM' && hours !== 12) {
|
||||
hours += 12
|
||||
hours += 12;
|
||||
} else if (period === 'AM' && hours === 12) {
|
||||
hours = 0
|
||||
hours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
startTime.setHours(hours, minutes)
|
||||
startTime.setHours(hours, minutes);
|
||||
if (event['end-time']) {
|
||||
let endTimeParts = event['end-time'].match(
|
||||
/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i)
|
||||
let endTimeParts = event['end-time'].match(/(\d+):(\d+)(?::\d+)?\s*(AM|PM)?/i);
|
||||
if (endTimeParts) {
|
||||
let endHours = parseInt(endTimeParts[1])
|
||||
let endMinutes = parseInt(endTimeParts[2])
|
||||
let endHours = parseInt(endTimeParts[1]);
|
||||
let endMinutes = parseInt(endTimeParts[2]);
|
||||
|
||||
// Handle AM/PM conversion if present
|
||||
if (endTimeParts[3]) {
|
||||
let endPeriod = endTimeParts[3].toUpperCase()
|
||||
let endPeriod = endTimeParts[3].toUpperCase();
|
||||
if (endPeriod === 'PM' && endHours !== 12) {
|
||||
endHours += 12
|
||||
endHours += 12;
|
||||
} else if (endPeriod === 'AM' && endHours === 12) {
|
||||
endHours = 0
|
||||
endHours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
endTime.setHours(endHours, endMinutes)
|
||||
endTime.setHours(endHours, endMinutes);
|
||||
}
|
||||
} else {
|
||||
// Default to 1 hour duration on same day
|
||||
endTime = new Date(startTime)
|
||||
endTime.setHours(
|
||||
startTime.getHours() + 1)
|
||||
endTime = new Date(startTime);
|
||||
endTime.setHours(startTime.getHours() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create unique ID for this event (to track multi-day events)
|
||||
let eventId = event.title + "_" + event['start-date']
|
||||
+ "_" + (event['start-time'] || 'allday')
|
||||
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time'] || 'allday');
|
||||
// Create event object template
|
||||
let extractedUrl = ""
|
||||
let extractedUrl = "";
|
||||
if (!event.url && event.description) {
|
||||
let urlMatch = event.description.match(/https?:\/\/[^\s]+/)
|
||||
let urlMatch = event.description.match(/https?:\/\/[^\s]+/);
|
||||
if (urlMatch) {
|
||||
extractedUrl = urlMatch[0]
|
||||
extractedUrl = urlMatch[0];
|
||||
}
|
||||
}
|
||||
let eventTemplate = {
|
||||
@@ -242,75 +239,71 @@ Singleton {
|
||||
"calendar": "",
|
||||
"color": "",
|
||||
"allDay": event['all-day'] === "True",
|
||||
"isMultiDay": startDate.toDateString(
|
||||
) !== endDate.toDateString()
|
||||
}
|
||||
"isMultiDay": startDate.toDateString() !== endDate.toDateString()
|
||||
};
|
||||
// Add event to each day it spans
|
||||
let currentDate = new Date(startDate)
|
||||
let currentDate = new Date(startDate);
|
||||
while (currentDate <= endDate) {
|
||||
let dateKey = Qt.formatDate(currentDate,
|
||||
"yyyy-MM-dd")
|
||||
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd");
|
||||
if (!newEventsByDate[dateKey])
|
||||
newEventsByDate[dateKey] = []
|
||||
newEventsByDate[dateKey] = [];
|
||||
|
||||
// Check if this exact event is already added to this date (prevent duplicates)
|
||||
let existingEvent = newEventsByDate[dateKey].find(
|
||||
e => {
|
||||
return e.id === eventId
|
||||
})
|
||||
let existingEvent = newEventsByDate[dateKey].find(e => {
|
||||
return e.id === eventId;
|
||||
});
|
||||
if (existingEvent) {
|
||||
// Move to next day without adding duplicate
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
continue
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
// 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
|
||||
if (currentDate.getTime() === startDate.getTime()) {
|
||||
// First day - use original start time
|
||||
dayEvent.start = new Date(startTime)
|
||||
dayEvent.start = new Date(startTime);
|
||||
} else {
|
||||
// Subsequent days - start at beginning of day for all-day events
|
||||
dayEvent.start = new Date(currentDate)
|
||||
dayEvent.start = new Date(currentDate);
|
||||
if (!dayEvent.allDay)
|
||||
dayEvent.start.setHours(0, 0, 0, 0)
|
||||
dayEvent.start.setHours(0, 0, 0, 0);
|
||||
}
|
||||
if (currentDate.getTime() === endDate.getTime()) {
|
||||
// Last day - use original end time
|
||||
dayEvent.end = new Date(endTime)
|
||||
dayEvent.end = new Date(endTime);
|
||||
} else {
|
||||
// Earlier days - end at end of day for all-day events
|
||||
dayEvent.end = new Date(currentDate)
|
||||
dayEvent.end = new Date(currentDate);
|
||||
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
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort events by start time within each date
|
||||
for (let dateKey in newEventsByDate) {
|
||||
newEventsByDate[dateKey].sort((a, b) => {
|
||||
return a.start.getTime(
|
||||
) - b.start.getTime()
|
||||
})
|
||||
return a.start.getTime() - b.start.getTime();
|
||||
});
|
||||
}
|
||||
root.eventsByDate = newEventsByDate
|
||||
root.lastError = ""
|
||||
root.eventsByDate = newEventsByDate;
|
||||
root.lastError = "";
|
||||
} catch (error) {
|
||||
root.lastError = "Failed to parse events JSON: " + error.toString()
|
||||
root.eventsByDate = {}
|
||||
root.lastError = "Failed to parse events JSON: " + error.toString();
|
||||
root.eventsByDate = {};
|
||||
}
|
||||
// Reset for next run
|
||||
eventsProcess.rawOutput = ""
|
||||
eventsProcess.rawOutput = "";
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
eventsProcess.rawOutput += data + "\n"
|
||||
eventsProcess.rawOutput += data + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("ChangelogService")
|
||||
|
||||
readonly property string currentVersion: "1.4"
|
||||
readonly property bool changelogEnabled: false
|
||||
@@ -101,7 +103,7 @@ Singleton {
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("ChangelogService: Failed to create changelog marker");
|
||||
log.warn("Failed to create changelog marker");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("ClipboardService")
|
||||
|
||||
readonly property int longTextThreshold: 200
|
||||
|
||||
@@ -78,7 +80,7 @@ Singleton {
|
||||
}
|
||||
DMSService.sendRequest("clipboard.getHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardService: Failed to get history:", response.error);
|
||||
log.warn("Failed to get history:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = response.result || [];
|
||||
@@ -144,7 +146,7 @@ Singleton {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardService: Failed to delete entry:", response.error);
|
||||
log.warn("Failed to delete entry:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = internalEntries.filter(e => e.id !== entry.id);
|
||||
@@ -169,7 +171,7 @@ Singleton {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardService: Failed to delete entry:", response.error);
|
||||
log.warn("Failed to delete entry:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = internalEntries.filter(e => e.id !== entry.id);
|
||||
@@ -223,7 +225,7 @@ Singleton {
|
||||
const savedCount = pinnedCount;
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardService: Failed to clear history:", response.error);
|
||||
log.warn("Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
refresh();
|
||||
|
||||
@@ -7,9 +7,11 @@ import Quickshell.I3
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("CompositorService")
|
||||
|
||||
property bool isHyprland: false
|
||||
property bool isNiri: false
|
||||
@@ -52,7 +54,7 @@ Singleton {
|
||||
randrScales = scales;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CompositorService: failed to parse randr data:", e);
|
||||
log.warn("failed to parse randr data:", e);
|
||||
}
|
||||
}
|
||||
randrReady = true;
|
||||
@@ -379,9 +381,7 @@ Singleton {
|
||||
const focusedWin = NiriService.windows.find(nw => nw.is_focused);
|
||||
if (!focusedWin)
|
||||
return [];
|
||||
const screenWsIds = new Set(
|
||||
NiriService.allWorkspaces.filter(ws => ws.output === screenName).map(ws => ws.id)
|
||||
);
|
||||
const screenWsIds = new Set(NiriService.allWorkspaces.filter(ws => ws.output === screenName).map(ws => ws.id));
|
||||
return screenWsIds.has(focusedWin.workspace_id) ? toplevels : [];
|
||||
}
|
||||
return NiriService.filterCurrentDisplay(toplevels, screenName);
|
||||
@@ -454,7 +454,7 @@ Singleton {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("CompositorService: workspace snapshot failed:", e);
|
||||
log.warn("workspace snapshot failed:", e);
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null)
|
||||
@@ -498,7 +498,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "hyprland";
|
||||
console.info("CompositorService: Detected Hyprland");
|
||||
log.info("Detected Hyprland");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "niri";
|
||||
console.info("CompositorService: Detected Niri with socket:", niriSocket);
|
||||
log.info("Detected Niri with socket:", niriSocket);
|
||||
NiriService.generateNiriBlurrule();
|
||||
}
|
||||
}, 0);
|
||||
@@ -531,7 +531,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "sway";
|
||||
console.info("CompositorService: Detected Sway with socket:", swaySocket);
|
||||
log.info("Detected Sway with socket:", swaySocket);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
@@ -548,7 +548,7 @@ Singleton {
|
||||
isMiracle = true;
|
||||
isLabwc = false;
|
||||
compositor = "miracle";
|
||||
console.info("CompositorService: Detected Miracle WM with socket:", miracleSocket);
|
||||
log.info("Detected Miracle WM with socket:", miracleSocket);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
@@ -565,7 +565,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "scroll";
|
||||
console.info("CompositorService: Detected Scroll with socket:", scrollSocket);
|
||||
log.info("Detected Scroll with socket:", scrollSocket);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
@@ -580,7 +580,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = true;
|
||||
compositor = "labwc";
|
||||
console.info("CompositorService: Detected LabWC with PID:", labwcPid);
|
||||
log.info("Detected LabWC with PID:", labwcPid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -595,7 +595,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "unknown";
|
||||
console.warn("CompositorService: No compositor detected");
|
||||
log.warn("No compositor detected");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,7 +618,7 @@ Singleton {
|
||||
isMiracle = false;
|
||||
isLabwc = false;
|
||||
compositor = "dwl";
|
||||
console.info("CompositorService: Detected DWL via DMS capability");
|
||||
log.info("Detected DWL via DMS capability");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,7 +638,7 @@ Singleton {
|
||||
if (isLabwc) {
|
||||
Quickshell.execDetached(["dms", "dpms", "off"]);
|
||||
}
|
||||
console.warn("CompositorService: Cannot power off monitors, unknown compositor");
|
||||
log.warn("Cannot power off monitors, unknown compositor");
|
||||
}
|
||||
|
||||
function powerOnMonitors() {
|
||||
@@ -657,12 +657,12 @@ Singleton {
|
||||
if (isLabwc) {
|
||||
Quickshell.execDetached(["dms", "dpms", "on"]);
|
||||
}
|
||||
console.warn("CompositorService: Cannot power on monitors, unknown compositor");
|
||||
log.warn("Cannot power on monitors, unknown compositor");
|
||||
}
|
||||
|
||||
function _dwlPowerOffMonitors() {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -676,7 +676,7 @@ Singleton {
|
||||
|
||||
function _dwlPowerOnMonitors() {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user