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

Compare commits

..

27 Commits

Author SHA1 Message Date
bbedward 164db6c949 restore niri overview connected mode 2026-04-22 18:07:17 -04:00
bbedward e83c276bec some more simplifications and bug fixes 2026-04-22 18:07:17 -04:00
bbedward d1466783d5 de-dupe and cleanup 2026-04-22 18:07:16 -04:00
bbedward 3cf7c39213 restore CC and notification standalone behavior 2026-04-22 18:07:16 -04:00
bbedward a297611bb4 refactor connected/standalone architecture 2026-04-22 18:07:16 -04:00
purian23 2476075521 (frameMode): New Modal & Launcher connections 2026-04-22 18:07:16 -04:00
purian23 21a3ec1e5b (Notifications): Update body card expansions 2026-04-22 18:07:16 -04:00
purian23 4cf1b1a09f (frame): QOL Control Center & Notification updates 2026-04-22 18:07:16 -04:00
purian23 b9f33cabd6 feat(Frame): Close the gaps 2026-04-22 18:07:16 -04:00
purian23 50603c312a frame(Notifications): Update Arc path & Motion 2026-04-22 18:07:16 -04:00
purian23 a40d287446 (frame): Update animation sync w/Dank Popouts 2026-04-22 18:07:16 -04:00
purian23 dc881e4618 (frame): Performance round 2026-04-22 18:07:16 -04:00
purian23 d359603ca4 (frame): Update Connected blur Arcs & Enable shadow modes 2026-04-22 18:07:16 -04:00
purian23 4e085b00b6 frame(ConnectedMode): Wire up Notifications 2026-04-22 18:07:16 -04:00
purian23 a5263bee85 (frame): Update connected mode animation & motion logic 2026-04-22 18:07:16 -04:00
purian23 a8c08729be (frame): implement ConnectedModeState to better handle component sync 2026-04-22 18:07:16 -04:00
purian23 6cec54d481 (frameMode): Restore user settings when exiting frame mode
- Align blur settings in non-FrameMode motion settings
2026-04-22 18:07:16 -04:00
purian23 5701a7e831 (frame): Update connected mode with blur 2026-04-22 18:07:16 -04:00
purian23 b88f4471ac (frame): Update connected mode & opacity connection settings 2026-04-22 18:07:16 -04:00
purian23 cb82d276d5 (frameInMotion): Initial Unified Frame Connected Mode 2026-04-22 18:07:16 -04:00
purian23 cf2d143d08 Add Directional Motion options 2026-04-22 18:07:16 -04:00
purian23 aaae1aab53 Initial staging for Animation & Motion effects 2026-04-22 18:07:16 -04:00
purian23 23e09d723e (frame): Add blur support & cleanup 2026-04-22 18:07:16 -04:00
purian23 4dab8604b9 (frame): Multi-monitor support 2026-04-22 18:07:16 -04:00
purian23 ff1ec871f2 Connected frames & defaults 2026-04-22 18:07:16 -04:00
purian23 436a585ec0 Continue frame implementation 2026-04-22 18:07:16 -04:00
purian23 0fe6e2ea7a Initial framework 2026-04-22 18:07:16 -04:00
273 changed files with 8259 additions and 20553 deletions
+2 -22
View File
@@ -1,4 +1,4 @@
name: Nix flake and NixOS tests name: Check nix flake
on: on:
pull_request: pull_request:
@@ -9,7 +9,6 @@ on:
jobs: jobs:
check-flake: check-flake:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 120
steps: steps:
- name: Checkout - name: Checkout
@@ -19,25 +18,6 @@ jobs:
- name: Install Nix - name: Install Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@v31
with:
enable_kvm: true
extra_nix_config: |
system-features = nixos-test benchmark big-parallel kvm
- name: Check the flake - name: Check the flake
run: nix flake check -L run: nix flake check
- name: Run NixOS module test
run: nix build .#nixosTests.x86_64-linux.nixos-module -L
- name: Run NixOS service start test
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
- name: Run greeter niri test
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
- name: Run home-manager module test
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
- name: Run niri home-manager module test
run: nix build .#nixosTests.x86_64-linux.niri-home-module -L
+1 -1
View File
@@ -243,7 +243,7 @@ jobs:
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# ppa-upload.sh uploads to questing + resolute when series is omitted # ppa-upload.sh uploads to questing + resolute when series is omitted
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" "" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
echo "::error::Upload failed for $PKG" echo "::error::Upload failed for $PKG"
exit 1 exit 1
fi fi
-8
View File
@@ -20,11 +20,3 @@ repos:
language: system language: system
files: ^core/.*\.(go|mod|sum)$ files: ^core/.*\.(go|mod|sum)$
pass_filenames: false pass_filenames: false
- repo: local
hooks:
- id: no-console-in-qml
name: no console.* in QML (use Log service)
entry: bash -c 'if grep -nE "console\.(log|error|info|warn|debug)" "$@"; then echo "Use the Log service (log.info/warn/error/debug/fatal) instead of console.*" >&2; exit 1; fi' --
language: system
files: ^quickshell/.*\.qml$
exclude: ^quickshell/(Services/Log\.qml$|dms-plugins/|PLUGINS/)
-13
View File
@@ -26,17 +26,6 @@ var runCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
daemon, _ := cmd.Flags().GetBool("daemon") daemon, _ := cmd.Flags().GetBool("daemon")
session, _ := cmd.Flags().GetBool("session") session, _ := cmd.Flags().GetBool("session")
if v, _ := cmd.Flags().GetString("log-level"); v != "" {
if err := os.Setenv("DMS_LOG_LEVEL", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_LEVEL: %v", err)
}
}
if v, _ := cmd.Flags().GetString("log-file"); v != "" {
if err := os.Setenv("DMS_LOG_FILE", v); err != nil {
log.Fatalf("Failed to set DMS_LOG_FILE: %v", err)
}
}
log.ApplyEnvOverrides()
if daemon { if daemon {
runShellDaemon(session) runShellDaemon(session)
} else { } else {
@@ -537,7 +526,5 @@ func getCommonCommands() []*cobra.Command {
dlCmd, dlCmd,
randrCmd, randrCmd,
blurCmd, blurCmd,
trashCmd,
systemCmd,
} }
} }
-35
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -12,7 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -269,8 +267,6 @@ func runSetupDmsConfig(name string) error {
func runSetup() error { func runSetup() error {
fmt.Println("=== DMS Configuration Setup ===") fmt.Println("=== DMS Configuration Setup ===")
ensureInputGroup()
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd() useSystemd := promptSystemd()
@@ -344,37 +340,6 @@ func runSetup() error {
return nil return nil
} }
// Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() {
if !utils.HasGroup("input") {
return
}
currentUser := os.Getenv("USER")
if currentUser == "" {
currentUser = os.Getenv("LOGNAME")
}
if currentUser == "" {
return
}
out, err := execGroups(currentUser)
if err == nil && strings.Contains(out, "input") {
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
return
}
fmt.Println("Adding user to input group for Caps Lock OSD support...")
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
} else {
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
}
}
func execGroups(user string) (string, error) {
out, err := exec.Command("groups", user).Output()
return string(out), err
}
func promptCompositor() (deps.WindowManager, bool) { func promptCompositor() (deps.WindowManager, bool) {
fmt.Println("Select compositor:") fmt.Println("Select compositor:")
fmt.Println("1) Niri") fmt.Println("1) Niri")
-277
View File
@@ -1,277 +0,0 @@
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
}
-122
View File
@@ -1,122 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/trash"
"github.com/spf13/cobra"
)
var trashCmd = &cobra.Command{
Use: "trash",
Short: "Manage the user's trash (XDG Trash spec 1.0)",
}
var trashPutCmd = &cobra.Command{
Use: "put <path...>",
Short: "Move files or directories into the trash",
Args: cobra.MinimumNArgs(1),
Run: runTrashPut,
}
var trashListCmd = &cobra.Command{
Use: "list",
Short: "List trashed items across all known trash directories",
Run: runTrashList,
}
var trashCountCmd = &cobra.Command{
Use: "count",
Short: "Print the total number of trashed items",
Run: runTrashCount,
}
var trashEmptyCmd = &cobra.Command{
Use: "empty",
Short: "Permanently delete every trashed item",
Run: runTrashEmpty,
}
var trashRestoreCmd = &cobra.Command{
Use: "restore <name>",
Short: "Restore a trashed item to its original location",
Args: cobra.ExactArgs(1),
Run: runTrashRestore,
}
var (
trashJSONOutput bool
trashRestoreDir string
)
func init() {
trashListCmd.Flags().BoolVar(&trashJSONOutput, "json", false, "Output as JSON")
trashRestoreCmd.Flags().StringVar(&trashRestoreDir, "trash-dir", "", "Trash directory containing the item (default: home trash)")
trashCmd.AddCommand(trashPutCmd, trashListCmd, trashCountCmd, trashEmptyCmd, trashRestoreCmd)
}
func runTrashPut(cmd *cobra.Command, args []string) {
var failed int
for _, p := range args {
if _, err := trash.Put(p); err != nil {
log.Errorf("trash %s: %v", p, err)
failed++
continue
}
fmt.Println(p)
}
if failed > 0 {
os.Exit(1)
}
}
func runTrashList(cmd *cobra.Command, args []string) {
entries, err := trash.List()
if err != nil {
log.Fatalf("list trash: %v", err)
}
if trashJSONOutput {
if entries == nil {
entries = []trash.Entry{}
}
out, _ := json.MarshalIndent(entries, "", " ")
fmt.Println(string(out))
return
}
if len(entries) == 0 {
fmt.Println("Trash is empty")
return
}
for _, e := range entries {
marker := "F"
if e.IsDir {
marker = "D"
}
fmt.Printf("%s %s %s %s\n", marker, e.DeletionDate, e.Name, e.OriginalPath)
}
}
func runTrashCount(cmd *cobra.Command, args []string) {
n, err := trash.Count()
if err != nil {
log.Fatalf("count trash: %v", err)
}
fmt.Println(n)
}
func runTrashEmpty(cmd *cobra.Command, args []string) {
if err := trash.Empty(); err != nil {
log.Fatalf("empty trash: %v", err)
}
}
func runTrashRestore(cmd *cobra.Command, args []string) {
if err := trash.Restore(args[0], trashRestoreDir); err != nil {
log.Fatalf("restore: %v", err)
}
}
-2
View File
@@ -15,8 +15,6 @@ func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
-2
View File
@@ -15,8 +15,6 @@ func init() {
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
-14
View File
@@ -80,16 +80,6 @@ func getRuntimeDir() string {
return os.TempDir() return os.TempDir()
} }
func appendLogEnv(env []string) []string {
if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
env = append(env, "DMS_LOG_LEVEL="+v)
}
if v := os.Getenv("DMS_LOG_FILE"); v != "" {
env = append(env, "DMS_LOG_FILE="+v)
}
return env
}
func hasSystemdRun() bool { func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run") _, err := exec.LookPath("systemd-run")
return err == nil return err == nil
@@ -226,8 +216,6 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
} }
cmd.Env = appendLogEnv(cmd.Env)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -471,8 +459,6 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
} }
cmd.Env = appendLogEnv(cmd.Env)
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0) devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil { if err != nil {
log.Fatalf("Error opening /dev/null: %v", err) log.Fatalf("Error opening /dev/null: %v", err)
+21 -21
View File
@@ -6,11 +6,11 @@ toolchain go1.26.1
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.24.0 github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0 github.com/charmbracelet/log v0.4.2
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/godbus/dbus/v5 v5.2.2 github.com/godbus/dbus/v5 v5.2.2
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
@@ -20,27 +20,28 @@ require (
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0 github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
golang.org/x/image v0.39.0 golang.org/x/image v0.36.0
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fogleman/gg v1.3.0 // indirect github.com/fogleman/gg v1.3.0 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260424211911-732291493fb8 // indirect github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
@@ -48,37 +49,36 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/crypto v0.50.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.53.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.20.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-alpha.2 github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.22 github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.43.0 golang.org/x/sys v0.41.0
golang.org/x/text v0.36.0 golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
+53 -49
View File
@@ -1,14 +1,14 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U= github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg= github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.24.0 h1:zrg+k0tAaVbM8whaT2hR5DOUqAdopsDaH998EGi6Llk= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.24.0/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -24,22 +24,22 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
@@ -52,8 +52,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -66,12 +66,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20260424211911-732291493fb8 h1:QRpwB1ans3fB3Cmeuog1ATzvXg/xhqubqiQi97xNO6E= github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
github.com/go-git/go-billy/v6 v6.0.0-20260424211911-732291493fb8/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU= 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/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s= 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-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI= github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak= github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -79,6 +79,8 @@ github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -93,20 +95,20 @@ github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7Dmvb
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -123,8 +125,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E= github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
@@ -153,33 +155,35 @@ github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0= github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -98
View File
@@ -1,16 +1,12 @@
package log package log
import ( import (
"io"
"os" "os"
"regexp"
"strings" "strings"
"sync" "sync"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
cblog "github.com/charmbracelet/log" cblog "github.com/charmbracelet/log"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
) )
// Logger embeds the Charm Logger and adds Printf/Fatalf // Logger embeds the Charm Logger and adds Printf/Fatalf
@@ -25,26 +21,8 @@ func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...)
var ( var (
logger *Logger logger *Logger
initLogger sync.Once initLogger sync.Once
logMu sync.Mutex
logFile *os.File
logStderr io.Writer = os.Stderr
ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
) )
// ansiStripWriter strips ANSI escape sequences before forwarding to w. Used
// for the file sink so colored stderr stays colored while the file stays plain.
type ansiStripWriter struct{ w io.Writer }
func (a *ansiStripWriter) Write(p []byte) (int, error) {
stripped := ansiRe.ReplaceAll(p, nil)
if _, err := a.w.Write(stripped); err != nil {
return 0, err
}
return len(p), nil
}
func parseLogLevel(level string) cblog.Level { func parseLogLevel(level string) cblog.Level {
switch strings.ToLower(level) { switch strings.ToLower(level) {
case "debug": case "debug":
@@ -108,7 +86,7 @@ func GetLogger() *Logger {
SetString(" DEBUG"). SetString(" DEBUG").
Foreground(lipgloss.Color("4")) Foreground(lipgloss.Color("4"))
base := cblog.New(logStderr) base := cblog.New(os.Stderr)
base.SetStyles(styles) base.SetStyles(styles)
base.SetReportTimestamp(false) base.SetReportTimestamp(false)
@@ -120,85 +98,10 @@ func GetLogger() *Logger {
base.SetPrefix(" go") base.SetPrefix(" go")
logger = &Logger{base} logger = &Logger{base}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
_ = SetLogFile(path)
}
}) })
return logger return logger
} }
// SetLevel updates the active log level. Accepts the same strings as
// DMS_LOG_LEVEL. Unknown values default to info.
func SetLevel(level string) {
GetLogger().SetLevel(parseLogLevel(level))
}
// SetLogFile makes the logger append to path in addition to stderr. Passing an
// empty string detaches the file sink. Atomic per-line writes (≤PIPE_BUF) on
// O_APPEND keep concurrent Go and QML writers from corrupting each other.
//
// Color handling: charmbracelet/log auto-detects color support from its
// io.Writer, and io.MultiWriter doesn't pass that through, so we force the ANSI
// profile when stderr is a TTY and route the file through ansiStripWriter so
// the file stays plain while stderr keeps its colors.
func SetLogFile(path string) error {
logMu.Lock()
defer logMu.Unlock()
if logFile != nil {
logFile.Close()
logFile = nil
}
l := GetLogger()
if path == "" {
l.SetOutput(logStderr)
applyColorProfile(l, logStderr)
return nil
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
if err != nil {
return err
}
logFile = f
out := io.MultiWriter(logStderr, &ansiStripWriter{w: f})
l.SetOutput(out)
applyColorProfile(l, logStderr)
return nil
}
// applyColorProfile forces the renderer's color profile to match what stderr
// would produce on its own, undoing the auto-downgrade triggered by wrapping
// stderr in a non-TTY writer (e.g. io.MultiWriter).
func applyColorProfile(l *Logger, stderr io.Writer) {
f, ok := stderr.(*os.File)
if !ok {
l.SetColorProfile(termenv.Ascii)
return
}
if isatty.IsTerminal(f.Fd()) {
l.SetColorProfile(termenv.ANSI)
return
}
l.SetColorProfile(termenv.Ascii)
}
// ApplyEnvOverrides re-reads DMS_LOG_LEVEL and DMS_LOG_FILE and reconfigures
// the singleton. Safe to call after CLI flags have rewritten the environment.
func ApplyEnvOverrides() {
GetLogger()
if level := os.Getenv("DMS_LOG_LEVEL"); level != "" {
SetLevel(level)
}
if path := os.Getenv("DMS_LOG_FILE"); path != "" {
if err := SetLogFile(path); err != nil {
Warnf("Failed to open log file %q: %v", path, err)
}
}
}
// * Convenience wrappers // * Convenience wrappers
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) } func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) }
-1
View File
@@ -60,7 +60,6 @@ var templateRegistry = []TemplateDef{
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, {ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"}, {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigFile: "vencord.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"}, {ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal}, {ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
+34 -70
View File
@@ -212,10 +212,9 @@ func (m *Manager) setupDataDeviceSync() {
} }
var offer any var offer any
switch { if e.Id != nil {
case e.Id != nil:
offer = e.Id offer = e.Id
case e.OfferId != 0: } else if e.OfferId != 0 {
m.offerMutex.RLock() m.offerMutex.RLock()
offer = m.offerRegistry[e.OfferId] offer = m.offerRegistry[e.OfferId]
m.offerMutex.RUnlock() m.offerMutex.RUnlock()
@@ -225,6 +224,10 @@ func (m *Manager) setupDataDeviceSync() {
wasOwner := m.isOwner wasOwner := m.isOwner
m.ownerLock.Unlock() m.ownerLock.Unlock()
if offer == nil {
return
}
if wasOwner { if wasOwner {
return return
} }
@@ -233,11 +236,9 @@ func (m *Manager) setupDataDeviceSync() {
m.currentOffer = offer m.currentOffer = offer
if prevOffer != nil && prevOffer != offer { if prevOffer != nil && prevOffer != offer {
m.releaseOffer(prevOffer) m.offerMutex.Lock()
} delete(m.offerMimeTypes, prevOffer)
m.offerMutex.Unlock()
if offer == nil {
return
} }
m.offerMutex.RLock() m.offerMutex.RLock()
@@ -291,33 +292,6 @@ func (m *Manager) setupDataDeviceSync() {
log.Info("Data device setup complete") log.Info("Data device setup complete")
} }
func (m *Manager) releaseOffer(offer any) {
if offer == nil {
return
}
typedOffer, ok := offer.(*ext_data_control.ExtDataControlOfferV1)
if !ok {
return
}
m.offerMutex.Lock()
delete(m.offerMimeTypes, offer)
delete(m.offerRegistry, typedOffer.ID())
m.offerMutex.Unlock()
typedOffer.Destroy()
}
func (m *Manager) releaseCurrentSource() {
if m.currentSource == nil {
return
}
source, ok := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
m.currentSource = nil
if !ok {
return
}
source.Destroy()
}
func (m *Manager) readAndStore(r *os.File, mimeType string) { func (m *Manager) readAndStore(r *os.File, mimeType string) {
defer r.Close() defer r.Close()
@@ -421,7 +395,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
if extractHash(v) != hash { if extractHash(v) != hash {
continue continue
} }
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err == nil && entry.Pinned { if err == nil && entry.Pinned {
continue continue
} }
@@ -439,7 +413,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
c := b.Cursor() c := b.Cursor()
var count int var count int
for k, v := c.Last(); k != nil; k, v = c.Prev() { for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err == nil && entry.Pinned { if err == nil && entry.Pinned {
continue continue
} }
@@ -482,14 +456,6 @@ func encodeEntry(e Entry) ([]byte, error) {
} }
func decodeEntry(data []byte) (Entry, error) { func decodeEntry(data []byte) (Entry, error) {
return decodeEntryFields(data, true)
}
func decodeEntryMeta(data []byte) (Entry, error) {
return decodeEntryFields(data, false)
}
func decodeEntryFields(data []byte, withData bool) (Entry, error) {
buf := bytes.NewReader(data) buf := bytes.NewReader(data)
var e Entry var e Entry
@@ -497,15 +463,8 @@ func decodeEntryFields(data []byte, withData bool) (Entry, error) {
var dataLen uint32 var dataLen uint32
binary.Read(buf, binary.BigEndian, &dataLen) binary.Read(buf, binary.BigEndian, &dataLen)
switch { e.Data = make([]byte, dataLen)
case withData: buf.Read(e.Data)
e.Data = make([]byte, dataLen)
buf.Read(e.Data)
default:
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
return e, err
}
}
var mimeLen uint32 var mimeLen uint32
binary.Read(buf, binary.BigEndian, &mimeLen) binary.Read(buf, binary.BigEndian, &mimeLen)
@@ -709,9 +668,14 @@ func sizeStr(size int) string {
func (m *Manager) updateState() { func (m *Manager) updateState() {
history := m.GetHistory() history := m.GetHistory()
for i := range history {
history[i].Data = nil
}
var current *Entry var current *Entry
if len(history) > 0 { if len(history) > 0 {
c := history[0] c := history[0]
c.Data = nil
current = &c current = &c
} }
@@ -786,7 +750,7 @@ func (m *Manager) GetHistory() []Entry {
c := b.Cursor() c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() { for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err != nil { if err != nil {
continue continue
} }
@@ -971,7 +935,7 @@ func (m *Manager) ClearHistory() {
var toDelete [][]byte var toDelete [][]byte
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err != nil || !entry.Pinned { if err != nil || !entry.Pinned {
toDelete = append(toDelete, k) toDelete = append(toDelete, k)
} }
@@ -994,7 +958,7 @@ func (m *Manager) ClearHistory() {
if b != nil { if b != nil {
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
entry, _ := decodeEntryMeta(v) entry, _ := decodeEntry(v)
if entry.Pinned { if entry.Pinned {
pinnedCount++ pinnedCount++
} }
@@ -1102,7 +1066,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
m.ownerLock.Unlock() m.ownerLock.Unlock()
}) })
m.releaseCurrentSource()
m.currentSource = source m.currentSource = source
m.sourceMutex.Lock() m.sourceMutex.Lock()
m.sourceMimeTypes = []string{mimeType} m.sourceMimeTypes = []string{mimeType}
@@ -1182,11 +1145,9 @@ func (m *Manager) Close() {
m.subscribers = make(map[string]chan State) m.subscribers = make(map[string]chan State)
m.subMutex.Unlock() m.subMutex.Unlock()
m.releaseCurrentSource() if m.currentSource != nil {
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
if m.currentOffer != nil { source.Destroy()
m.releaseOffer(m.currentOffer)
m.currentOffer = nil
} }
if m.dataDevice != nil { if m.dataDevice != nil {
@@ -1230,10 +1191,11 @@ func (m *Manager) clearOldEntries(days int) error {
var toDelete [][]byte var toDelete [][]byte
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err != nil { if err != nil {
continue continue
} }
// Skip pinned entries
if entry.Pinned { if entry.Pinned {
continue continue
} }
@@ -1348,7 +1310,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
c := b.Cursor() c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() { for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err != nil { if err != nil {
continue continue
} }
@@ -1373,6 +1335,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
continue continue
} }
entry.Data = nil
all = append(all, entry) all = append(all, entry)
} }
return nil return nil
@@ -1547,7 +1510,7 @@ func (m *Manager) PinEntry(id uint64) error {
} }
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err != nil || !entry.Pinned { if err != nil || !entry.Pinned {
continue continue
} }
@@ -1565,6 +1528,7 @@ func (m *Manager) PinEntry(id uint64) error {
return nil return nil
} }
// Check pinned count
cfg := m.getConfig() cfg := m.getConfig()
pinnedCount := 0 pinnedCount := 0
if err := m.db.View(func(tx *bolt.Tx) error { if err := m.db.View(func(tx *bolt.Tx) error {
@@ -1574,7 +1538,7 @@ func (m *Manager) PinEntry(id uint64) error {
} }
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err == nil && entry.Pinned { if err == nil && entry.Pinned {
pinnedCount++ pinnedCount++
} }
@@ -1665,11 +1629,12 @@ func (m *Manager) GetPinnedEntries() []Entry {
c := b.Cursor() c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() { for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err != nil { if err != nil {
continue continue
} }
if entry.Pinned { if entry.Pinned {
entry.Data = nil
pinned = append(pinned, entry) pinned = append(pinned, entry)
} }
} }
@@ -1695,7 +1660,7 @@ func (m *Manager) GetPinnedCount() int {
c := b.Cursor() c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() { for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntryMeta(v) entry, err := decodeEntry(v)
if err == nil && entry.Pinned { if err == nil && entry.Pinned {
count++ count++
} }
@@ -1814,7 +1779,6 @@ func (m *Manager) CopyFile(filePath string) error {
m.ownerLock.Unlock() m.ownerLock.Unlock()
}) })
m.releaseCurrentSource()
m.currentSource = source m.currentSource = source
m.ownerLock.Lock() m.ownerLock.Lock()
+1 -1
View File
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
func InitializeManager() (*Manager, error) { func InitializeManager() (*Manager, error) {
if os.Getuid() != 0 && !hasInputGroupAccess() { if os.Getuid() != 0 && !hasInputGroupAccess() {
return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`") return nil, fmt.Errorf("insufficient permissions to access input devices")
} }
return NewManager() return NewManager()
@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
return false return false
} }
if reply != dbus.RequestNameReplyPrimaryOwner { if reply != dbus.RequestNameReplyPrimaryOwner {
log.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", name) log.Warnf("Screensaver name %s already owned by another process", name)
return false return false
} }
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil { if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
-10
View File
@@ -20,7 +20,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins" serverPlugins "github.com/AvengeMedia/DankMaterialShell/core/internal/server/plugins"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes" serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
@@ -203,15 +202,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "sysupdate.") {
if sysUpdateManager == nil {
models.RespondError(conn, req.ID, "sysupdate manager not initialized")
return
}
sysupdate.HandleRequest(conn, req, sysUpdateManager)
return
}
switch req.Method { switch req.Method {
case "ping": case "ping":
models.Respond(conn, req.ID, "pong") models.Respond(conn, req.ID, "pong")
-62
View File
@@ -30,7 +30,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
@@ -76,7 +75,6 @@ var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var trayRecoveryManager *trayrecovery.Manager var trayRecoveryManager *trayrecovery.Manager
var locationManager *location.Manager var locationManager *location.Manager
var sysUpdateManager *sysupdate.Manager
var geoClientInstance geolocation.Client var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
@@ -423,19 +421,6 @@ func InitializeLocationManager(geoClient geolocation.Client) error {
return nil return nil
} }
func InitializeSysUpdateManager() error {
manager, err := sysupdate.NewManager()
if err != nil {
log.Warnf("Failed to initialize sysupdate manager: %v", err)
return err
}
sysUpdateManager = manager
log.Info("Sysupdate manager initialized")
return nil
}
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
@@ -521,10 +506,6 @@ func getCapabilities() Capabilities {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
if sysUpdateManager != nil {
caps = append(caps, "sysupdate")
}
return Capabilities{Capabilities: caps} return Capabilities{Capabilities: caps}
} }
@@ -595,10 +576,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "dbus") caps = append(caps, "dbus")
} }
if sysUpdateManager != nil {
caps = append(caps, "sysupdate")
}
return ServerInfo{ return ServerInfo{
APIVersion: APIVersion, APIVersion: APIVersion,
CLIVersion: CLIVersion, CLIVersion: CLIVersion,
@@ -1266,38 +1243,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("sysupdate") && sysUpdateManager != nil {
wg.Add(1)
sysupdateChan := sysUpdateManager.Subscribe(clientID + "-sysupdate")
go func() {
defer wg.Done()
defer sysUpdateManager.Unsubscribe(clientID + "-sysupdate")
initialState := sysUpdateManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "sysupdate", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-sysupdateChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "sysupdate", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("dbus") && dbusManager != nil { if shouldSubscribe("dbus") && dbusManager != nil {
wg.Add(1) wg.Add(1)
dbusChan := dbusManager.SubscribeSignals(dbusClientID) dbusChan := dbusManager.SubscribeSignals(dbusClientID)
@@ -1403,9 +1348,6 @@ func cleanupManagers() {
if locationManager != nil { if locationManager != nil {
locationManager.Close() locationManager.Close()
} }
if sysUpdateManager != nil {
sysUpdateManager.Close()
}
if geoClientInstance != nil { if geoClientInstance != nil {
geoClientInstance.Close() geoClientInstance.Close()
} }
@@ -1791,10 +1733,6 @@ func Start(printDocs bool) error {
} }
}() }()
if err := InitializeSysUpdateManager(); err != nil {
log.Warnf("Sysupdate manager unavailable: %v", err)
}
log.Info("") log.Info("")
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities) log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
-96
View File
@@ -1,96 +0,0 @@
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
}
@@ -1,79 +0,0 @@
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
}
@@ -1,72 +0,0 @@
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)
}
})
}
}
@@ -1,125 +0,0 @@
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
}
@@ -1,80 +0,0 @@
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)
}
})
}
}
@@ -1,169 +0,0 @@
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
}
@@ -1,150 +0,0 @@
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)
}
})
}
}
@@ -1,258 +0,0 @@
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
}
@@ -1,114 +0,0 @@
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)
}
})
}
}
@@ -1,125 +0,0 @@
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})
}
@@ -1,104 +0,0 @@
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)
}
})
}
}
@@ -1,83 +0,0 @@
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})
}
@@ -1,80 +0,0 @@
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
View File
@@ -1,125 +0,0 @@
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}
}
}
@@ -1,55 +0,0 @@
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
View File
@@ -1,506 +0,0 @@
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
View File
@@ -1,86 +0,0 @@
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
}
-455
View File
@@ -1,455 +0,0 @@
// Package trash implements the FreeDesktop.org Trash specification 1.0.
// See: https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html
package trash
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
const trashInfoExt = ".trashinfo"
type Entry struct {
Name string `json:"name"`
OriginalPath string `json:"originalPath"`
DeletionDate string `json:"deletionDate"`
TrashDir string `json:"trashDir"`
FilesPath string `json:"filesPath"`
InfoPath string `json:"infoPath"`
Size int64 `json:"size"`
IsDir bool `json:"isDir"`
}
func homeTrashDir() (string, error) {
xdg := os.Getenv("XDG_DATA_HOME")
if xdg == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
xdg = filepath.Join(home, ".local", "share")
}
return filepath.Join(xdg, "Trash"), nil
}
func ensureTrashDirs(trashDir string) error {
if err := os.MkdirAll(filepath.Join(trashDir, "files"), 0o700); err != nil {
return err
}
return os.MkdirAll(filepath.Join(trashDir, "info"), 0o700)
}
func fsDevice(path string) (uint64, error) {
var st syscall.Stat_t
if err := syscall.Lstat(path, &st); err != nil {
return 0, err
}
return uint64(st.Dev), nil
}
func fsDeviceWalkUp(start string) (uint64, error) {
cur := start
for {
if dev, err := fsDevice(cur); err == nil {
return dev, nil
}
parent := filepath.Dir(cur)
if parent == cur {
return 0, fmt.Errorf("no existing ancestor for %s", start)
}
cur = parent
}
}
func findTopDir(path string) (string, error) {
abs, err := filepath.Abs(path)
if err != nil {
return "", err
}
dev, err := fsDevice(abs)
if err != nil {
return "", err
}
cur := abs
for {
parent := filepath.Dir(cur)
if parent == cur {
return cur, nil
}
pdev, err := fsDevice(parent)
if err != nil {
return cur, nil
}
if pdev != dev {
return cur, nil
}
cur = parent
}
}
// isValidSharedTrash enforces the spec's checks on $topdir/.Trash:
// must exist, must be a directory, must not be a symlink, must have sticky bit.
func isValidSharedTrash(p string) bool {
info, err := os.Lstat(p)
if err != nil {
return false
}
if info.Mode()&os.ModeSymlink != 0 {
return false
}
if !info.IsDir() {
return false
}
return info.Mode()&os.ModeSticky != 0
}
// trashDirForPath chooses the correct trash dir per spec and returns the value
// to store in the .trashinfo Path field (absolute for home, relative-to-topdir
// for per-mountpoint trash).
func trashDirForPath(absPath string) (trashDir string, storedPath string, err error) {
home, err := homeTrashDir()
if err != nil {
return "", "", err
}
pathDev, err := fsDevice(absPath)
if err != nil {
return "", "", err
}
homeDev, err := fsDeviceWalkUp(home)
if err != nil {
return "", "", err
}
if pathDev == homeDev {
return home, absPath, nil
}
topDir, err := findTopDir(absPath)
if err != nil {
return "", "", err
}
uid := strconv.Itoa(os.Getuid())
stored, rerr := filepath.Rel(topDir, absPath)
if rerr != nil || strings.HasPrefix(stored, "..") {
stored = absPath
}
shared := filepath.Join(topDir, ".Trash")
if isValidSharedTrash(shared) {
return filepath.Join(shared, uid), stored, nil
}
return filepath.Join(topDir, ".Trash-"+uid), stored, nil
}
// uniqueName returns a basename in trashDir that does not collide with an
// existing entry in either files/ or info/.
func uniqueName(trashDir, basename string) (string, error) {
filesDir := filepath.Join(trashDir, "files")
infoDir := filepath.Join(trashDir, "info")
if !exists(filepath.Join(filesDir, basename)) && !exists(filepath.Join(infoDir, basename+trashInfoExt)) {
return basename, nil
}
ext := filepath.Ext(basename)
stem := strings.TrimSuffix(basename, ext)
for i := 2; i < 100000; i++ {
candidate := fmt.Sprintf("%s.%d%s", stem, i, ext)
if !exists(filepath.Join(filesDir, candidate)) && !exists(filepath.Join(infoDir, candidate+trashInfoExt)) {
return candidate, nil
}
}
return "", errors.New("could not find unique trash name")
}
func exists(p string) bool {
_, err := os.Lstat(p)
return err == nil
}
// pathEncode percent-escapes a POSIX path per RFC 2396, preserving "/".
func pathEncode(p string) string {
parts := strings.Split(p, "/")
for i, seg := range parts {
parts[i] = url.PathEscape(seg)
}
return strings.Join(parts, "/")
}
func pathDecode(p string) string {
if d, err := url.PathUnescape(p); err == nil {
return d
}
return p
}
func writeTrashInfo(infoPath, storedPath string, when time.Time) error {
body := "[Trash Info]\nPath=" + pathEncode(storedPath) +
"\nDeletionDate=" + when.Format("2006-01-02T15:04:05") + "\n"
f, err := os.OpenFile(infoPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(body)
return err
}
// Put trashes a single file or directory.
func Put(path string) (Entry, error) {
abs, err := filepath.Abs(path)
if err != nil {
return Entry{}, err
}
info, err := os.Lstat(abs)
if err != nil {
return Entry{}, err
}
trashDir, storedPath, err := trashDirForPath(abs)
if err != nil {
return Entry{}, err
}
if err := ensureTrashDirs(trashDir); err != nil {
return Entry{}, fmt.Errorf("create trash dir %s: %w", trashDir, err)
}
name, err := uniqueName(trashDir, filepath.Base(abs))
if err != nil {
return Entry{}, err
}
infoPath := filepath.Join(trashDir, "info", name+trashInfoExt)
when := time.Now()
if err := writeTrashInfo(infoPath, storedPath, when); err != nil {
return Entry{}, err
}
target := filepath.Join(trashDir, "files", name)
if err := os.Rename(abs, target); err != nil {
os.Remove(infoPath)
return Entry{}, err
}
return Entry{
Name: name,
OriginalPath: storedPath,
DeletionDate: when.Format("2006-01-02T15:04:05"),
TrashDir: trashDir,
FilesPath: target,
InfoPath: infoPath,
Size: info.Size(),
IsDir: info.IsDir(),
}, nil
}
// allTrashDirs returns the home trash plus every per-mountpoint trash dir
// that exists (and passes the spec's safety checks for $topdir/.Trash).
func allTrashDirs() []string {
var dirs []string
if h, err := homeTrashDir(); err == nil {
dirs = append(dirs, h)
}
uid := strconv.Itoa(os.Getuid())
for _, mount := range readMountPoints() {
shared := filepath.Join(mount, ".Trash")
if isValidSharedTrash(shared) {
candidate := filepath.Join(shared, uid)
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
dirs = append(dirs, candidate)
}
}
candidate := filepath.Join(mount, ".Trash-"+uid)
if info, err := os.Lstat(candidate); err == nil && info.IsDir() && info.Mode()&os.ModeSymlink == 0 {
dirs = append(dirs, candidate)
}
}
return dirs
}
// readMountPoints returns user-visible mount points from /proc/self/mountinfo,
// skipping pseudo and system filesystems.
func readMountPoints() []string {
data, err := os.ReadFile("/proc/self/mountinfo")
if err != nil {
return nil
}
skipPrefixes := []string{"/proc", "/sys", "/dev"}
var out []string
seen := map[string]bool{}
for line := range strings.SplitSeq(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) < 5 {
continue
}
mp := fields[4]
if mp == "/" {
continue
}
skip := false
for _, p := range skipPrefixes {
if mp == p || strings.HasPrefix(mp, p+"/") {
skip = true
break
}
}
if skip || seen[mp] {
continue
}
seen[mp] = true
out = append(out, mp)
}
return out
}
func List() ([]Entry, error) {
var entries []Entry
for _, d := range allTrashDirs() {
es, _ := listOne(d)
entries = append(entries, es...)
}
return entries, nil
}
func listOne(trashDir string) ([]Entry, error) {
infoDir := filepath.Join(trashDir, "info")
filesDir := filepath.Join(trashDir, "files")
dir, err := os.ReadDir(infoDir)
if err != nil {
return nil, err
}
var entries []Entry
for _, ent := range dir {
if !strings.HasSuffix(ent.Name(), trashInfoExt) {
continue
}
name := strings.TrimSuffix(ent.Name(), trashInfoExt)
infoPath := filepath.Join(infoDir, ent.Name())
filesPath := filepath.Join(filesDir, name)
body, err := os.ReadFile(infoPath)
if err != nil {
continue
}
e := Entry{Name: name, TrashDir: trashDir, InfoPath: infoPath, FilesPath: filesPath}
for line := range strings.SplitSeq(string(body), "\n") {
if v, ok := strings.CutPrefix(line, "Path="); ok {
e.OriginalPath = pathDecode(v)
continue
}
if v, ok := strings.CutPrefix(line, "DeletionDate="); ok {
e.DeletionDate = v
}
}
if info, err := os.Lstat(filesPath); err == nil {
e.Size = info.Size()
e.IsDir = info.IsDir()
}
entries = append(entries, e)
}
return entries, nil
}
func Count() (int, error) {
n := 0
for _, d := range allTrashDirs() {
ents, err := os.ReadDir(filepath.Join(d, "info"))
if err != nil {
continue
}
for _, e := range ents {
if strings.HasSuffix(e.Name(), trashInfoExt) {
n++
}
}
}
return n, nil
}
func Empty() error {
var firstErr error
for _, d := range allTrashDirs() {
if err := emptyOne(d); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func emptyOne(trashDir string) error {
var firstErr error
for _, sub := range []string{"files", "info"} {
path := filepath.Join(trashDir, sub)
ents, err := os.ReadDir(path)
if err != nil {
continue
}
for _, e := range ents {
if err := os.RemoveAll(filepath.Join(path, e.Name())); err != nil && firstErr == nil {
firstErr = err
}
}
}
os.Remove(filepath.Join(trashDir, "directorysizes"))
return firstErr
}
// Restore returns a trashed item to its original location.
func Restore(name, trashDir string) error {
if trashDir == "" {
h, err := homeTrashDir()
if err != nil {
return err
}
trashDir = h
}
infoPath := filepath.Join(trashDir, "info", name+trashInfoExt)
filesPath := filepath.Join(trashDir, "files", name)
body, err := os.ReadFile(infoPath)
if err != nil {
return err
}
var stored string
for line := range strings.SplitSeq(string(body), "\n") {
if v, ok := strings.CutPrefix(line, "Path="); ok {
stored = pathDecode(v)
break
}
}
if stored == "" {
return errors.New("invalid .trashinfo: missing Path")
}
target := stored
if !filepath.IsAbs(stored) {
topDir := filepath.Dir(trashDir)
if filepath.Base(topDir) == ".Trash" {
topDir = filepath.Dir(topDir)
}
target = filepath.Join(topDir, stored)
}
if exists(target) {
return fmt.Errorf("restore target already exists: %s", target)
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
if err := os.Rename(filesPath, target); err != nil {
return err
}
os.Remove(infoPath)
return nil
}
-315
View File
@@ -1,315 +0,0 @@
package trash
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func setupHomeTrash(t *testing.T) (homeRoot string, trashDir string) {
t.Helper()
homeRoot = t.TempDir()
xdg := filepath.Join(homeRoot, ".local", "share")
if err := os.MkdirAll(xdg, 0o700); err != nil {
t.Fatalf("mkdir xdg: %v", err)
}
t.Setenv("XDG_DATA_HOME", xdg)
t.Setenv("HOME", homeRoot)
trashDir = filepath.Join(xdg, "Trash")
return homeRoot, trashDir
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func TestPutHomeTrashAbsolutePath(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
src := filepath.Join(homeRoot, "doc.txt")
writeFile(t, src, "hi")
entry, err := Put(src)
if err != nil {
t.Fatalf("Put: %v", err)
}
if entry.Name != "doc.txt" {
t.Errorf("name = %q, want doc.txt", entry.Name)
}
if entry.OriginalPath != src {
t.Errorf("originalPath = %q, want %q", entry.OriginalPath, src)
}
if entry.TrashDir != trashDir {
t.Errorf("trashDir = %q, want %q", entry.TrashDir, trashDir)
}
if _, err := os.Stat(src); !os.IsNotExist(err) {
t.Errorf("source still exists: %v", err)
}
body, err := os.ReadFile(filepath.Join(trashDir, "info", "doc.txt.trashinfo"))
if err != nil {
t.Fatalf("read trashinfo: %v", err)
}
if !strings.HasPrefix(string(body), "[Trash Info]\n") {
t.Errorf("trashinfo missing header: %q", body)
}
if !strings.Contains(string(body), "Path="+src+"\n") {
t.Errorf("Path key missing or wrong: %q", body)
}
if !strings.Contains(string(body), "DeletionDate=") {
t.Errorf("DeletionDate missing: %q", body)
}
}
func TestPutPercentEncodesPath(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
name := "spaces & %.txt"
src := filepath.Join(homeRoot, name)
writeFile(t, src, "x")
if _, err := Put(src); err != nil {
t.Fatalf("Put: %v", err)
}
body, err := os.ReadFile(filepath.Join(trashDir, "info", name+".trashinfo"))
if err != nil {
t.Fatalf("read: %v", err)
}
want := "Path=" + filepath.Dir(src) + "/spaces%20&%20%25.txt"
if !strings.Contains(string(body), want) {
t.Errorf("expected %q in %q", want, body)
}
}
func TestPutCollisionGetsUniqueName(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
for i := range 3 {
src := filepath.Join(homeRoot, "dup.txt")
writeFile(t, src, "x")
if _, err := Put(src); err != nil {
t.Fatalf("Put #%d: %v", i, err)
}
}
want := []string{"dup.txt", "dup.2.txt", "dup.3.txt"}
for _, n := range want {
if _, err := os.Stat(filepath.Join(trashDir, "files", n)); err != nil {
t.Errorf("expected %s in trash: %v", n, err)
}
if _, err := os.Stat(filepath.Join(trashDir, "info", n+".trashinfo")); err != nil {
t.Errorf("expected %s.trashinfo: %v", n, err)
}
}
}
func TestListAndCount(t *testing.T) {
homeRoot, _ := setupHomeTrash(t)
if n, _ := Count(); n != 0 {
t.Errorf("initial count = %d, want 0", n)
}
entries, _ := List()
if len(entries) != 0 {
t.Errorf("initial list len = %d, want 0", len(entries))
}
for _, n := range []string{"a.txt", "b.txt", "c.log"} {
src := filepath.Join(homeRoot, n)
writeFile(t, src, n)
if _, err := Put(src); err != nil {
t.Fatalf("Put %s: %v", n, err)
}
}
got, _ := Count()
if got != 3 {
t.Errorf("count = %d, want 3", got)
}
entries, _ = List()
if len(entries) != 3 {
t.Errorf("list len = %d, want 3", len(entries))
}
for _, e := range entries {
if e.OriginalPath == "" {
t.Errorf("entry %s: empty OriginalPath", e.Name)
}
if _, err := time.Parse("2006-01-02T15:04:05", e.DeletionDate); err != nil {
t.Errorf("entry %s: bad DeletionDate %q: %v", e.Name, e.DeletionDate, err)
}
}
}
func TestEmptyClearsAll(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
for _, n := range []string{"x", "y", "z"} {
src := filepath.Join(homeRoot, n)
writeFile(t, src, n)
if _, err := Put(src); err != nil {
t.Fatalf("Put: %v", err)
}
}
if n, _ := Count(); n != 3 {
t.Fatalf("pre-empty count = %d", n)
}
if err := Empty(); err != nil {
t.Fatalf("Empty: %v", err)
}
if n, _ := Count(); n != 0 {
t.Errorf("post-empty count = %d, want 0", n)
}
for _, sub := range []string{"files", "info"} {
ents, err := os.ReadDir(filepath.Join(trashDir, sub))
if err != nil {
t.Fatalf("readdir %s: %v", sub, err)
}
if len(ents) != 0 {
t.Errorf("%s/ has %d entries, want 0", sub, len(ents))
}
}
}
func TestRestoreToOriginalPath(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
src := filepath.Join(homeRoot, "sub", "dir", "thing.txt")
writeFile(t, src, "payload")
entry, err := Put(src)
if err != nil {
t.Fatalf("Put: %v", err)
}
os.RemoveAll(filepath.Join(homeRoot, "sub"))
if err := Restore(entry.Name, trashDir); err != nil {
t.Fatalf("Restore: %v", err)
}
body, err := os.ReadFile(src)
if err != nil {
t.Fatalf("read restored: %v", err)
}
if string(body) != "payload" {
t.Errorf("restored content = %q, want %q", body, "payload")
}
if _, err := os.Stat(entry.InfoPath); !os.IsNotExist(err) {
t.Errorf("info file still present: %v", err)
}
if _, err := os.Stat(entry.FilesPath); !os.IsNotExist(err) {
t.Errorf("files entry still present: %v", err)
}
}
func TestRestoreRefusesToOverwrite(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
src := filepath.Join(homeRoot, "keep.txt")
writeFile(t, src, "v1")
entry, err := Put(src)
if err != nil {
t.Fatalf("Put: %v", err)
}
writeFile(t, src, "v2-blocking")
err = Restore(entry.Name, trashDir)
if err == nil {
t.Fatalf("expected error on conflicting restore, got nil")
}
if !strings.Contains(err.Error(), "exists") {
t.Errorf("error %q does not mention conflict", err)
}
body, _ := os.ReadFile(src)
if string(body) != "v2-blocking" {
t.Errorf("blocking file altered: %q", body)
}
}
func TestPutDirectory(t *testing.T) {
homeRoot, trashDir := setupHomeTrash(t)
dir := filepath.Join(homeRoot, "myfolder")
writeFile(t, filepath.Join(dir, "child.txt"), "inside")
entry, err := Put(dir)
if err != nil {
t.Fatalf("Put dir: %v", err)
}
if !entry.IsDir {
t.Errorf("IsDir = false, want true")
}
moved := filepath.Join(trashDir, "files", "myfolder", "child.txt")
body, err := os.ReadFile(moved)
if err != nil {
t.Fatalf("read moved child: %v", err)
}
if string(body) != "inside" {
t.Errorf("child content = %q", body)
}
}
func TestIsValidSharedTrashRejectsSymlink(t *testing.T) {
tmp := t.TempDir()
target := filepath.Join(tmp, "real")
if err := os.MkdirAll(target, os.ModeSticky|0o777); err != nil {
t.Fatalf("mkdir target: %v", err)
}
link := filepath.Join(tmp, ".Trash")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}
if isValidSharedTrash(link) {
t.Errorf("symlinked .Trash accepted; spec requires rejection")
}
}
func TestIsValidSharedTrashRequiresStickyBit(t *testing.T) {
tmp := t.TempDir()
dir := filepath.Join(tmp, ".Trash")
if err := os.MkdirAll(dir, 0o777); err != nil {
t.Fatalf("mkdir: %v", err)
}
if isValidSharedTrash(dir) {
t.Errorf(".Trash without sticky bit accepted; spec requires rejection")
}
if err := os.Chmod(dir, os.ModeSticky|0o777); err != nil {
t.Fatalf("chmod: %v", err)
}
if !isValidSharedTrash(dir) {
t.Errorf(".Trash with sticky bit rejected; spec accepts it")
}
}
func TestPathEncodeRoundTrip(t *testing.T) {
cases := []string{
"/home/u/file.txt",
"/path with spaces/and-symbols & %.txt",
"relative/path/é unicode.md",
}
for _, in := range cases {
got := pathDecode(pathEncode(in))
if got != in {
t.Errorf("round-trip %q -> %q", in, got)
}
}
}
-52
View File
@@ -1,52 +0,0 @@
{
self,
pkgs,
...
}:
rec {
all = pkgs.symlinkJoin {
name = "dms-nixos-tests";
paths = [
nixos-module
nixos-service-start-module
greeter-niri-module
niri-home-module
home-manager-module
];
};
nixos-module = import ./nixos-module.nix {
inherit
self
pkgs
;
};
nixos-service-start-module = import ./nixos-service-start-module.nix {
inherit
self
pkgs
;
};
greeter-niri-module = import ./greeter-niri-module.nix {
inherit
self
pkgs
;
};
niri-home-module = import ./niri-home-module.nix {
inherit
self
pkgs
;
};
home-manager-module = import ./home-manager-module.nix {
inherit
self
pkgs
;
};
}
-60
View File
@@ -1,60 +0,0 @@
{
self,
pkgs,
...
}:
pkgs.testers.runNixOSTest {
name = "dms-greeter-niri-module";
nodes.machine = {
imports = [
self.nixosModules.greeter
];
users.groups.greeter = { };
users.users.greeter = {
isSystemUser = true;
group = "greeter";
};
services.greetd.settings.default_session.user = "greeter";
programs.niri.enable = true;
programs.dank-material-shell.greeter = {
enable = true;
compositor.name = "niri";
};
system.stateVersion = "25.11";
};
testScript = ''
import re
machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("greetd.service")
machine.succeed("systemctl is-enabled greetd.service")
machine.succeed("systemctl is-active greetd.service")
greetd_unit = machine.succeed("cat /etc/systemd/system/greetd.service")
config_match = re.search(r'--config (/nix/store[^ ]+-greetd.toml)', greetd_unit)
if config_match is None:
raise AssertionError(greetd_unit)
greetd_config_path = config_match.group(1)
greetd_config = machine.succeed(f"cat {greetd_config_path}")
t.assertIn("dms-greeter", greetd_config)
script_match = re.search(r'command\s*=\s*"([^"]+/bin/dms-greeter)"', greetd_config)
if script_match is None:
raise AssertionError(greetd_config)
script_path = script_match.group(1)
script = machine.succeed(f"cat {script_path}")
t.assertIn("--command", script)
t.assertIn("niri", script)
t.assertIn("/share/quickshell/dms", script)
'';
}
-107
View File
@@ -1,107 +0,0 @@
{
self,
pkgs,
...
}:
let
homeManagerNixosModule =
(fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
})
+ "/nixos";
in
pkgs.testers.runNixOSTest {
name = "dms-home-manager-module";
nodes.machine = {
...
}: {
imports = [
homeManagerNixosModule
];
users.users.danklinux = {
isNormalUser = true;
createHome = true;
home = "/home/danklinux";
extraGroups = [ "wheel" ];
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.danklinux = {
pkgs,
...
}: {
imports = [
self.homeModules.dank-material-shell
];
home.username = "danklinux";
home.homeDirectory = "/home/danklinux";
home.stateVersion = "25.11";
programs.dank-material-shell = {
enable = true;
systemd = {
enable = true;
target = "default.target";
};
settings = {
theme = "integration-test";
};
clipboardSettings = {
maxItems = 10;
};
session = {
startedFrom = "nixos-test";
};
plugins.TestPlugin = {
enable = true;
src = pkgs.runCommand "dms-test-plugin" { } ''
mkdir -p "$out"
echo plugin > "$out/plugin.txt"
'';
settings = {
enabled = true;
source = "test";
};
};
};
};
system.stateVersion = "25.11";
};
testScript = ''
import json
machine.wait_for_unit("multi-user.target")
machine.succeed("su -- danklinux -c 'command -v dms'")
machine.succeed("su -- danklinux -c 'test -f ~/.config/DankMaterialShell/settings.json'")
machine.succeed("su -- danklinux -c 'test -f ~/.config/DankMaterialShell/clsettings.json'")
machine.succeed("su -- danklinux -c 'test -f ~/.config/DankMaterialShell/plugin_settings.json'")
machine.succeed("su -- danklinux -c 'test -e ~/.config/DankMaterialShell/plugins/TestPlugin'")
machine.succeed("su -- danklinux -c 'test -f ~/.local/state/DankMaterialShell/session.json'")
settings = json.loads(machine.succeed("su -- danklinux -c 'cat ~/.config/DankMaterialShell/settings.json'"))
clipboard = json.loads(machine.succeed("su -- danklinux -c 'cat ~/.config/DankMaterialShell/clsettings.json'"))
session = json.loads(machine.succeed("su -- danklinux -c 'cat ~/.local/state/DankMaterialShell/session.json'"))
plugins = json.loads(machine.succeed("su -- danklinux -c 'cat ~/.config/DankMaterialShell/plugin_settings.json'"))
doctor = json.loads(machine.succeed("su -- danklinux -c 'dms doctor --json'"))
t.assertEqual(settings["theme"], "integration-test")
t.assertEqual(clipboard["maxItems"], 10)
t.assertEqual(session["startedFrom"], "nixos-test")
t.assertTrue(plugins["TestPlugin"]["enabled"])
t.assertEqual(plugins["TestPlugin"]["source"], "test")
t.assertIsInstance(doctor.get("results"), list)
'';
}
-84
View File
@@ -1,84 +0,0 @@
{
self,
pkgs,
...
}:
let
homeManagerNixosModule =
(fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
})
+ "/nixos";
niriFlake = builtins.getFlake "github:sodiboo/niri-flake/2bb22af2985e5f3cfd051b3d977ebfbf81126280?narHash=sha256-ooPmu%2B8tqOGh4kozPW4rJC7Y7WM/FHtEY3OK1PoNW7g%3D";
fakeNiri = (pkgs.writeScriptBin "niri" "") // {
cargoBuildNoDefaultFeatures = false;
};
in
pkgs.testers.runNixOSTest {
name = "dms-niri-home-module";
nodes.machine = {
...
}: {
imports = [
homeManagerNixosModule
];
users.users.danklinux = {
isNormalUser = true;
createHome = true;
home = "/home/danklinux";
extraGroups = [ "wheel" ];
};
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
environment.pathsToLink = [
"/share/applications"
"/share/xdg-desktop-portal"
];
home-manager.users.danklinux = {
...
}: {
imports = [
self.homeModules.dank-material-shell
niriFlake.homeModules.niri
self.homeModules.niri
];
home.username = "danklinux";
home.homeDirectory = "/home/danklinux";
home.stateVersion = "25.11";
programs.niri = {
enable = true;
package = fakeNiri; # avoids niri from being compiled in the CI
};
programs.dank-material-shell = {
enable = true;
niri = {
enableKeybinds = false;
enableSpawn = true;
};
};
};
system.stateVersion = "25.11";
};
testScript = ''
machine.wait_for_unit("multi-user.target")
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
'';
}
-47
View File
@@ -1,47 +0,0 @@
{
self,
pkgs,
...
}:
pkgs.testers.runNixOSTest {
name = "dms-nixos-module";
nodes.machine = {
imports = [
self.nixosModules.dank-material-shell
];
users.users.danklinux = {
isNormalUser = true;
extraGroups = [ "wheel" ];
};
programs.dank-material-shell = {
enable = true;
systemd.enable = true;
plugins = {
TestPlugin = {
src = pkgs.emptyDirectory;
};
};
};
system.stateVersion = "25.11";
};
testScript = ''
import json
machine.wait_for_unit("multi-user.target")
machine.succeed("command -v dms")
machine.succeed("command -v quickshell")
machine.succeed("su -- danklinux -c 'dms --help >/dev/null'")
machine.succeed("test -d /etc/xdg/quickshell/dms-plugins")
machine.succeed("test -f /run/current-system/sw/lib/systemd/user/dms.service")
payload = json.loads(machine.succeed("su -- danklinux -c 'dms doctor --json'"))
t.assertIn("summary", payload)
t.assertIsInstance(payload.get("results"), list)
'';
}
@@ -1,48 +0,0 @@
{
self,
pkgs,
...
}:
let
fakeDms = pkgs.writeShellScriptBin "dms" ''
printf '%s\n' "$@" > /tmp/dms-service-args
exec ${pkgs.coreutils}/bin/sleep 300
'';
in
pkgs.testers.runNixOSTest {
name = "dms-nixos-service-start-module";
nodes.machine = {
imports = [
self.nixosModules.dank-material-shell
];
users.users.danklinux = {
isNormalUser = true;
linger = true;
extraGroups = [ "wheel" ];
};
programs.dank-material-shell = {
enable = true;
package = fakeDms;
systemd = {
enable = true;
target = "default.target";
};
};
system.stateVersion = "25.11";
};
testScript = ''
machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("user@1000.service")
machine.succeed("systemctl --machine=danklinux@ --user start dms.service")
machine.wait_until_succeeds("systemctl --machine=danklinux@ --user is-active dms.service")
machine.wait_until_succeeds("test -f /tmp/dms-service-args")
machine.succeed("grep -Fx run /tmp/dms-service-args")
machine.succeed("grep -Fx -- --session /tmp/dms-service-args")
'';
}
Generated
+5 -5
View File
@@ -39,16 +39,16 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776854048, "lastModified": 1766725085,
"narHash": "sha256-lLbV66V3RMNp1l8/UelmR4YzoJ5ONtgvEtiUMJATH/o=", "narHash": "sha256-O2aMFdDUYJazFrlwL7aSIHbUSEm3ADVZjmf41uBJfHs=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "783c953987dc56ff0601abe6845ed96f1d00495a", "rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff",
"revCount": 806, "revCount": 715,
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell" "url": "https://git.outfoxxed.me/quickshell/quickshell"
}, },
"original": { "original": {
"rev": "783c953987dc56ff0601abe6845ed96f1d00495a", "rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff",
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell" "url": "https://git.outfoxxed.me/quickshell/quickshell"
} }
+75 -97
View File
@@ -4,7 +4,7 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
quickshell = { quickshell = {
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=783c953987dc56ff0601abe6845ed96f1d00495a"; url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=41828c4180fb921df7992a5405f5ff05d2ac2fff";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
flake-compat = { flake-compat = {
@@ -45,12 +45,10 @@
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] ( nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
system: fn system nixpkgs.legacyPackages.${system} system: fn system nixpkgs.legacyPackages.${system}
); );
forEachLinuxSystem = buildDmsPkgs = pkgs: {
fn: dms-shell = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
nixpkgs.lib.genAttrs [ "aarch64-linux" "x86_64-linux" ] ( quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
system: fn system nixpkgs.legacyPackages.${system} };
);
mkModuleWithDmsPkgs = mkModuleWithDmsPkgs =
modulePath: modulePath:
args@{ pkgs, ... }: args@{ pkgs, ... }:
@@ -59,7 +57,6 @@
(import modulePath (args // { dmsPkgs = buildDmsPkgs pkgs; })) (import modulePath (args // { dmsPkgs = buildDmsPkgs pkgs; }))
]; ];
}; };
mkQmlImportPath = mkQmlImportPath =
pkgs: qmlPkgs: pkgs: qmlPkgs:
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs); pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs);
@@ -76,11 +73,10 @@
qtimageformats qtimageformats
kimageformats kimageformats
]; ];
in
# Allows downstream modules to provide their own 'pkgs' (with overlays) {
# instead of being forced to use the flake's locked nixpkgs. packages = forEachSystem (
mkDmsShell = system: pkgs:
pkgs:
let let
mkDate = mkDate =
longDate: longDate:
@@ -98,96 +94,89 @@
in in
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
pkgs.lib.makeOverridable ( {
{ dms-shell = pkgs.lib.makeOverridable (
extraQtPackages ? [ ],
}:
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{ {
inherit version; extraQtPackages ? [ ],
pname = "dms-shell"; }:
src = ./core; (pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
vendorHash = "sha256-kPu3MLqhLaCaBpCwIP8JXep0J/Z45kxDFOEY8JvcWdU="; let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X 'main.Version=${version}'" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''
mkdir -p $out/share/quickshell/dms mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/ cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon # Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \ install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop $out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \ install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg $out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \ --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}" --prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \ substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \ substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \
--replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.so --replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh) --zsh <($out/bin/dms completion zsh)
''; '';
meta = { meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO"; description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com"; homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}"; changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
mainProgram = "dms"; mainProgram = "dms";
platforms = pkgs.lib.platforms.linux; platforms = pkgs.lib.platforms.linux;
}; };
} }
) )
) { }; ) { };
buildDmsPkgs = pkgs: {
dms-shell = mkDmsShell pkgs;
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
in
{
packages = forEachSystem (
system: pkgs: {
dms-shell = mkDmsShell pkgs;
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;
default = self.packages.${system}.dms-shell; default = self.packages.${system}.dms-shell;
} }
); );
@@ -251,16 +240,5 @@
}; };
} }
); );
nixosTests = forEachLinuxSystem (
system: pkgs:
import ./distro/nix/tests {
inherit
self
pkgs
;
lib = pkgs.lib;
}
);
}; };
} }
+1 -1
View File
@@ -55,7 +55,7 @@ Singleton {
readonly property bool isDirectionalEffect: isConnectedEffect || _effect === 1 readonly property bool isDirectionalEffect: isConnectedEffect || _effect === 1
readonly property bool isDepthEffect: _effect === 2 readonly property bool isDepthEffect: _effect === 2
readonly property bool isConnectedEffect: (typeof SettingsData !== "undefined") && SettingsData.connectedFrameModeActive readonly property bool isConnectedEffect: (typeof SettingsData !== "undefined") && SettingsData.frameEnabled && _effect === 1 && SettingsData.directionalAnimationMode === 3
readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96 readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96
readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16 readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16
+6 -8
View File
@@ -5,11 +5,9 @@ import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("CacheData")
readonly property int cacheConfigVersion: 1 readonly property int cacheConfigVersion: 1
@@ -133,7 +131,7 @@ Singleton {
} }
} }
} catch (e) { } catch (e) {
log.warn("Failed to parse cache:", e.message); console.warn("CacheData: Failed to parse cache:", e.message);
} finally { } finally {
_loading = false; _loading = false;
} }
@@ -151,7 +149,7 @@ Singleton {
} }
function migrateFromUndefinedToV1(cache) { function migrateFromUndefinedToV1(cache) {
log.info("Migrating configuration from undefined to version 1"); console.info("CacheData: Migrating configuration from undefined to version 1");
} }
function cleanupUnusedKeys() { function cleanupUnusedKeys() {
@@ -166,7 +164,7 @@ Singleton {
for (const key in cache) { for (const key in cache) {
if (!validKeys.includes(key)) { if (!validKeys.includes(key)) {
log.debug("Removing unused key:", key); console.log("CacheData: Removing unused key:", key);
delete cache[key]; delete cache[key];
needsSave = true; needsSave = true;
} }
@@ -176,7 +174,7 @@ Singleton {
cacheFile.setText(JSON.stringify(cache, null, 2)); cacheFile.setText(JSON.stringify(cache, null, 2));
} }
} catch (e) { } catch (e) {
log.warn("Failed to cleanup unused keys:", e.message); console.warn("CacheData: Failed to cleanup unused keys:", e.message);
} }
} }
@@ -186,7 +184,7 @@ Singleton {
if (content && content.trim()) if (content && content.trim())
return JSON.parse(content); return JSON.parse(content);
} catch (e) { } catch (e) {
log.warn("Failed to parse launcher cache:", e.message); console.warn("CacheData: Failed to parse launcher cache:", e.message);
} }
return null; return null;
} }
@@ -222,7 +220,7 @@ Singleton {
} }
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) { if (!isGreeterMode) {
log.info("No cache file found, starting fresh"); console.info("CacheData: No cache file found, starting fresh");
} }
} }
} }
+7 -78
View File
@@ -161,22 +161,12 @@ Singleton {
}; };
} }
function _sameDockState(a, b) {
if (!a || !b)
return false;
return a.reveal === b.reveal && a.barSide === b.barSide && Math.abs(a.bodyX - b.bodyX) < 0.5 && Math.abs(a.bodyY - b.bodyY) < 0.5 && Math.abs(a.bodyW - b.bodyW) < 0.5 && Math.abs(a.bodyH - b.bodyH) < 0.5 && Math.abs(a.slideX - b.slideX) < 0.5 && Math.abs(a.slideY - b.slideY) < 0.5;
}
function setDockState(screenName, state) { function setDockState(screenName, state) {
if (!screenName || !state) if (!screenName || !state)
return false; return false;
const normalized = _normalizeDockState(state);
if (_sameDockState(dockStates[screenName], normalized))
return true;
const next = _cloneDict(dockStates); const next = _cloneDict(dockStates);
next[screenName] = normalized; next[screenName] = _normalizeDockState(state);
dockStates = next; dockStates = next;
return true; return true;
} }
@@ -201,20 +191,17 @@ Singleton {
function setDockSlide(screenName, x, y) { function setDockSlide(screenName, x, y) {
if (!screenName) if (!screenName)
return false; return false;
const numX = Number(x);
const numY = Number(y);
const cur = dockSlides[screenName];
if (cur && Math.abs(cur.x - numX) < 0.5 && Math.abs(cur.y - numY) < 0.5)
return true;
const next = _cloneDict(dockSlides); const next = _cloneDict(dockSlides);
next[screenName] = { next[screenName] = {
"x": numX, "x": Number(x),
"y": numY "y": Number(y)
}; };
dockSlides = next; dockSlides = next;
return true; return true;
} }
// Notification state (per screen, updated by NotificationSurface)
readonly property var emptyNotificationState: ({ readonly property var emptyNotificationState: ({
"visible": false, "visible": false,
"barSide": "top", "barSide": "top",
@@ -382,6 +369,8 @@ Singleton {
return true; return true;
} }
// Dock retract coordination
property var dockRetractRequests: ({}) property var dockRetractRequests: ({})
function requestDockRetract(requesterId, screenName, side) { function requestDockRetract(requesterId, screenName, side) {
@@ -418,64 +407,4 @@ Singleton {
} }
return false; return false;
} }
// Prune state for screens that are no longer connected. Stale entries
// accumulate across hotplug cycles otherwise Frame's per-screen
// FrameInstance doesn't notice when its peer dicts go orphan.
function _pruneToLiveScreens() {
const live = {};
const screens = Quickshell.screens || [];
for (let i = 0; i < screens.length; i++) {
const s = screens[i];
if (s && s.name)
live[s.name] = true;
}
function pruneKeyed(dict) {
let changed = false;
const next = {};
for (const k in dict) {
if (live[k])
next[k] = dict[k];
else
changed = true;
}
return changed ? next : null;
}
const nextDock = pruneKeyed(dockStates);
if (nextDock !== null)
dockStates = nextDock;
const nextSlides = pruneKeyed(dockSlides);
if (nextSlides !== null)
dockSlides = nextSlides;
const nextNotif = pruneKeyed(notificationStates);
if (nextNotif !== null)
notificationStates = nextNotif;
const nextModal = pruneKeyed(modalStates);
if (nextModal !== null)
modalStates = nextModal;
let retractChanged = false;
const nextRetract = {};
for (const k in dockRetractRequests) {
const r = dockRetractRequests[k];
if (r && live[r.screenName])
nextRetract[k] = r;
else
retractChanged = true;
}
if (retractChanged)
dockRetractRequests = nextRetract;
if (popoutOwnerId && popoutScreen && !live[popoutScreen])
releasePopout(popoutOwnerId);
}
Connections {
target: Quickshell
function onScreensChanged() {
root._pruneToLiveScreens();
}
}
} }
+5 -7
View File
@@ -5,11 +5,9 @@ import QtQuick
import Qt.labs.folderlistmodel import Qt.labs.folderlistmodel
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("I18n")
property string _resolvedLocale: "en" property string _resolvedLocale: "en"
@@ -56,15 +54,15 @@ Singleton {
try { try {
root.translations = JSON.parse(text()); root.translations = JSON.parse(text());
root.translationsLoaded = true; root.translationsLoaded = true;
log.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`); console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
} catch (e) { } catch (e) {
log.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English"); console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
root._fallbackToEnglish(); root._fallbackToEnglish();
} }
} }
onLoadFailed: error => { onLoadFailed: error => {
log.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English"); console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
root._fallbackToEnglish(); root._fallbackToEnglish();
} }
} }
@@ -107,14 +105,14 @@ Singleton {
_selectedPath = fileUrl; _selectedPath = fileUrl;
translationsLoaded = false; translationsLoaded = false;
translations = ({}); translations = ({});
log.info(`I18n: Using locale '${localeTag}' from ${fileUrl}`); console.info(`I18n: Using locale '${localeTag}' from ${fileUrl}`);
} }
function _fallbackToEnglish() { function _fallbackToEnglish() {
_selectedPath = ""; _selectedPath = "";
translationsLoaded = false; translationsLoaded = false;
translations = ({}); translations = ({});
log.warn("Falling back to built-in English strings"); console.warn("I18n: Falling back to built-in English strings");
} }
function tr(term, context) { function tr(term, context) {
+4 -10
View File
@@ -161,16 +161,10 @@ const NIRI_ACTIONS = {
{ id: "focus-monitor-right", label: "Focus Monitor Right" }, { id: "focus-monitor-right", label: "Focus Monitor Right" },
{ id: "focus-monitor-down", label: "Focus Monitor Down" }, { id: "focus-monitor-down", label: "Focus Monitor Down" },
{ id: "focus-monitor-up", label: "Focus Monitor Up" }, { id: "focus-monitor-up", label: "Focus Monitor Up" },
{ id: "move-column-to-monitor-left", label: "Move Column to Monitor Left" }, { id: "move-column-to-monitor-left", label: "Move to Monitor Left" },
{ id: "move-column-to-monitor-right", label: "Move Column to Monitor Right" }, { id: "move-column-to-monitor-right", label: "Move to Monitor Right" },
{ id: "move-column-to-monitor-down", label: "Move Column to Monitor Down" }, { id: "move-column-to-monitor-down", label: "Move to Monitor Down" },
{ id: "move-column-to-monitor-up", label: "Move Column to Monitor Up" }, { id: "move-column-to-monitor-up", label: "Move to Monitor Up" }
{ id: "move-workspace-to-monitor-left", label: "Move Workspace to Monitor Left" },
{ id: "move-workspace-to-monitor-right", label: "Move Workspace to Monitor Right" },
{ id: "move-workspace-to-monitor-down", label: "Move Workspace to Monitor Down" },
{ id: "move-workspace-to-monitor-up", label: "Move Workspace to Monitor Up" },
{ id: "move-workspace-to-monitor-next", label: "Move Workspace to Next Monitor" },
{ id: "move-workspace-to-monitor-previous", label: "Move Workspace to Previous Monitor" }
], ],
"Screenshot": [ "Screenshot": [
{ id: "screenshot", label: "Screenshot (Interactive)" }, { id: "screenshot", label: "Screenshot (Interactive)" },
+1 -3
View File
@@ -3,11 +3,9 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("Proc")
readonly property int noTimeout: -1 readonly property int noTimeout: -1
property int defaultDebounceMs: 50 property int defaultDebounceMs: 50
@@ -114,7 +112,7 @@ Singleton {
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1; const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
entry.callback(safeOutput, safeExitCode); entry.callback(safeOutput, safeExitCode);
} catch (e) { } catch (e) {
log.warn("runCommand callback error for command:", entry.command, "Error:", e); console.warn("runCommand callback error for command:", entry.command, "Error:", e);
} }
} }
try { try {
+7 -35
View File
@@ -12,7 +12,6 @@ import "settings/SessionStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("SessionData")
readonly property int sessionConfigVersion: 3 readonly property int sessionConfigVersion: 3
@@ -31,36 +30,9 @@ Singleton {
property bool isLightMode: false property bool isLightMode: false
property bool doNotDisturb: false property bool doNotDisturb: false
property real doNotDisturbUntil: 0 property real doNotDisturbUntil: 0
property string terminalOverride: ""
property bool isSwitchingMode: false property bool isSwitchingMode: false
property bool suppressOSD: true property bool suppressOSD: true
readonly property var terminalOptions: ["ghostty", "kitty", "foot", "alacritty", "wezterm", "konsole", "gnome-terminal", "xterm"]
property var installedTerminals: []
function resolveTerminal() {
if (terminalOverride && terminalOverride.length > 0) {
return terminalOverride;
}
const env = Quickshell.env("TERMINAL");
if (env && env.length > 0) {
return env;
}
return "";
}
Process {
id: terminalProbe
running: true
command: ["sh", "-c", "for t in ghostty kitty foot alacritty wezterm konsole gnome-terminal xterm; do command -v \"$t\" >/dev/null 2>&1 && echo \"$t\"; done"]
stdout: StdioCollector {
onStreamFinished: {
const found = text.trim().split("\n").filter(line => line.length > 0);
root.installedTerminals = found;
}
}
}
Timer { Timer {
id: dndExpireTimer id: dndExpireTimer
repeat: false repeat: false
@@ -258,7 +230,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to parse session.json - file will not be overwritten."); console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
} }
} }
@@ -338,7 +310,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to parse session.json - file will not be overwritten."); console.error("SessionData: Failed to parse session.json - file will not be overwritten.");
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
} }
} }
@@ -553,7 +525,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
log.warn("Screen not found"); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -650,7 +622,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
log.warn("Screen not found"); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -681,7 +653,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
log.warn("Screen not found"); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -712,7 +684,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
log.warn("Screen not found"); console.warn("SessionData: Screen not found");
return; return;
} }
@@ -743,7 +715,7 @@ Singleton {
} }
if (!screen) { if (!screen) {
log.warn("Screen not found"); console.warn("SessionData: Screen not found");
return; return;
} }
+15 -32
View File
@@ -13,7 +13,6 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 11 readonly property int settingsConfigVersion: 11
@@ -185,6 +184,8 @@ Singleton {
onAnimationVariantChanged: saveSettings() onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings() onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings() onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12 property int m3ElevationIntensity: 12
@@ -206,15 +207,11 @@ Singleton {
property bool blurEnabled: false property bool blurEnabled: false
onBlurEnabledChanged: saveSettings() onBlurEnabledChanged: saveSettings()
property bool blurForegroundLayers: true
onBlurForegroundLayersChanged: saveSettings()
property real blurLayerOutlineOpacity: 0.12
onBlurLayerOutlineOpacityChanged: saveSettings()
property string blurBorderColor: "outline" property string blurBorderColor: "outline"
onBlurBorderColorChanged: saveSettings() onBlurBorderColorChanged: saveSettings()
property string blurBorderCustomColor: "#ffffff" property string blurBorderCustomColor: "#ffffff"
onBlurBorderCustomColorChanged: saveSettings() onBlurBorderCustomColorChanged: saveSettings()
property real blurBorderOpacity: 0.35 property real blurBorderOpacity: 1.0
onBlurBorderOpacityChanged: saveSettings() onBlurBorderOpacityChanged: saveSettings()
property string wallpaperFillMode: "Fill" property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
@@ -238,18 +235,16 @@ Singleton {
onFrameShowOnOverviewChanged: saveSettings() onFrameShowOnOverviewChanged: saveSettings()
property bool frameBlurEnabled: true property bool frameBlurEnabled: true
onFrameBlurEnabledChanged: saveSettings() onFrameBlurEnabledChanged: saveSettings()
property bool frameCloseGaps: true property bool frameCloseGaps: false
onFrameCloseGapsChanged: saveSettings() onFrameCloseGapsChanged: saveSettings()
property string frameLauncherEmergeSide: "bottom" property string frameLauncherEmergeSide: "bottom"
onFrameLauncherEmergeSideChanged: saveSettings() onFrameLauncherEmergeSideChanged: saveSettings()
property bool frameLauncherArcExtender: false
onFrameLauncherArcExtenderChanged: saveSettings()
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
property string frameMode: "separate" property int previousDirectionalMode: 1
onFrameModeChanged: saveSettings() onPreviousDirectionalModeChanged: saveSettings()
property var connectedFrameBarStyleBackups: ({}) property var connectedFrameBarStyleBackups: ({})
onConnectedFrameBarStyleBackupsChanged: saveSettings() onConnectedFrameBarStyleBackupsChanged: saveSettings()
readonly property bool connectedFrameModeActive: frameEnabled && frameMode === "connected" readonly property bool connectedFrameModeActive: frameEnabled && motionEffect === SettingsData.AnimationEffect.Directional && directionalAnimationMode === 3
onConnectedFrameModeActiveChanged: { onConnectedFrameModeActiveChanged: {
if (_loading) if (_loading)
return; return;
@@ -280,9 +275,7 @@ Singleton {
property int selectedGpuIndex: 0 property int selectedGpuIndex: 0
property var enabledGpuPciIds: [] property var enabledGpuPciIds: []
property bool showSystemTray: true property bool showSystemTray: true
property string systemTrayIconTintMode: "none" property bool systemTrayMonochromeIcons: false
property int systemTrayIconTintSaturation: 50
property int systemTrayIconTintStrength: 135
property bool showClock: true property bool showClock: true
property bool showNotificationButton: true property bool showNotificationButton: true
property bool showBattery: true property bool showBattery: true
@@ -558,7 +551,6 @@ Singleton {
property bool matugenTemplatePywalfox: true property bool matugenTemplatePywalfox: true
property bool matugenTemplateZenBrowser: true property bool matugenTemplateZenBrowser: true
property bool matugenTemplateVesktop: true property bool matugenTemplateVesktop: true
property bool matugenTemplateVencord: true
property bool matugenTemplateEquibop: true property bool matugenTemplateEquibop: true
property bool matugenTemplateGhostty: true property bool matugenTemplateGhostty: true
property bool matugenTemplateKitty: true property bool matugenTemplateKitty: true
@@ -611,9 +603,6 @@ Singleton {
property int dockMaxVisibleApps: 0 property int dockMaxVisibleApps: 0
property int dockMaxVisibleRunningApps: 0 property int dockMaxVisibleRunningApps: 0
property bool dockShowOverflowBadge: true property bool dockShowOverflowBadge: true
property bool dockShowTrash: false
property string dockTrashFileManager: "default"
property string dockTrashCustomCommand: ""
property bool notificationOverlayEnabled: false property bool notificationOverlayEnabled: false
property bool notificationPopupShadowEnabled: true property bool notificationPopupShadowEnabled: true
@@ -706,9 +695,6 @@ Singleton {
property bool updaterUseCustomCommand: false property bool updaterUseCustomCommand: false
property string updaterCustomCommand: "" property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: "" property string updaterTerminalAdditionalParams: ""
property int updaterIntervalSeconds: 1800
property bool updaterIncludeFlatpak: true
property bool updaterAllowAUR: true
property string displayNameMode: "system" property string displayNameMode: "system"
property var screenPreferences: ({}) property var screenPreferences: ({})
@@ -1340,9 +1326,6 @@ Singleton {
Store.parse(root, obj); Store.parse(root, obj);
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
frameMode = "connected";
if (obj?.weatherLocation !== undefined) if (obj?.weatherLocation !== undefined)
_legacyWeatherLocation = obj.weatherLocation; _legacyWeatherLocation = obj.weatherLocation;
if (obj?.weatherCoordinates !== undefined) if (obj?.weatherCoordinates !== undefined)
@@ -1363,7 +1346,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to parse settings.json - file will not be overwritten. Error:", msg); console.error("SettingsData: Failed to parse settings.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
applyStoredTheme(); applyStoredTheme();
} finally { } finally {
@@ -1385,12 +1368,12 @@ Singleton {
if (_isReadOnly) { if (_isReadOnly) {
_hasUnsavedChanges = _checkForUnsavedChanges(); _hasUnsavedChanges = _checkForUnsavedChanges();
if (!wasReadOnly) if (!wasReadOnly)
log.info("settings.json is now read-only"); console.info("SettingsData: settings.json is now read-only");
} else { } else {
_loadedSettingsSnapshot = JSON.stringify(Store.toJson(root)); _loadedSettingsSnapshot = JSON.stringify(Store.toJson(root));
_hasUnsavedChanges = false; _hasUnsavedChanges = false;
if (wasReadOnly) if (wasReadOnly)
log.info("settings.json is now writable"); console.info("SettingsData: settings.json is now writable");
if (_pendingMigration) if (_pendingMigration)
settingsFile.setText(JSON.stringify(_pendingMigration, null, 2)); settingsFile.setText(JSON.stringify(_pendingMigration, null, 2));
} }
@@ -1444,7 +1427,7 @@ Singleton {
} catch (e) { } catch (e) {
const msg = e.message || String(e); const msg = e.message || String(e);
if (!_isMissingPluginSettingsError(e)) if (!_isMissingPluginSettingsError(e))
log.warn("Failed to load plugin_settings.json. Error:", msg); console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings(); _resetPluginSettings();
} }
} }
@@ -1461,7 +1444,7 @@ Singleton {
} catch (e) { } catch (e) {
_pluginParseError = true; _pluginParseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to parse plugin_settings.json - file will not be overwritten. Error:", msg); console.error("SettingsData: Failed to parse plugin_settings.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse plugin_settings.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse plugin_settings.json"), msg));
pluginSettings = {}; pluginSettings = {};
} finally { } finally {
@@ -3099,7 +3082,7 @@ Singleton {
} catch (e) { } catch (e) {
_parseError = true; _parseError = true;
const msg = e.message; const msg = e.message;
log.error("Failed to reload settings.json - file will not be overwritten. Error:", msg); console.error("SettingsData: Failed to reload settings.json - file will not be overwritten. Error:", msg);
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg)); Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
} finally { } finally {
_loading = false; _loading = false;
@@ -3134,7 +3117,7 @@ Singleton {
if (!isGreeterMode) { if (!isGreeterMode) {
const msg = String(error || ""); const msg = String(error || "");
if (!_isMissingPluginSettingsError(error)) if (!_isMissingPluginSettingsError(error))
log.warn("Failed to load plugin_settings.json. Error:", msg); console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings(); _resetPluginSettings();
} }
} }
+31 -50
View File
@@ -12,7 +12,6 @@ import "StockThemes.js" as StockThemes
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("Theme")
readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell" readonly property string stateDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericCacheLocation).toString()) + "/DankMaterialShell"
readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true" readonly property bool envDisableMatugen: Quickshell.env("DMS_DISABLE_MATUGEN") === "1" || Quickshell.env("DMS_DISABLE_MATUGEN") === "true"
@@ -149,7 +148,7 @@ Singleton {
} }
if (colorsFileLoadFailed && currentTheme === dynamic && rawWallpaperPath) { if (colorsFileLoadFailed && currentTheme === dynamic && rawWallpaperPath) {
log.info("Matugen now available, regenerating colors for dynamic theme"); console.info("Theme: Matugen now available, regenerating colors for dynamic theme");
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode); const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode);
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"; const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot"; const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
@@ -377,7 +376,7 @@ Singleton {
"use": true "use": true
}, response => { }, response => {
if (!response.error) { if (!response.error) {
log.info("Theme automation: IP location enabled after connection"); console.info("Theme automation: IP location enabled after connection");
} }
}); });
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { } else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
@@ -390,13 +389,13 @@ Singleton {
"longitude": SessionData.longitude "longitude": SessionData.longitude
}, locationResponse => { }, locationResponse => {
if (locationResponse?.error) { if (locationResponse?.error) {
log.warn("Theme automation: Failed to set location", locationResponse.error); console.warn("Theme automation: Failed to set location", locationResponse.error);
} }
}); });
} }
}); });
} else { } else {
log.warn("Theme automation: No location configured"); console.warn("Theme automation: No location configured");
} }
} }
} }
@@ -550,8 +549,8 @@ Singleton {
property color success: currentThemeData.success || "#4CAF50" property color success: currentThemeData.success || "#4CAF50"
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12) property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.12 : 0.08) property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.24 : 0.16) property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3) property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04) property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
@@ -560,28 +559,17 @@ Singleton {
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08) property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12) property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15) property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, transparentBlurLayers ? 0.3 : 0.1) property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2) property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
readonly property bool blurForegroundLayers: BlurService.enabled && (typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true))
readonly property bool transparentBlurLayers: BlurService.enabled && !blurForegroundLayers
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
readonly property color floatingSurface: transparentBlurLayers ? "transparent" : readableSurface
readonly property color floatingSurfaceHigh: transparentBlurLayers ? "transparent" : readableSurfaceHigh
readonly property color nestedSurface: floatingSurfaceHigh
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
readonly property real layerOutlineOpacity: BlurService.enabled ? blurLayerOutlineOpacity : 0.08
readonly property int layerOutlineWidth: BlurService.enabled && layerOutlineOpacity > 0 ? 1 : 0
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08) property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3) property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06) property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7) property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5) property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 0.625) : 0.05) property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, layerOutlineOpacity) property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 1.5) : 0.12) property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12) property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16) property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
@@ -599,12 +587,6 @@ Singleton {
} }
} }
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (blurForegroundLayers ? withAlpha(surfaceContainerHigh, Math.min(popupTransparency, 0.24)) : withAlpha(surfaceContainer, popupTransparency))
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
readonly property real ccSliderTrackOpacity: transparentBlurLayers ? 0.18 : popupTransparency
readonly property color ccTileActiveText: { readonly property color ccTileActiveText: {
switch (SettingsData.controlCenterTileColorMode) { switch (SettingsData.controlCenterTileColorMode) {
case "primaryContainer": case "primaryContainer":
@@ -986,6 +968,7 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
} }
// Animation variant proxy
// Theme is the canonical access point for animation variant state. The // Theme is the canonical access point for animation variant state. The
// aliases below forward to AnimVariants.qml so consumers don't need two // aliases below forward to AnimVariants.qml so consumers don't need two
// imports. ~200 call sites read through Theme.variantEnterCurve / // imports. ~200 call sites read through Theme.variantEnterCurve /
@@ -1575,12 +1558,12 @@ Singleton {
function setDesiredTheme(kind, value, isLight, iconTheme, matugenType, stockColors) { function setDesiredTheme(kind, value, isLight, iconTheme, matugenType, stockColors) {
if (!matugenAvailable) { if (!matugenAvailable) {
log.warn("matugen not available or disabled - cannot set system theme"); console.warn("Theme: matugen not available or disabled - cannot set system theme");
return; return;
} }
if (workerRunning) { if (workerRunning) {
log.info("Worker already running, queueing request"); console.info("Theme: Worker already running, queueing request");
pendingThemeRequest = { pendingThemeRequest = {
kind, kind,
value, value,
@@ -1592,7 +1575,7 @@ Singleton {
return; return;
} }
log.info("Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)"); console.info("Theme: Setting desired theme -", kind, "mode:", isLight ? "light" : "dark", stockColors ? "(stock colors)" : "(dynamic)");
if (typeof NiriService !== "undefined" && CompositorService.isNiri) { if (typeof NiriService !== "undefined" && CompositorService.isNiri) {
NiriService.suppressNextToast(); NiriService.suppressNextToast();
@@ -1607,7 +1590,7 @@ Singleton {
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true "runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
}; };
log.debug("Starting matugen worker"); console.log("Theme: Starting matugen worker");
workerRunning = true; workerRunning = true;
const args = ["dms", "matugen", "queue", "--state-dir", stateDir, "--shell-dir", shellDir, "--config-dir", configDir, "--kind", desired.kind, "--value", desired.value, "--mode", desired.mode, "--icon-theme", desired.iconTheme, "--matugen-type", desired.matugenType,]; const args = ["dms", "matugen", "queue", "--state-dir", stateDir, "--shell-dir", shellDir, "--config-dir", configDir, "--kind", desired.kind, "--value", desired.value, "--mode", desired.mode, "--icon-theme", desired.iconTheme, "--matugen-type", desired.matugenType,];
@@ -1631,7 +1614,7 @@ Singleton {
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "vencord", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");
@@ -1653,8 +1636,6 @@ Singleton {
skipTemplates.push("zenbrowser"); skipTemplates.push("zenbrowser");
if (!SettingsData.matugenTemplateVesktop) if (!SettingsData.matugenTemplateVesktop)
skipTemplates.push("vesktop"); skipTemplates.push("vesktop");
if (!SettingsData.matugenTemplateVencord)
skipTemplates.push("vencord");
if (!SettingsData.matugenTemplateEquibop) if (!SettingsData.matugenTemplateEquibop)
skipTemplates.push("equibop"); skipTemplates.push("equibop");
if (!SettingsData.matugenTemplateGhostty) if (!SettingsData.matugenTemplateGhostty)
@@ -1767,7 +1748,7 @@ Singleton {
} }
if (!darkTheme || !darkTheme.primary) { if (!darkTheme || !darkTheme.primary) {
log.warn("Theme data not available for:", currentTheme); console.warn("Theme data not available for:", currentTheme);
return; return;
} }
@@ -2011,10 +1992,10 @@ Singleton {
id: systemThemeGenerator id: systemThemeGenerator
running: false running: false
stdout: SplitParser { stdout: SplitParser {
onRead: data => log.info("Theme worker:", data) onRead: data => console.info("Theme worker:", data)
} }
stderr: SplitParser { stderr: SplitParser {
onRead: data => log.warn("Theme worker:", data) onRead: data => console.warn("Theme worker:", data)
} }
onExited: exitCode => { onExited: exitCode => {
@@ -2023,18 +2004,18 @@ Singleton {
switch (exitCode) { switch (exitCode) {
case 0: case 0:
log.info("Matugen worker completed successfully"); console.info("Theme: Matugen worker completed successfully");
root.matugenCompleted(currentMode, "success"); root.matugenCompleted(currentMode, "success");
break; break;
case 2: case 2:
log.debug("Matugen worker completed with code 2 (no changes needed)"); console.log("Theme: Matugen worker completed with code 2 (no changes needed)");
root.matugenCompleted(currentMode, "no-changes"); root.matugenCompleted(currentMode, "no-changes");
break; break;
default: default:
if (typeof ToastService !== "undefined") { if (typeof ToastService !== "undefined") {
ToastService.showError("Theme worker failed (" + exitCode + ")"); ToastService.showError("Theme worker failed (" + exitCode + ")");
} }
log.warn("Matugen worker failed with exit code:", exitCode); console.warn("Theme: Matugen worker failed with exit code:", exitCode);
root.matugenCompleted(currentMode, "error"); root.matugenCompleted(currentMode, "error");
} }
@@ -2043,7 +2024,7 @@ Singleton {
const req = pendingThemeRequest; const req = pendingThemeRequest;
pendingThemeRequest = null; pendingThemeRequest = null;
log.info("Processing queued theme request"); console.info("Theme: Processing queued theme request");
setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors); setDesiredTheme(req.kind, req.value, req.isLight, req.iconTheme, req.matugenType, req.stockColors);
} }
} }
@@ -2097,7 +2078,7 @@ Singleton {
} }
} }
} catch (e) { } catch (e) {
log.error("Failed to parse dynamic colors:", e); console.error("Theme: Failed to parse dynamic colors:", e);
if (typeof ToastService !== "undefined") { if (typeof ToastService !== "undefined") {
ToastService.wallpaperErrorStatus = "error"; ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Dynamic colors parse error: " + e.message); ToastService.showError("Dynamic colors parse error: " + e.message);
@@ -2117,11 +2098,11 @@ Singleton {
onLoadFailed: function (error) { onLoadFailed: function (error) {
if (currentTheme === dynamic) { if (currentTheme === dynamic) {
log.warn("Dynamic colors file load failed, marking for regeneration"); console.warn("Theme: Dynamic colors file load failed, marking for regeneration");
colorsFileLoadFailed = true; colorsFileLoadFailed = true;
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
if (!isGreeterMode && matugenAvailable && rawWallpaperPath) { if (!isGreeterMode && matugenAvailable && rawWallpaperPath) {
log.debug("Matugen available, triggering immediate regeneration"); console.log("Theme: Matugen available, triggering immediate regeneration");
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
} }
@@ -2245,7 +2226,7 @@ Singleton {
"endMinute": endMinute "endMinute": endMinute
}, response => { }, response => {
if (response && response.error) { if (response && response.error) {
log.error("Theme automation: Failed to sync time schedule:", response.error); console.error("Theme automation: Failed to sync time schedule:", response.error);
} }
}); });
@@ -2338,9 +2319,9 @@ Singleton {
if (root.themeModeAutomationActive) { if (root.themeModeAutomationActive) {
if (SessionData.nightModeUseIPLocation) { if (SessionData.nightModeUseIPLocation) {
log.warn("Theme automation: Waiting for IP location from backend"); console.warn("Theme automation: Waiting for IP location from backend");
} else { } else {
log.warn("Theme automation: Location mode requires coordinates"); console.warn("Theme automation: Location mode requires coordinates");
} }
} }
} }
@@ -2422,7 +2403,7 @@ Singleton {
"use": true "use": true
}, response => { }, response => {
if (response?.error) { if (response?.error) {
log.warn("Theme automation: Failed to enable IP location", response.error); console.warn("Theme automation: Failed to enable IP location", response.error);
} }
}); });
return true; return true;
@@ -2436,7 +2417,7 @@ Singleton {
"longitude": SessionData.longitude "longitude": SessionData.longitude
}, locResp => { }, locResp => {
if (locResp?.error) { if (locResp?.error) {
log.warn("Theme automation: Failed to set location", locResp.error); console.warn("Theme automation: Failed to set location", locResp.error);
} }
}); });
} }
@@ -4,7 +4,6 @@ var SPEC = {
isLightMode: { def: false }, isLightMode: { def: false },
doNotDisturb: { def: false }, doNotDisturb: { def: false },
doNotDisturbUntil: { def: 0 }, doNotDisturbUntil: { def: 0 },
terminalOverride: { def: "" },
wallpaperPath: { def: "" }, wallpaperPath: { def: "" },
perMonitorWallpaper: { def: false }, perMonitorWallpaper: { def: false },
+6 -17
View File
@@ -51,6 +51,8 @@ var SPEC = {
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
animationVariant: { def: 0 }, animationVariant: { def: 0 },
motionEffect: { def: 0 }, motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
previousDirectionalMode: { def: 1 },
m3ElevationEnabled: { def: true }, m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 }, m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 }, m3ElevationOpacity: { def: 30 },
@@ -61,11 +63,9 @@ var SPEC = {
popoutElevationEnabled: { def: true }, popoutElevationEnabled: { def: true },
barElevationEnabled: { def: true }, barElevationEnabled: { def: true },
blurEnabled: { def: false }, blurEnabled: { def: false },
blurForegroundLayers: { def: true },
blurLayerOutlineOpacity: { def: 0.12, coerce: percentToUnit },
blurBorderColor: { def: "outline" }, blurBorderColor: { def: "outline" },
blurBorderCustomColor: { def: "#ffffff" }, blurBorderCustomColor: { def: "#ffffff" },
blurBorderOpacity: { def: 0.35, coerce: percentToUnit }, blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
wallpaperFillMode: { def: "Fill" }, wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false }, blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false }, blurWallpaperOnOverview: { def: false },
@@ -83,9 +83,7 @@ var SPEC = {
selectedGpuIndex: { def: 0 }, selectedGpuIndex: { def: 0 },
enabledGpuPciIds: { def: [] }, enabledGpuPciIds: { def: [] },
showSystemTray: { def: true }, showSystemTray: { def: true },
systemTrayIconTintMode: { def: "none" }, systemTrayMonochromeIcons: { def: false },
systemTrayIconTintSaturation: { def: 50 },
systemTrayIconTintStrength: { def: 135 },
showClock: { def: true }, showClock: { def: true },
showNotificationButton: { def: true }, showNotificationButton: { def: true },
showBattery: { def: true }, showBattery: { def: true },
@@ -304,7 +302,6 @@ var SPEC = {
matugenTemplatePywalfox: { def: true }, matugenTemplatePywalfox: { def: true },
matugenTemplateZenBrowser: { def: true }, matugenTemplateZenBrowser: { def: true },
matugenTemplateVesktop: { def: true }, matugenTemplateVesktop: { def: true },
matugenTemplateVencord: { def: true },
matugenTemplateEquibop: { def: true }, matugenTemplateEquibop: { def: true },
matugenTemplateGhostty: { def: true }, matugenTemplateGhostty: { def: true },
matugenTemplateKitty: { def: true }, matugenTemplateKitty: { def: true },
@@ -353,9 +350,6 @@ var SPEC = {
dockMaxVisibleApps: { def: 0 }, dockMaxVisibleApps: { def: 0 },
dockMaxVisibleRunningApps: { def: 0 }, dockMaxVisibleRunningApps: { def: 0 },
dockShowOverflowBadge: { def: true }, dockShowOverflowBadge: { def: true },
dockShowTrash: { def: false },
dockTrashFileManager: { def: "default" },
dockTrashCustomCommand: { def: "" },
notificationOverlayEnabled: { def: false }, notificationOverlayEnabled: { def: false },
notificationPopupShadowEnabled: { def: true }, notificationPopupShadowEnabled: { def: true },
@@ -431,9 +425,6 @@ var SPEC = {
updaterUseCustomCommand: { def: false }, updaterUseCustomCommand: { def: false },
updaterCustomCommand: { def: "" }, updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" }, updaterTerminalAdditionalParams: { def: "" },
updaterIntervalSeconds: { def: 1800 },
updaterIncludeFlatpak: { def: true },
updaterAllowAUR: { def: true },
displayNameMode: { def: "system" }, displayNameMode: { def: "system" },
screenPreferences: { def: {} }, screenPreferences: { def: {} },
@@ -563,10 +554,8 @@ var SPEC = {
frameBarSize: { def: 40 }, frameBarSize: { def: 40 },
frameShowOnOverview: { def: false }, frameShowOnOverview: { def: false },
frameBlurEnabled: { def: true }, frameBlurEnabled: { def: true },
frameCloseGaps: { def: true }, frameCloseGaps: { def: false },
frameLauncherEmergeSide: { def: "bottom" }, frameLauncherEmergeSide: { def: "bottom" }
frameLauncherArcExtender: { def: false },
frameMode: { def: "separate" }
}; };
function getValidKeys() { function getValidKeys() {
+14 -64
View File
@@ -4,7 +4,6 @@ import qs.Common
import qs.Modals import qs.Modals
import qs.Modals.Changelog import qs.Modals.Changelog
import qs.Modals.Clipboard import qs.Modals.Clipboard
import qs.Modals.Common
import qs.Modals.Greeter import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
import qs.Modals.DankLauncherV2 import qs.Modals.DankLauncherV2
@@ -28,7 +27,6 @@ import qs.Services
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DMSShell")
property bool osdSurfacesLoaded: true property bool osdSurfacesLoaded: true
property int pendingOsdResumeReloads: 0 property int pendingOsdResumeReloads: 0
@@ -56,7 +54,7 @@ Item {
item.popoutService = PopoutService; item.popoutService = PopoutService;
} }
item.pluginId = pluginId; item.pluginId = pluginId;
log.info("Daemon plugin loaded:", pluginId); console.info("Daemon plugin loaded:", pluginId);
} }
} }
} }
@@ -95,7 +93,7 @@ Item {
} }
onFadeCancelled: { onFadeCancelled: {
log.debug("Fade to lock cancelled by user on screen:", fadeWindowLoader.modelData.name); console.log("Fade to lock cancelled by user on screen:", fadeWindowLoader.modelData.name);
} }
} }
@@ -135,7 +133,7 @@ Item {
} }
onFadeCancelled: { onFadeCancelled: {
log.debug("Fade to DPMS cancelled by user on screen:", fadeDpmsWindowLoader.modelData.name); console.log("Fade to DPMS cancelled by user on screen:", fadeDpmsWindowLoader.modelData.name);
} }
} }
@@ -289,15 +287,11 @@ Item {
sourceComponent: Dock { sourceComponent: Dock {
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
trashContextMenu: dockTrashContextMenuLoader.item ? dockTrashContextMenuLoader.item : null
} }
onLoaded: { onLoaded: {
if (item) { if (item) {
dockContextMenuLoader.active = true; dockContextMenuLoader.active = true;
if (SettingsData.dockShowTrash) {
dockTrashContextMenuLoader.active = true;
}
} }
} }
@@ -334,6 +328,7 @@ Item {
sourceComponent: Component { sourceComponent: Component {
DankDashPopout { DankDashPopout {
id: dankDashPopout id: dankDashPopout
onPopoutClosed: PopoutService.unloadDankDash()
} }
} }
} }
@@ -348,43 +343,6 @@ Item {
} }
} }
LazyLoader {
id: dockTrashContextMenuLoader
active: false
DockTrashContextMenu {
id: dockTrashContextMenu
}
}
Connections {
target: SettingsData
function onDockShowTrashChanged() {
if (SettingsData.dockShowTrash) {
dockTrashContextMenuLoader.active = true;
}
}
}
ConfirmModal {
id: emptyTrashConfirm
}
Connections {
target: TrashService
function onEmptyTrashConfirmRequested(itemCount) {
emptyTrashConfirm.showWithOptions({
title: I18n.tr("Empty Trash?"),
message: I18n.tr("Permanently delete %1 item(s)? This cannot be undone.").arg(itemCount),
confirmText: I18n.tr("Empty"),
cancelText: I18n.tr("Cancel"),
confirmColor: Theme.error,
onConfirm: () => TrashService.emptyTrash()
});
}
}
LazyLoader { LazyLoader {
id: notificationCenterLoader id: notificationCenterLoader
@@ -776,7 +734,7 @@ Item {
cmd += " " + escapedPath; cmd += " " + escapedPath;
} }
log.debug("FilePicker: Launching", cmd); console.log("FilePicker: Launching", cmd);
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
@@ -808,10 +766,10 @@ Item {
} }
function onAppPickerRequested(data) { function onAppPickerRequested(data) {
log.debug("App picker requested with data:", JSON.stringify(data)); console.log("DMSShell: App picker requested with data:", JSON.stringify(data));
if (!data || !data.target) { if (!data || !data.target) {
log.warn("Invalid app picker request data"); console.warn("DMSShell: Invalid app picker request data");
return; return;
} }
@@ -880,19 +838,10 @@ Item {
ProcessListModal { ProcessListModal {
id: processListModal id: processListModal
property bool wasShown: false
Component.onCompleted: { Component.onCompleted: {
PopoutService.processListModal = processListModal; PopoutService.processListModal = processListModal;
} }
onVisibleChanged: {
if (visible) {
wasShown = true;
} else if (wasShown) {
PopoutService.unloadProcessListModal();
}
}
} }
} }
@@ -907,12 +856,7 @@ Item {
SystemUpdatePopout { SystemUpdatePopout {
id: systemUpdatePopout id: systemUpdatePopout
onPopoutClosed: { onPopoutClosed: PopoutService.unloadSystemUpdate()
if (systemUpdatePopout._reopenAfterUpgrade) {
return;
}
PopoutService.unloadSystemUpdate();
}
Component.onCompleted: { Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout; PopoutService.systemUpdatePopout = systemUpdatePopout;
@@ -1109,6 +1053,12 @@ Item {
} }
} }
Loader {
id: powerProfileWatcherLoader
active: SettingsData.osdPowerProfileEnabled
source: "Services/PowerProfileWatcher.qml"
}
LazyLoader { LazyLoader {
id: hyprlandOverviewLoader id: hyprlandOverviewLoader
active: CompositorService.isHyprland active: CompositorService.isHyprland
+41 -26
View File
@@ -9,7 +9,6 @@ import qs.Modules.Settings.DisplayConfig
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DMSShellIPC")
required property var powerMenuModalLoader required property var powerMenuModalLoader
required property var processListModalLoader required property var processListModalLoader
@@ -162,36 +161,37 @@ Item {
} }
IpcHandler { IpcHandler {
function resolveTabIndex(tab: string): int {
switch ((tab || "").toLowerCase()) {
case "media":
return 1;
case "wallpaper":
return 2;
case "weather":
return SettingsData.weatherEnabled ? 3 : 0;
default:
return 0;
}
}
function open(tab: string): string { function open(tab: string): string {
const bar = root.getPreferredBar("clockButtonRef"); const bar = root.getPreferredBar("clockButtonRef");
if (!bar) if (!bar)
return "DASH_OPEN_FAILED"; return "DASH_OPEN_FAILED";
const tabIndex = resolveTabIndex(tab);
const dash = root.dankDashPopoutLoader.item; const dash = root.dankDashPopoutLoader.item;
if (dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name) { const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
dash.currentTabIndex = tabIndex;
if (dash.updateSurfacePosition) if (!onSameScreen) {
dash.updateSurfacePosition(); bar.triggerWallpaperBrowser();
return "DASH_OPEN_SUCCESS";
} }
if (!bar.triggerDashTab(tabIndex)) if (!root.dankDashPopoutLoader.item)
return "DASH_OPEN_FAILED"; return "DASH_OPEN_FAILED";
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS"; return "DASH_OPEN_SUCCESS";
} }
@@ -211,8 +211,23 @@ Item {
const bar = root.getPreferredBar("clockButtonRef"); const bar = root.getPreferredBar("clockButtonRef");
if (bar) { if (bar) {
if (!bar.triggerDashTab(resolveTabIndex(tab))) bar.triggerWallpaperBrowser();
return "DASH_TOGGLE_FAILED"; if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
}
return "DASH_TOGGLE_SUCCESS"; return "DASH_TOGGLE_SUCCESS";
} }
return "DASH_TOGGLE_FAILED"; return "DASH_TOGGLE_FAILED";
@@ -846,7 +861,7 @@ Item {
function set(key: string, value: string): string { function set(key: string, value: string): string {
if (!(key in SettingsData)) { if (!(key in SettingsData)) {
log.warn("Cannot set property, not found:", key); console.warn("Cannot set property, not found:", key);
return "SETTINGS_INVALID_KEY"; return "SETTINGS_INVALID_KEY";
} }
@@ -879,12 +894,12 @@ Item {
throw "Unsupported type"; throw "Unsupported type";
} }
log.warn("Setting:", key, value); console.warn("Setting:", key, value);
SettingsData[key] = value; SettingsData[key] = value;
SettingsData.saveSettings(); SettingsData.saveSettings();
return "SETTINGS_SET_SUCCESS"; return "SETTINGS_SET_SUCCESS";
} catch (e) { } catch (e) {
log.warn("Failed to set property:", key, "error:", e); console.warn("Failed to set property:", key, "error:", e);
return "SETTINGS_SET_FAILURE"; return "SETTINGS_SET_FAILURE";
} }
} }
+113 -114
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Widgets import qs.Widgets
@@ -6,7 +7,6 @@ import qs.Services
DankModal { DankModal {
id: root id: root
readonly property var log: Log.scoped("AppPickerModal")
property string title: I18n.tr("Select Application") property string title: I18n.tr("Select Application")
property string targetData: "" property string targetData: ""
@@ -30,52 +30,52 @@ DankModal {
onBackgroundClicked: close() onBackgroundClicked: close()
onDialogClosed: { onDialogClosed: {
searchQuery = ""; searchQuery = ""
selectedIndex = 0; selectedIndex = 0
keyboardNavigationActive = false; keyboardNavigationActive = false
} }
onOpened: { onOpened: {
searchQuery = ""; searchQuery = ""
updateApplicationList(); updateApplicationList()
selectedIndex = 0; selectedIndex = 0
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) { if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus()
} }
}); })
} }
function updateApplicationList() { function updateApplicationList() {
applicationsModel.clear(); applicationsModel.clear()
const apps = AppSearchService.applications; const apps = AppSearchService.applications
const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {}; const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {}
let filteredApps = []; let filteredApps = []
for (const app of apps) { for (const app of apps) {
if (!app || !app.categories) if (!app || !app.categories) continue
continue;
let matchesCategory = categoryFilter.length === 0; let matchesCategory = categoryFilter.length === 0
if (categoryFilter.length > 0) { if (categoryFilter.length > 0) {
try { try {
for (const cat of app.categories) { for (const cat of app.categories) {
if (categoryFilter.includes(cat)) { if (categoryFilter.includes(cat)) {
matchesCategory = true; matchesCategory = true
break; break
} }
} }
} catch (e) { } catch (e) {
log.warn("AppPicker: Error iterating categories for", app.name, ":", e); console.warn("AppPicker: Error iterating categories for", app.name, ":", e)
continue; continue
} }
} }
if (matchesCategory) { if (matchesCategory) {
const name = app.name || ""; const name = app.name || ""
const lowerName = name.toLowerCase(); const lowerName = name.toLowerCase()
const lowerQuery = searchQuery.toLowerCase(); const lowerQuery = searchQuery.toLowerCase()
if (searchQuery === "" || lowerName.includes(lowerQuery)) { if (searchQuery === "" || lowerName.includes(lowerQuery)) {
filteredApps.push({ filteredApps.push({
@@ -84,21 +84,21 @@ DankModal {
exec: app.exec || app.execString || "", exec: app.exec || app.execString || "",
startupClass: app.startupWMClass || "", startupClass: app.startupWMClass || "",
appData: app appData: app
}); })
} }
} }
} }
filteredApps.sort((a, b) => { filteredApps.sort((a, b) => {
const aId = a.appData.id || a.appData.execString || a.appData.exec || ""; const aId = a.appData.id || a.appData.execString || a.appData.exec || ""
const bId = b.appData.id || b.appData.execString || b.appData.exec || ""; const bId = b.appData.id || b.appData.execString || b.appData.exec || ""
const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0; const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0
const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0; const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0
if (aUsage !== bUsage) { if (aUsage !== bUsage) {
return bUsage - aUsage; return bUsage - aUsage
} }
return (a.name || "").localeCompare(b.name || ""); return (a.name || "").localeCompare(b.name || "")
}); })
filteredApps.forEach(app => { filteredApps.forEach(app => {
applicationsModel.append({ applicationsModel.append({
@@ -107,10 +107,10 @@ DankModal {
exec: app.exec, exec: app.exec,
startupClass: app.startupClass, startupClass: app.startupClass,
appId: app.appData.id || app.appData.execString || app.appData.exec || "" appId: app.appData.id || app.appData.execString || app.appData.exec || ""
}); })
}); })
log.debug("AppPicker: Found " + filteredApps.length + " applications"); console.log("AppPicker: Found " + filteredApps.length + " applications")
} }
onSearchQueryChanged: updateApplicationList() onSearchQueryChanged: updateApplicationList()
@@ -129,57 +129,56 @@ DankModal {
focus: true focus: true
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
root.close(); root.close()
event.accepted = true; event.accepted = true
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (applicationsModel.count === 0) if (applicationsModel.count === 0) return
return;
// Toggle view mode with Tab key // Toggle view mode with Tab key
if (event.key === Qt.Key_Tab) { if (event.key === Qt.Key_Tab) {
root.viewMode = root.viewMode === "grid" ? "list" : "grid"; root.viewMode = root.viewMode === "grid" ? "list" : "grid"
event.accepted = true; event.accepted = true
return; return
} }
if (root.viewMode === "grid") { if (root.viewMode === "grid") {
if (event.key === Qt.Key_Left) { if (event.key === Qt.Key_Left) {
root.keyboardNavigationActive = true; root.keyboardNavigationActive = true
root.selectedIndex = Math.max(0, root.selectedIndex - 1); root.selectedIndex = Math.max(0, root.selectedIndex - 1)
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Right) { } else if (event.key === Qt.Key_Right) {
root.keyboardNavigationActive = true; root.keyboardNavigationActive = true
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1); root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Up) { } else if (event.key === Qt.Key_Up) {
root.keyboardNavigationActive = true; root.keyboardNavigationActive = true
root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns); root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns)
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
root.keyboardNavigationActive = true; root.keyboardNavigationActive = true
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns); root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns)
event.accepted = true; event.accepted = true
} }
} else { } else {
if (event.key === Qt.Key_Up) { if (event.key === Qt.Key_Up) {
root.keyboardNavigationActive = true; root.keyboardNavigationActive = true
root.selectedIndex = Math.max(0, root.selectedIndex - 1); root.selectedIndex = Math.max(0, root.selectedIndex - 1)
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
root.keyboardNavigationActive = true; root.keyboardNavigationActive = true
root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1); root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
event.accepted = true; event.accepted = true
} }
} }
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) { if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) {
const app = applicationsModel.get(root.selectedIndex); const app = applicationsModel.get(root.selectedIndex)
launchApplication(app); launchApplication(app)
} }
event.accepted = true; event.accepted = true
} }
} }
@@ -218,7 +217,7 @@ DankModal {
iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText
backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: { onClicked: {
root.viewMode = "list"; root.viewMode = "list"
} }
} }
@@ -230,7 +229,7 @@ DankModal {
iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText
backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: { onClicked: {
root.viewMode = "grid"; root.viewMode = "grid"
} }
} }
} }
@@ -258,42 +257,42 @@ DankModal {
keyForwardTargets: [appContent] keyForwardTargets: [appContent]
onTextEdited: { onTextEdited: {
root.searchQuery = text; root.searchQuery = text
} }
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.close(); root.close()
event.accepted = true; event.accepted = true
return; return
} }
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key); const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
const hasText = text.length > 0; const hasText = text.length > 0
if (isEnterKey && hasText) { if (isEnterKey && hasText) {
if (root.keyboardNavigationActive && applicationsModel.count > 0) { if (root.keyboardNavigationActive && applicationsModel.count > 0) {
const app = applicationsModel.get(root.selectedIndex); const app = applicationsModel.get(root.selectedIndex)
launchApplication(app); launchApplication(app)
} else if (applicationsModel.count > 0) { } else if (applicationsModel.count > 0) {
const app = applicationsModel.get(0); const app = applicationsModel.get(0)
launchApplication(app); launchApplication(app)
} }
event.accepted = true; event.accepted = true
return; return
} }
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]; const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]
const isNavigationKey = navigationKeys.includes(event.key); const isNavigationKey = navigationKeys.includes(event.key)
const isEmptyEnter = isEnterKey && !hasText; const isEmptyEnter = isEnterKey && !hasText
event.accepted = !(isNavigationKey || isEmptyEnter); event.accepted = !(isNavigationKey || isEmptyEnter)
} }
Connections { Connections {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (!root.shouldBeVisible) { if (!root.shouldBeVisible) {
searchField.focus = false; searchField.focus = false
} }
} }
@@ -304,12 +303,12 @@ DankModal {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: { height: {
let usedHeight = 40 + Theme.spacingS; let usedHeight = 40 + Theme.spacingS
usedHeight += 52 + Theme.spacingS; usedHeight += 52 + Theme.spacingS
if (root.showTargetData) { if (root.showTargetData) {
usedHeight += 36 + Theme.spacingS; usedHeight += 36 + Theme.spacingS
} }
return parent.height - usedHeight; return parent.height - usedHeight
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: "transparent" color: "transparent"
@@ -321,14 +320,14 @@ DankModal {
property int itemSpacing: Theme.spacingS property int itemSpacing: Theme.spacingS
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) if (index < 0 || index >= count) return
return;
const itemY = index * (itemHeight + itemSpacing); const itemY = index * (itemHeight + itemSpacing)
const itemBottom = itemY + itemHeight; const itemBottom = itemY + itemHeight
if (itemY < contentY) { if (itemY < contentY) {
contentY = itemY; contentY = itemY
} else if (itemBottom > contentY + height) { } else if (itemBottom > contentY + height) {
contentY = itemBottom - height; contentY = itemBottom - height
} }
} }
@@ -344,9 +343,9 @@ DankModal {
spacing: itemSpacing spacing: itemSpacing
onCurrentIndexChanged: { onCurrentIndexChanged: {
root.selectedIndex = currentIndex; root.selectedIndex = currentIndex
if (root.keyboardNavigationActive) { if (root.keyboardNavigationActive) {
ensureVisible(currentIndex); ensureVisible(currentIndex)
} }
} }
@@ -361,11 +360,11 @@ DankModal {
hoverUpdatesSelection: true hoverUpdatesSelection: true
onItemClicked: (idx, modelData) => { onItemClicked: (idx, modelData) => {
launchApplication(modelData); launchApplication(modelData)
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
root.keyboardNavigationActive = false; root.keyboardNavigationActive = false
} }
} }
} }
@@ -374,14 +373,14 @@ DankModal {
id: appGrid id: appGrid
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) if (index < 0 || index >= count) return
return;
const itemY = Math.floor(index / root.gridColumns) * cellHeight; const itemY = Math.floor(index / root.gridColumns) * cellHeight
const itemBottom = itemY + cellHeight; const itemBottom = itemY + cellHeight
if (itemY < contentY) { if (itemY < contentY) {
contentY = itemY; contentY = itemY
} else if (itemBottom > contentY + height) { } else if (itemBottom > contentY + height) {
contentY = itemBottom - height; contentY = itemBottom - height
} }
} }
@@ -398,9 +397,9 @@ DankModal {
currentIndex: root.selectedIndex currentIndex: root.selectedIndex
onCurrentIndexChanged: { onCurrentIndexChanged: {
root.selectedIndex = currentIndex; root.selectedIndex = currentIndex
if (root.keyboardNavigationActive) { if (root.keyboardNavigationActive) {
ensureVisible(currentIndex); ensureVisible(currentIndex)
} }
} }
@@ -414,11 +413,11 @@ DankModal {
hoverUpdatesSelection: true hoverUpdatesSelection: true
onItemClicked: (idx, modelData) => { onItemClicked: (idx, modelData) => {
launchApplication(modelData); launchApplication(modelData)
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
root.keyboardNavigationActive = false; root.keyboardNavigationActive = false
} }
} }
} }
@@ -450,22 +449,22 @@ DankModal {
} }
function launchApplication(app) { function launchApplication(app) {
if (!app) if (!app) return
return;
root.applicationSelected(app, root.targetData); root.applicationSelected(app, root.targetData)
if (usageHistoryKey && app.appId) { if (usageHistoryKey && app.appId) {
const usageHistory = SettingsData[usageHistoryKey] || {}; const usageHistory = SettingsData[usageHistoryKey] || {}
const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0; const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0
usageHistory[app.appId] = { usageHistory[app.appId] = {
count: currentCount + 1, count: currentCount + 1,
lastUsed: Date.now(), lastUsed: Date.now(),
name: app.name name: app.name
}; }
SettingsData.set(usageHistoryKey, usageHistory); SettingsData.set(usageHistoryKey, usageHistory)
} }
root.close(); root.close()
} }
} }
} }
+2 -3
View File
@@ -7,7 +7,6 @@ import qs.Widgets
DankModal { DankModal {
id: root id: root
readonly property var log: Log.scoped("BluetoothPairingModal")
layerNamespace: "dms:bluetooth-pairing" layerNamespace: "dms:bluetooth-pairing"
@@ -25,7 +24,7 @@ DankModal {
property string passkeyInput: "" property string passkeyInput: ""
function show(pairingData) { function show(pairingData) {
log.debug("BluetoothPairingModal.show() called:", JSON.stringify(pairingData)); console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
token = pairingData.token || ""; token = pairingData.token || "";
deviceName = pairingData.deviceName || ""; deviceName = pairingData.deviceName || "";
deviceAddress = pairingData.deviceAddr || ""; deviceAddress = pairingData.deviceAddr || "";
@@ -34,7 +33,7 @@ DankModal {
pinInput = ""; pinInput = "";
passkeyInput = ""; passkeyInput = "";
log.debug("Calling open()"); console.log("BluetoothPairingModal: Calling open()");
open(); open();
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
+15 -26
View File
@@ -2,11 +2,9 @@ import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Modals import qs.Modals
import qs.Services
AppPickerModal { AppPickerModal {
id: root id: root
readonly property var log: Log.scoped("BrowserPickerModal")
property string url: "" property string url: ""
@@ -19,44 +17,35 @@ AppPickerModal {
showTargetData: true showTargetData: true
function shellEscape(str) { function shellEscape(str) {
return "'" + str.replace(/'/g, "'\\''") + "'"; return "'" + str.replace(/'/g, "'\\''") + "'"
} }
onApplicationSelected: (app, url) => { onApplicationSelected: (app, url) => {
if (!app) if (!app) return
return;
let cmd = app.exec || "";
const escapedUrl = shellEscape(url);
let hasField = false; let cmd = app.exec || ""
if (cmd.includes("%u")) { const escapedUrl = shellEscape(url)
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, ""); 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) { if (!hasField) {
cmd += " " + escapedUrl; cmd += " " + escapedUrl
} }
log.debug("BrowserPicker: Launching", cmd); console.log("BrowserPicker: Launching", cmd)
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", cmd] command: ["sh", "-c", cmd]
}); })
} }
onViewModeChanged: { onViewModeChanged: {
SettingsData.set("browserPickerViewMode", viewMode); SettingsData.set("browserPickerViewMode", viewMode)
} }
} }
@@ -53,19 +53,6 @@ QtObject {
} }
} }
function togglePinSelected() {
const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
return;
}
const selectedEntry = entries[ClipboardService.selectedIndex];
if (modal.activeTab === "saved") {
modal.unpinEntry(selectedEntry);
} else {
modal.pinEntry(selectedEntry);
}
}
function handleKey(event) { function handleKey(event) {
switch (event.key) { switch (event.key) {
case Qt.Key_Escape: case Qt.Key_Escape:
@@ -78,12 +65,6 @@ QtObject {
return; return;
case Qt.Key_Down: case Qt.Key_Down:
case Qt.Key_Tab: case Qt.Key_Tab:
if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ControlModifier)) {
modal.activeTab = modal.activeTab === "saved" ? "recents" : "saved";
ClipboardService.selectedIndex = 0;
event.accepted = true;
return;
}
if (!ClipboardService.keyboardNavigationActive) { if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0; ClipboardService.selectedIndex = 0;
@@ -94,12 +75,6 @@ QtObject {
return; return;
case Qt.Key_Up: case Qt.Key_Up:
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (event.key === Qt.Key_Backtab && (event.modifiers & Qt.ControlModifier)) {
modal.activeTab = modal.activeTab === "saved" ? "recents" : "saved";
ClipboardService.selectedIndex = 0;
event.accepted = true;
return;
}
if (!ClipboardService.keyboardNavigationActive) { if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0; ClipboardService.selectedIndex = 0;
@@ -146,12 +121,6 @@ QtObject {
event.accepted = true; event.accepted = true;
} }
return; return;
case Qt.Key_S:
if (ClipboardService.keyboardNavigationActive) {
togglePinSelected();
event.accepted = true;
}
return;
} }
} }
@@ -9,8 +9,8 @@ Rectangle {
property bool enterToPaste: false property bool enterToPaste: false
readonly property string hintsText: { readonly property string hintsText: {
if (!wtypeAvailable) if (!wtypeAvailable)
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close"); return I18n.tr("Shift+Del: Clear All • Esc: Close");
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close"); return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
} }
height: ClipboardConstants.keyboardHintsHeight height: ClipboardConstants.keyboardHintsHeight
@@ -26,7 +26,9 @@ Rectangle {
spacing: 2 spacing: 2
StyledText { StyledText {
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help") text: keyboardHints.enterToPaste
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -6,7 +6,6 @@ import qs.Widgets
Item { Item {
id: thumbnail id: thumbnail
readonly property var log: Log.scoped("ClipboardThumbnail")
required property var entry required property var entry
required property string entryType required property string entryType
@@ -53,7 +52,7 @@ Item {
modal.activeImageLoads--; modal.activeImageLoads--;
} }
if (response.error) { if (response.error) {
log.warn("Failed to load image:", entry.id); console.warn("ClipboardThumbnail: Failed to load image:", entry.id);
return; return;
} }
const data = response.result?.data; const data = response.result?.data;
+14 -52
View File
@@ -4,7 +4,6 @@ import qs.Services
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankModal")
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
property string layerNamespace: "dms:modal" property string layerNamespace: "dms:modal"
@@ -84,31 +83,9 @@ Item {
impl.item.toggle(); impl.item.toggle();
} }
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
property var _resolvedBackend: null
Component.onCompleted: _resolvedBackend = _desiredBackend
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
root._maybeResolveBackend();
}
}
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down a modal mid-animation when frame mode is toggled.
function _maybeResolveBackend() {
if (_resolvedBackend === _desiredBackend)
return;
if (impl.item && (impl.item.shouldBeVisible || impl.item.isClosing))
return;
_resolvedBackend = _desiredBackend;
}
Loader { Loader {
id: impl id: impl
sourceComponent: root._resolvedBackend sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
onItemChanged: if (item) onItemChanged: if (item)
root._wireBackend(item) root._wireBackend(item)
} }
@@ -159,7 +136,20 @@ Item {
it.useOverlayLayer = Qt.binding(() => root.useOverlayLayer); it.useOverlayLayer = Qt.binding(() => root.useOverlayLayer);
it.shouldBeVisible = root.shouldBeVisible; it.shouldBeVisible = root.shouldBeVisible;
it.shouldBeVisibleChanged.connect(function () {
if (root.shouldBeVisible !== it.shouldBeVisible)
root.shouldBeVisible = it.shouldBeVisible;
});
it.shouldHaveFocus = root.shouldHaveFocus; it.shouldHaveFocus = root.shouldHaveFocus;
it.shouldHaveFocusChanged.connect(function () {
if (root.shouldHaveFocus !== it.shouldHaveFocus)
root.shouldHaveFocus = it.shouldHaveFocus;
});
it.opened.connect(root.opened);
it.dialogClosed.connect(root.dialogClosed);
it.backgroundClicked.connect(root.backgroundClicked);
if (it.modalFocusScope) if (it.modalFocusScope)
_modalFocusScope.parent = it.modalFocusScope; _modalFocusScope.parent = it.modalFocusScope;
@@ -176,32 +166,4 @@ Item {
impl.item.shouldHaveFocus = root.shouldHaveFocus; impl.item.shouldHaveFocus = root.shouldHaveFocus;
} }
} }
Connections {
target: impl.item
ignoreUnknownSignals: true
function onShouldBeVisibleChanged() {
if (impl.item && root.shouldBeVisible !== impl.item.shouldBeVisible)
root.shouldBeVisible = impl.item.shouldBeVisible;
}
function onShouldHaveFocusChanged() {
if (impl.item && root.shouldHaveFocus !== impl.item.shouldHaveFocus)
root.shouldHaveFocus = impl.item.shouldHaveFocus;
}
function onOpened() {
root.opened();
}
function onDialogClosed() {
root.dialogClosed();
root._maybeResolveBackend();
}
function onBackgroundClicked() {
root.backgroundClicked();
}
}
} }
+58 -78
View File
@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
@@ -9,7 +7,6 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankModalConnected")
property var modalHandle: root property var modalHandle: root
property string layerNamespace: "dms:modal" property string layerNamespace: "dms:modal"
@@ -89,22 +86,16 @@ Item {
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: false readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened signal opened
signal dialogClosed signal dialogClosed
signal backgroundClicked signal backgroundClicked
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer {
id: _syncTimer
interval: 0
onTriggered: root._flushSync()
}
property bool animationsEnabled: true property bool animationsEnabled: true
// Connected chrome sync
property string _chromeClaimId: "" property string _chromeClaimId: ""
property bool _fullSyncPending: false property bool _fullSyncPending: false
@@ -148,37 +139,19 @@ Item {
ConnectedModeState.releaseDockRetract(_chromeClaimId); ConnectedModeState.releaseDockRetract(_chromeClaimId);
} }
property bool _animSyncQueued: false function _flushFullSync() {
property bool _bodySyncQueued: false _fullSyncPending = false;
_syncModalChromeState();
}
function _queueFullSync() { function _queueFullSync() {
if (_fullSyncPending)
return;
_fullSyncPending = true; _fullSyncPending = true;
if (!_syncTimer.running) Qt.callLater(() => {
_syncTimer.restart(); if (root && typeof root._flushFullSync === "function")
} root._flushFullSync();
function _queueAnimSync() { });
_animSyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _queueBodySync() {
_bodySyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _flushSync() {
const fullDirty = _fullSyncPending;
const animDirty = _animSyncQueued;
const bodyDirty = _bodySyncQueued;
_fullSyncPending = false;
_animSyncQueued = false;
_bodySyncQueued = false;
if (fullDirty)
_syncModalChromeState();
if (animDirty)
_syncModalAnim();
if (bodyDirty)
_syncModalBody();
} }
function _syncModalAnim() { function _syncModalAnim() {
@@ -212,10 +185,10 @@ Item {
onFrameOwnsConnectedChromeChanged: _syncModalChromeState() onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
onResolvedConnectedBarSideChanged: _queueFullSync() onResolvedConnectedBarSideChanged: _queueFullSync()
onShouldBeVisibleChanged: _queueFullSync() onShouldBeVisibleChanged: _queueFullSync()
onAlignedXChanged: _queueBodySync() onAlignedXChanged: _syncModalBody()
onAlignedYChanged: _queueBodySync() onAlignedYChanged: _syncModalBody()
onAlignedWidthChanged: _queueBodySync() onAlignedWidthChanged: _syncModalBody()
onAlignedHeightChanged: _queueBodySync() onAlignedHeightChanged: _syncModalBody()
Component.onDestruction: _releaseModalChrome() Component.onDestruction: _releaseModalChrome()
@@ -338,6 +311,8 @@ Item {
readonly property real shadowMotionPadding: { readonly property real shadowMotionPadding: {
if (Theme.isConnectedEffect) if (Theme.isConnectedEffect)
return 0; return 0;
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide") if (animationType === "slide")
return 30; return 30;
if (Theme.isDirectionalEffect) if (Theme.isDirectionalEffect)
@@ -481,8 +456,8 @@ Item {
readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0 blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0 && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0 blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0 && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0
blurRadius: root.effectiveCornerRadius blurRadius: root.effectiveCornerRadius
} }
@@ -492,10 +467,10 @@ Item {
return WlrLayershell.Overlay; return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom": case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "background": case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "overlay": case "overlay":
return WlrLayershell.Overlay; return WlrLayershell.Overlay;
@@ -611,6 +586,8 @@ Item {
} }
return 0; return 0;
} }
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect) if (slide && !directionalEffect && !depthEffect)
return 15; return 15;
if (directionalEffect) { if (directionalEffect) {
@@ -653,6 +630,8 @@ Item {
} }
return 0; return 0;
} }
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect) if (slide && !directionalEffect && !depthEffect)
return -30; return -30;
if (directionalEffect) { if (directionalEffect) {
@@ -687,30 +666,43 @@ Item {
return root.animationOffset; return root.animationOffset;
} }
readonly property real computedScaleCollapsed: root.animationScaleCollapsed property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
// openProgress: 0 = closed (at frozenMotionOffset, scaleCollapsed), 1 = open (at 0, scale 1). onAnimXChanged: if (root.frameOwnsConnectedChrome)
QtObject { root._syncModalAnim()
id: morph onAnimYChanged: if (root.frameOwnsConnectedChrome)
property real openProgress: root.shouldBeVisible ? 1 : 0 root._syncModalAnim()
Behavior on openProgress {
enabled: root.animationsEnabled readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
NumberAnimation { property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline Behavior on animX {
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve enabled: root.animationsEnabled
} NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress) Behavior on animY {
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress) enabled: root.animationsEnabled
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
onAnimXChanged: if (root.frameOwnsConnectedChrome) Behavior on scaleValue {
root._queueAnimSync() enabled: root.animationsEnabled
onAnimYChanged: if (root.frameOwnsConnectedChrome) NumberAnimation {
root._queueAnimSync() duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item { Item {
id: contentContainer id: contentContainer
@@ -723,9 +715,6 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0) opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
transformOrigin: Item.Center transformOrigin: Item.Center
@@ -739,15 +728,6 @@ Item {
} }
} }
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
ElevationShadow { ElevationShadow {
id: modalShadowLayer id: modalShadowLayer
anchors.fill: parent anchors.fill: parent
@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
@@ -9,7 +7,6 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankModalStandalone")
property var modalHandle: root property var modalHandle: root
property string layerNamespace: "dms:modal" property string layerNamespace: "dms:modal"
@@ -244,8 +241,8 @@ Item {
readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: shouldBeVisible ? modalContainer.width * s : 0 blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: shouldBeVisible ? modalContainer.height * s : 0 blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius blurRadius: root.cornerRadius
} }
@@ -255,10 +252,10 @@ Item {
return WlrLayershell.Overlay; return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom": case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "background": case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "overlay": case "overlay":
return WlrLayershell.Overlay; return WlrLayershell.Overlay;
@@ -321,8 +318,7 @@ Item {
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
easing.type: Easing.BezierSpline
duration: root.animationDuration duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
@@ -351,22 +347,45 @@ Item {
readonly property real offsetX: slide ? 15 : 0 readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset readonly property real offsetY: slide ? -30 : root.animationOffset
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1). property real animX: 0
QtObject { property real animY: 0
id: morph property real scaleValue: root.animationScaleCollapsed
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress { onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
enabled: root.animationsEnabled onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
DankAnim {
duration: root.animationDuration Connections {
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve target: root
} function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
} }
} }
readonly property real animX: modalContainer.offsetX * (1 - morph.openProgress) Behavior on animX {
readonly property real animY: modalContainer.offsetY * (1 - morph.openProgress) enabled: root.animationsEnabled
readonly property real scaleValue: root.animationScaleCollapsed + (1.0 - root.animationScaleCollapsed) * morph.openProgress DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item { Item {
id: contentContainer id: contentContainer
@@ -379,7 +398,6 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: root.shouldBeVisible ? 1 : 0 opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5 x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
+34 -35
View File
@@ -9,7 +9,6 @@ import qs.Widgets
DankModal { DankModal {
id: root id: root
readonly property var log: Log.scoped("DankColorPickerModal")
layerNamespace: "dms:color-picker" layerNamespace: "dms:color-picker"
@@ -51,17 +50,17 @@ DankModal {
function toggle() { function toggle() {
if (shouldBeVisible) { if (shouldBeVisible) {
hide(); hide();
} else { } else {
show(); show();
} }
} }
function toggleInstant() { function toggleInstant() {
if (shouldBeVisible) { if (shouldBeVisible) {
hideInstant(); hideInstant();
} else { } else {
show(); show();
} }
} }
@@ -112,7 +111,7 @@ DankModal {
hideInstant(); hideInstant();
Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => { Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => {
if (exitCode !== 0) { if (exitCode !== 0) {
log.warn("dms color pick exited with code:", exitCode); console.warn("dms color pick exited with code:", exitCode);
root.show(); root.show();
return; return;
} }
@@ -121,11 +120,11 @@ DankModal {
if (result.hex) { if (result.hex) {
applyPickedColor(result.hex); applyPickedColor(result.hex);
} else { } else {
log.warn("Failed to parse dms color pick output: missing hex"); console.warn("Failed to parse dms color pick output: missing hex");
root.show(); root.show();
} }
} catch (e) { } catch (e) {
log.warn("Failed to parse dms color pick JSON:", e); console.warn("Failed to parse dms color pick JSON:", e);
root.show(); root.show();
} }
}, 0, Proc.noTimeout); }, 0, Proc.noTimeout);
@@ -143,39 +142,39 @@ DankModal {
onBackgroundClicked: hide() onBackgroundClicked: hide()
IpcHandler { IpcHandler {
function open(): string { function open(): string {
root.show(); root.show();
return "COLOR_PICKER_MODAL_OPEN_SUCCESS"; return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
} }
function openColor(color: string): string { function openColor(color: string): string {
root.selectedColor = Qt.color(color); root.selectedColor = Qt.color(color);
root.currentColor = Qt.color(color); root.currentColor = Qt.color(color);
root.updateFromColor(Qt.color(color)); root.updateFromColor(Qt.color(color));
return open(); return open();
} }
function close(): string { function close(): string {
root.hide(); root.hide();
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS"; return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
} }
function closeInstant(): string { function closeInstant(): string {
root.hideInstant(); root.hideInstant();
return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS"; return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS";
} }
function toggle(): string { function toggle(): string {
root.toggle(); root.toggle();
return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS"; return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS";
} }
function toggleInstant(): string { function toggleInstant(): string {
root.toggleInstant(); root.toggleInstant();
return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS"; return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS";
} }
target: "color-picker" target: "color-picker"
} }
content: Component { content: Component {
@@ -1881,7 +1881,7 @@ Item {
function openTerminal(path) { function openTerminal(path) {
if (!path) if (!path)
return; return;
var terminal = SessionData.resolveTerminal() || "xterm"; var terminal = Quickshell.env("TERMINAL") || "xterm";
Quickshell.execDetached({ Quickshell.execDetached({
command: [terminal], command: [terminal],
workingDirectory: path workingDirectory: path
@@ -1,10 +1,8 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankLauncherV2Modal")
readonly property bool spotlightOpen: impl.item ? impl.item.spotlightOpen : false readonly property bool spotlightOpen: impl.item ? impl.item.spotlightOpen : false
readonly property bool isClosing: impl.item ? impl.item.isClosing : false readonly property bool isClosing: impl.item ? impl.item.isClosing : false
@@ -22,7 +20,6 @@ Item {
readonly property real modalY: impl.item ? impl.item.modalY : 0 readonly property real modalY: impl.item ? impl.item.modalY : 0
readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : "" readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
signal dialogClosed signal dialogClosed
@@ -61,31 +58,9 @@ Item {
impl.item.toggleWithMode(mode); impl.item.toggleWithMode(mode);
} }
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
property var _resolvedBackend: null
Component.onCompleted: _resolvedBackend = _desiredBackend
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
root._maybeResolveBackend();
}
}
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down the launcher mid-animation when frame mode is toggled.
function _maybeResolveBackend() {
if (_resolvedBackend === _desiredBackend)
return;
if (impl.item && (impl.item.spotlightOpen || impl.item.isClosing))
return;
_resolvedBackend = _desiredBackend;
}
Loader { Loader {
id: impl id: impl
sourceComponent: root._resolvedBackend sourceComponent: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
onItemChanged: if (item) onItemChanged: if (item)
root._wireBackend(item) root._wireBackend(item)
} }
@@ -104,15 +79,6 @@ Item {
if (!it) if (!it)
return; return;
it.modalHandle = root; it.modalHandle = root;
} it.dialogClosed.connect(root.dialogClosed);
Connections {
target: impl.item
ignoreUnknownSignals: true
function onDialogClosed() {
root.dialogClosed();
root._maybeResolveBackend();
}
} }
} }
@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
@@ -10,7 +8,6 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
property var modalHandle: root property var modalHandle: root
@@ -75,7 +72,6 @@ Item {
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : "" readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== ""
readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom")
function _dockOccupiesSide(side) { function _dockOccupiesSide(side) {
if (!SettingsData.showDock) if (!SettingsData.showDock)
@@ -113,13 +109,10 @@ Item {
{ {
const insetL = _frameEdgeInset("left"); const insetL = _frameEdgeInset("left");
const insetR = _frameEdgeInset("right"); const insetR = _frameEdgeInset("right");
const insetT = _frameEdgeInset("top");
const insetB = _frameEdgeInset("bottom");
const usable = Math.max(0, screenWidth - insetL - insetR); const usable = Math.max(0, screenWidth - insetL - insetR);
const usableH = Math.max(0, screenHeight - insetT - insetB);
return { return {
"x": insetL + Math.max(0, (usable - modalWidth) / 2), "x": insetL + Math.max(0, (usable - modalWidth) / 2),
"y": launcherArcExtenderActive ? insetT + Math.max(0, (usableH - modalHeight) / 2) : (resolvedConnectedBarSide === "top" ? insetT : screenHeight - modalHeight - insetB) "y": resolvedConnectedBarSide === "top" ? _frameEdgeInset("top") : screenHeight - modalHeight - _frameEdgeInset("bottom")
}; };
} }
case "left": case "left":
@@ -177,34 +170,15 @@ Item {
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr) readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr) readonly property real alignedY: Theme.snap(modalY, dpr)
readonly property real _connectedChromeX: alignedX
readonly property real _connectedChromeY: {
if (!launcherArcExtenderActive)
return alignedY;
return resolvedConnectedBarSide === "top" ? Theme.snap(_frameEdgeInset("top"), dpr) : alignedY;
}
readonly property real _connectedChromeWidth: alignedWidth
readonly property real _connectedChromeHeight: {
if (!launcherArcExtenderActive)
return alignedHeight;
if (resolvedConnectedBarSide === "top")
return Theme.snap(Math.max(alignedHeight, alignedY + alignedHeight - _frameEdgeInset("top")), dpr);
if (resolvedConnectedBarSide === "bottom")
return Theme.snap(Math.max(alignedHeight, screenHeight - _frameEdgeInset("bottom") - alignedY), dpr);
return alignedHeight;
}
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
// For directional/depth: window extends from screen top (content slides within) // For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding // For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect
// Content window geometry // Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr) readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: launcherArcExtenderActive ? _connectedChromeY : (_needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)) readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2 readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: { readonly property real _cwHeight: {
if (launcherArcExtenderActive)
return _connectedChromeHeight;
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect) if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
return screenHeight + shadowPad; return screenHeight + shadowPad;
if (Theme.isDepthEffect) if (Theme.isDepthEffect)
@@ -213,17 +187,11 @@ Item {
} }
// Where the content container sits inside the content window // Where the content container sits inside the content window
readonly property real _ccX: shadowPad readonly property real _ccX: shadowPad
readonly property real _ccY: launcherArcExtenderActive ? 0 : (_needsExtendedWindow ? alignedY : shadowPad) readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed signal dialogClosed
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick. // Connected chrome sync
Timer {
id: _syncTimer
interval: 0
onTriggered: root._flushSync()
}
property string _chromeClaimId: "" property string _chromeClaimId: ""
property bool _fullSyncPending: false property bool _fullSyncPending: false
@@ -242,10 +210,10 @@ Item {
ConnectedModeState.setModalState(screenName, { ConnectedModeState.setModalState(screenName, {
"visible": spotlightOpen || contentWindow.visible, "visible": spotlightOpen || contentWindow.visible,
"barSide": resolvedConnectedBarSide, "barSide": resolvedConnectedBarSide,
"bodyX": _connectedChromeX, "bodyX": alignedX,
"bodyY": _connectedChromeY, "bodyY": alignedY,
"bodyW": _connectedChromeWidth, "bodyW": alignedWidth,
"bodyH": _connectedChromeHeight, "bodyH": alignedHeight,
"animX": contentContainer ? contentContainer.animX : 0, "animX": contentContainer ? contentContainer.animX : 0,
"animY": contentContainer ? contentContainer.animY : 0, "animY": contentContainer ? contentContainer.animY : 0,
"omitStartConnector": false, "omitStartConnector": false,
@@ -267,37 +235,19 @@ Item {
ConnectedModeState.releaseDockRetract(_chromeClaimId); ConnectedModeState.releaseDockRetract(_chromeClaimId);
} }
property bool _animSyncQueued: false function _flushFullSync() {
property bool _bodySyncQueued: false _fullSyncPending = false;
_syncModalChromeState();
}
function _queueFullSync() { function _queueFullSync() {
if (_fullSyncPending)
return;
_fullSyncPending = true; _fullSyncPending = true;
if (!_syncTimer.running) Qt.callLater(() => {
_syncTimer.restart(); if (root && typeof root._flushFullSync === "function")
} root._flushFullSync();
function _queueAnimSync() { });
_animSyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _queueBodySync() {
_bodySyncQueued = true;
if (!_syncTimer.running)
_syncTimer.restart();
}
function _flushSync() {
const fullDirty = _fullSyncPending;
const animDirty = _animSyncQueued;
const bodyDirty = _bodySyncQueued;
_fullSyncPending = false;
_animSyncQueued = false;
_bodySyncQueued = false;
if (fullDirty)
_syncModalChromeState();
if (animDirty)
_syncModalAnim();
if (bodyDirty)
_syncModalBody();
} }
function _syncModalAnim() { function _syncModalAnim() {
@@ -315,7 +265,7 @@ Item {
const screenName = _currentScreenName(); const screenName = _currentScreenName();
if (!screenName) if (!screenName)
return; return;
ConnectedModeState.setModalBody(screenName, _connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight); ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight);
} }
function _releaseModalChrome() { function _releaseModalChrome() {
@@ -329,13 +279,12 @@ Item {
} }
onFrameOwnsConnectedChromeChanged: _syncModalChromeState() onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
onLauncherArcExtenderActiveChanged: _queueFullSync()
onResolvedConnectedBarSideChanged: _queueFullSync() onResolvedConnectedBarSideChanged: _queueFullSync()
onSpotlightOpenChanged: _queueFullSync() onSpotlightOpenChanged: _queueFullSync()
onAlignedXChanged: _queueBodySync() onAlignedXChanged: _syncModalBody()
onAlignedYChanged: _queueBodySync() onAlignedYChanged: _syncModalBody()
onAlignedWidthChanged: _queueBodySync() onAlignedWidthChanged: _syncModalBody()
onAlignedHeightChanged: _queueBodySync() onAlignedHeightChanged: _syncModalBody()
Component.onDestruction: _releaseModalChrome() Component.onDestruction: _releaseModalChrome()
@@ -579,26 +528,22 @@ Item {
} }
} }
// Background window: fullscreen, handles darkening + click-to-dismiss
PanelWindow { PanelWindow {
id: backgroundWindow id: backgroundWindow
visible: false visible: false
color: "transparent" color: "transparent"
readonly property real _topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
readonly property real _bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
readonly property real _leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
WlrLayershell.namespace: "dms:spotlight:bg" WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins { WlrLayershell.margins {
top: backgroundWindow._topMargin top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
bottom: backgroundWindow._bottomMargin bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
left: backgroundWindow._leftMargin left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
right: backgroundWindow._rightMargin right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
} }
anchors { anchors {
@@ -610,11 +555,6 @@ Item {
mask: Region { mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
Region {
item: bgContentHole
intersection: Intersection.Subtract
}
} }
Item { Item {
@@ -622,15 +562,6 @@ Item {
anchors.fill: parent anchors.fill: parent
} }
Item {
id: bgContentHole
visible: false
x: root._cwMarginLeft + contentContainer.x - backgroundWindow._leftMargin
y: root._cwMarginTop + contentContainer.y - backgroundWindow._topMargin
width: root.alignedWidth
height: root.contentSurfaceHeight
}
Rectangle { Rectangle {
id: backgroundDarken id: backgroundDarken
anchors.fill: parent anchors.fill: parent
@@ -640,8 +571,7 @@ Item {
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation { DankAnim {
easing.type: Easing.BezierSpline
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
} }
@@ -655,6 +585,7 @@ Item {
} }
} }
// Content window: SMALL, positioned with margins only renders the modal area
PanelWindow { PanelWindow {
id: contentWindow id: contentWindow
visible: false visible: false
@@ -666,8 +597,8 @@ Item {
readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (root.spotlightOpen || root.isClosing) && !root.frameOwnsConnectedChrome ? root.alignedWidth * s : 0 blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 && !root.frameOwnsConnectedChrome ? root.alignedWidth * s : 0
blurHeight: (root.spotlightOpen || root.isClosing) && !root.frameOwnsConnectedChrome ? root.alignedHeight * s : 0 blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 && !root.frameOwnsConnectedChrome ? root.alignedHeight * s : 0
blurRadius: root.cornerRadius blurRadius: root.cornerRadius
} }
@@ -675,10 +606,10 @@ Item {
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom": case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer."); console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "background": case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer."); console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top; return WlrLayershell.Top;
case "overlay": case "overlay":
return WlrLayershell.Overlay; return WlrLayershell.Overlay;
@@ -709,10 +640,10 @@ Item {
Item { Item {
id: contentInputMask id: contentInputMask
visible: false visible: false
x: contentContainer.x x: contentContainer.x + contentWrapper.x
y: contentContainer.y y: contentContainer.y + contentWrapper.y
width: root.alignedWidth width: root.alignedWidth
height: root.contentSurfaceHeight height: root.alignedHeight
} }
Item { Item {
@@ -723,7 +654,7 @@ Item {
x: root._ccX x: root._ccX
y: root._ccY y: root._ccY
width: root.alignedWidth width: root.alignedWidth
height: root.contentSurfaceHeight height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1 readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0 readonly property bool dockTop: dockEdge === 0
@@ -736,7 +667,7 @@ Item {
readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect readonly property bool depthEffect: Theme.isDepthEffect
readonly property real _connectedTravelX: Math.max(Theme.effectAnimOffset, root.alignedWidth + Theme.spacingL) readonly property real _connectedTravelX: Math.max(Theme.effectAnimOffset, root.alignedWidth + Theme.spacingL)
readonly property real _connectedTravelY: root.launcherArcExtenderActive ? root._connectedChromeHeight : Math.max(Theme.effectAnimOffset, root.alignedHeight + Theme.spacingL) readonly property real _connectedTravelY: Math.max(Theme.effectAnimOffset, root.alignedHeight + Theme.spacingL)
readonly property real collapsedMotionX: { readonly property real collapsedMotionX: {
if (root.frameOwnsConnectedChrome) { if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) { switch (root.resolvedConnectedBarSide) {
@@ -779,31 +710,43 @@ Item {
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40); return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
} }
// openProgress: 0 = closed (at frozenMotion, scaleCollapsed), 1 = open (at 0, scale 1). // Declarative bindings snap applied at render layer (contentWrapper x/y)
QtObject { property real animX: root._motionActive ? 0 : root._frozenMotionX
id: morph property real animY: root._motionActive ? 0 : root._frozenMotionY
property real openProgress: root._motionActive ? 1 : 0 property real scaleValue: root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed))
Behavior on openProgress {
enabled: root.animationsEnabled onAnimXChanged: if (root.frameOwnsConnectedChrome)
DankAnim { root._syncModalAnim()
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) onAnimYChanged: if (root.frameOwnsConnectedChrome)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve root._syncModalAnim()
}
Behavior on animX {
enabled: root.animationsEnabled
DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
} }
} }
readonly property real animX: root._frozenMotionX * (1 - morph.openProgress) Behavior on animY {
readonly property real animY: root._frozenMotionY * (1 - morph.openProgress) enabled: root.animationsEnabled
readonly property real scaleValue: Theme.effectScaleCollapsed + (1.0 - Theme.effectScaleCollapsed) * morph.openProgress DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
onAnimXChanged: if (root.frameOwnsConnectedChrome) Behavior on scaleValue {
root._queueAnimSync() enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
onAnimYChanged: if (root.frameOwnsConnectedChrome) DankAnim {
root._queueAnimSync() duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
}
}
Item { Item {
id: directionalClipMask id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 2000 readonly property real clipOversize: 2000
clip: shouldClip clip: shouldClip
@@ -826,7 +769,7 @@ Item {
id: launcherShadowLayer id: launcherShadowLayer
width: parent.width width: parent.width
height: parent.height height: parent.height
opacity: contentWrapper.publishedOpacity opacity: contentWrapper.opacity
scale: contentWrapper.scale scale: contentWrapper.scale
x: contentWrapper.x x: contentWrapper.x
y: contentWrapper.y y: contentWrapper.y
@@ -844,44 +787,20 @@ Item {
id: contentWrapper id: contentWrapper
width: parent.width width: parent.width
height: parent.height height: parent.height
property bool _renderActive: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || launcherMotionVisible
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0) opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
visible: _renderActive visible: opacity > 0
scale: contentContainer.scaleValue scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation { DankAnim {
easing.type: Easing.BezierSpline
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
} }
} }
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
onRunningChanged: if (!running && contentWrapper.publishedOpacity === 0)
contentWrapper._renderActive = false
}
}
Connections {
target: root
function onLauncherMotionVisibleChanged() {
if (root.launcherMotionVisible)
contentWrapper._renderActive = true;
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onPressed: mouse => mouse.accepted = true onPressed: mouse => mouse.accepted = true
@@ -8,7 +8,6 @@ import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
property var modalHandle: root property var modalHandle: root
@@ -62,20 +61,6 @@ Item {
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100) readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
readonly property real modalX: (screenWidth - modalWidth) / 2 readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2 readonly property real modalY: (screenHeight - modalHeight) / 2
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr))
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad, dpr))
readonly property real contentX: Theme.snap(alignedX - windowX, dpr)
readonly property real contentY: Theme.snap(alignedY - windowY, dpr)
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real windowHeight: alignedHeight + contentY + shadowPad
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: Theme.cornerRadius readonly property real cornerRadius: Theme.cornerRadius
@@ -293,16 +278,37 @@ Item {
} }
PanelWindow { PanelWindow {
id: clickCatcher id: launcherWindow
screen: launcherWindow.screen visible: spotlightOpen || isClosing
visible: spotlightOpen
color: "transparent" color: "transparent"
updatesEnabled: false exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight:clickcatcher" WindowBlur {
WlrLayershell.layer: WlrLayershell.Top targetWindow: launcherWindow
WlrLayershell.exclusiveZone: -1 readonly property real s: Math.min(1, modalContainer.scale)
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.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.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
@@ -312,154 +318,75 @@ Item {
} }
mask: Region { mask: Region {
item: outsideClickMask item: spotlightOpen ? fullScreenMask : null
Region {
item: outsideClickHole
intersection: Intersection.Subtract
}
} }
Item { Item {
id: outsideClickMask id: fullScreenMask
visible: false
anchors.fill: parent anchors.fill: parent
} }
Rectangle { Rectangle {
id: outsideClickHole id: backgroundDarken
visible: false anchors.fill: parent
color: "transparent" color: "black"
x: root.alignedX opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
y: root.alignedY visible: contentVisible || opacity > 0
width: root.alignedWidth
height: root.alignedHeight Behavior on opacity {
DankAnim {
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: spotlightOpen enabled: spotlightOpen
onClicked: root.hide() onClicked: mouse => {
} var contentX = modalContainer.x;
} var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
PanelWindow { if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
id: launcherWindow root.hide();
visible: spotlightOpen || isClosing }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.publishedScale)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
blurWidth: contentVisible ? modalContainer.width * s : 0
blurHeight: contentVisible ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
} }
} }
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
left: true
}
WlrLayershell.margins {
left: root.windowX
top: root.windowY
right: 0
bottom: 0
}
implicitWidth: root.windowWidth
implicitHeight: root.windowHeight
mask: Region {
item: launcherInputMask
}
Rectangle {
id: launcherInputMask
visible: false
color: "transparent"
x: modalContainer.x
y: modalContainer.y
width: modalContainer.width
height: modalContainer.height
}
Item { Item {
id: modalContainer id: modalContainer
x: root.contentX x: root.modalX
y: root.contentY y: root.modalY
width: root.alignedWidth width: root.modalWidth
height: root.alignedHeight height: root.modalHeight
visible: _renderActive visible: contentVisible || opacity > 0
property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96
opacity: contentVisible ? 1 : 0 opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96 scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center transformOrigin: Item.Center
Behavior on opacity { Behavior on opacity {
NumberAnimation { DankAnim {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
onRunningChanged: if (!running && !root.contentVisible)
modalContainer._renderActive = false
} }
} }
Behavior on scale { Behavior on scale {
NumberAnimation { DankAnim {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
Behavior on publishedScale {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Connections {
target: root
function onContentVisibleChanged() {
if (root.contentVisible)
modalContainer._renderActive = true;
}
}
ElevationShadow { ElevationShadow {
id: launcherShadowLayer id: launcherShadowLayer
anchors.fill: parent anchors.fill: parent
level: root.shadowLevel level: Theme.elevationLevel3
fallbackOffset: root.shadowFallbackOffset fallbackOffset: 6
targetColor: root.backgroundColor targetColor: root.backgroundColor
borderColor: root.borderColor borderColor: root.borderColor
borderWidth: root.borderWidth borderWidth: root.borderWidth
@@ -274,16 +274,14 @@ FocusScope {
Item { Item {
id: footerBar id: footerBar
readonly property bool _connectedBottomEmerge: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom")
readonly property bool _connectedArcAtFooter: _connectedBottomEmerge && !(root.parentModal?.launcherArcExtenderActive ?? false)
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.leftMargin: root.parentModal?.borderWidth ?? 1 anchors.leftMargin: root.parentModal?.borderWidth ?? 1
anchors.rightMargin: root.parentModal?.borderWidth ?? 1 anchors.rightMargin: root.parentModal?.borderWidth ?? 1
anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingM : (root.parentModal?.borderWidth ?? 1) anchors.bottomMargin: root.parentModal?.borderWidth ?? 1
readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
readonly property bool _connectedArcAtFooter: (root.parentModal?.frameOwnsConnectedChrome ?? false) && (root.parentModal?.resolvedConnectedBarSide === "bottom")
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0 height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
visible: showFooter visible: showFooter
clip: true clip: true
@@ -372,7 +372,7 @@ Popup {
anchors.fill: parent anchors.fill: parent
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2) implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2 implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.floatingSurface color: BlurService.enabled ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
+98 -109
View File
@@ -58,9 +58,9 @@ Item {
item: items[i], item: items[i],
flatIndex: flatIdx, flatIndex: flatIdx,
sectionId: sectionId, sectionId: sectionId,
height: 56 height: 52
}); });
cumY += 56; cumY += 52;
} }
} else { } else {
var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns; var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
@@ -190,136 +190,125 @@ Item {
} }
} }
Item { DankListView {
id: listClip id: mainListView
anchors.fill: parent anchors.fill: parent
anchors.topMargin: BlurService.enabled && stickyHeader.visible ? 32 : 0
clip: true clip: true
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
DankListView { model: ScriptModel {
id: mainListView values: root._visualRows
y: -listClip.anchors.topMargin objectProp: "_rowId"
width: parent.width }
height: parent.height + listClip.anchors.topMargin
clip: true
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
model: ScriptModel { add: null
values: root._visualRows remove: null
objectProp: "_rowId" displaced: null
move: null
delegate: Item {
id: delegateRoot
required property var modelData
required property int index
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
} }
add: null ResultItem {
remove: null anchors.fill: parent
displaced: null visible: delegateRoot.modelData?.type === "list_item"
move: null item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
delegate: Item { onClicked: {
id: delegateRoot if (root.controller && delegateRoot.modelData?.item) {
required property var modelData root.controller.executeItem(delegateRoot.modelData.item);
required property int index
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
}
ResultItem {
anchors.fill: parent
anchors.topMargin: 2
anchors.bottomMargin: 2
visible: delegateRoot.modelData?.type === "list_item"
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
onClicked: {
if (root.controller && delegateRoot.modelData?.item) {
root.controller.executeItem(delegateRoot.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
} }
} }
Row { onRightClicked: (mouseX, mouseY) => {
id: gridRowContent root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
anchors.fill: parent }
visible: delegateRoot.modelData?.type === "grid_row" }
Repeater { Row {
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : [] id: gridRowContent
anchors.fill: parent
visible: delegateRoot.modelData?.type === "grid_row"
Item { Repeater {
id: gridCellDelegate model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
required property var modelData
required property int index
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns)) Item {
id: gridCellDelegate
required property var modelData
required property int index
width: cellWidth readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
height: delegateRoot.height
GridItem { width: cellWidth
width: parent.width - 4 height: delegateRoot.height
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "grid"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: { GridItem {
if (root.controller && gridCellDelegate.modelData?.item) { width: parent.width - 4
root.controller.executeItem(gridCellDelegate.modelData.item); height: parent.height - 4
} anchors.centerIn: parent
} visible: delegateRoot.modelData?.viewMode === "grid"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onRightClicked: (mouseX, mouseY) => { onClicked: {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
} }
} }
TileItem { onRightClicked: (mouseX, mouseY) => {
width: parent.width - 4 root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
height: parent.height - 4 }
anchors.centerIn: parent }
visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: { TileItem {
if (root.controller && gridCellDelegate.modelData?.item) { width: parent.width - 4
root.controller.executeItem(gridCellDelegate.modelData.item); height: parent.height - 4
} anchors.centerIn: parent
} visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onRightClicked: (mouseX, mouseY) => { onClicked: {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
} }
} }
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
} }
} }
} }
@@ -376,7 +365,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
height: 32 height: 32
z: 101 z: 101
color: Theme.floatingSurface color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: stickyHeaderSection !== null visible: stickyHeaderSection !== null
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0 readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
+1 -1
View File
@@ -50,7 +50,7 @@ Item {
id: listComponent id: listComponent
Column { Column {
spacing: 4 spacing: 2
width: contentLoader.width width: contentLoader.width
Repeater { Repeater {
@@ -158,13 +158,6 @@ FocusScope {
selectedFileIsDir = isDir; selectedFileIsDir = isDir;
} }
function openItemContextMenu(sender, localX, localY, path, name, isDir) {
if (!sender)
return;
const pos = sender.mapToItem(root, localX, localY);
itemContextMenu.showAt(root, pos.x, pos.y, path, name, isDir);
}
function navigateUp() { function navigateUp() {
const path = currentPath; const path = currentPath;
if (path === homeDir) if (path === homeDir)
@@ -766,9 +759,6 @@ FocusScope {
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
} }
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
}
Connections { Connections {
function onKeyboardSelectionRequestedChanged() { function onKeyboardSelectionRequestedChanged() {
@@ -827,9 +817,6 @@ FocusScope {
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
} }
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
}
Connections { Connections {
function onKeyboardSelectionRequestedChanged() { function onKeyboardSelectionRequestedChanged() {
@@ -930,9 +917,4 @@ FocusScope {
} }
} }
} }
FileBrowserItemContextMenu {
id: itemContextMenu
parentFocusItem: root
}
} }
@@ -19,7 +19,6 @@ StyledRect {
signal itemClicked(int index, string path, string name, bool isDir) signal itemClicked(int index, string path, string name, bool isDir)
signal itemSelected(int index, string path, string name, bool isDir) signal itemSelected(int index, string path, string name, bool isDir)
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
function getFileExtension(fileName) { function getFileExtension(fileName) {
const parts = fileName.split('.'); const parts = fileName.split('.');
@@ -108,11 +107,11 @@ StyledRect {
const size = _thumbnailPx; const size = _thumbnailPx;
const fp = delegateRoot.filePath; const fp = delegateRoot.filePath;
Paths.mkdir(thumbDir); Paths.mkdir(thumbDir);
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) { Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
if (exitCode === 0) { if (exitCode === 0) {
_videoThumb = thumbPath; _videoThumb = thumbPath;
} else { } else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function (output, exitCode) { Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) {
if (exitCode === 0) if (exitCode === 0)
_videoThumb = thumbPath; _videoThumb = thumbPath;
}); });
@@ -247,16 +246,8 @@ StyledRect {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: {
onClicked: mouse => { itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
switch (mouse.button) {
case Qt.LeftButton:
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
break;
case Qt.RightButton:
itemContextMenuRequested(delegateRoot, mouse.x, mouse.y, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
break;
}
} }
} }
} }
@@ -1,153 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Popup {
id: root
property string filePath: ""
property string fileName: ""
property bool fileIsDir: false
property var parentFocusItem: null
signal trashed
signal menuClosed
readonly property var menuItems: [
{
text: I18n.tr("Move to Trash"),
icon: "delete",
action: trashItem,
enabled: filePath.length > 0,
dangerous: true
},
{
text: I18n.tr("Copy Path"),
icon: "content_copy",
action: copyPath,
enabled: filePath.length > 0
}
]
function showAt(parentItem, localX, localY, path, name, isDir) {
if (!parentItem)
return;
parent = parentItem;
filePath = path || "";
fileName = name || "";
fileIsDir = !!isDir;
x = Math.max(0, Math.min(parentItem.width - width, localX));
y = Math.max(0, Math.min(parentItem.height - height, localY));
open();
}
function trashItem() {
if (!filePath)
return;
TrashService.trashPath(filePath, ok => {
if (ok)
root.trashed();
});
close();
}
function copyPath() {
if (!filePath)
return;
Quickshell.execDetached(["dms", "cl", "copy", filePath]);
close();
}
width: 220
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
menuClosed();
if (parentFocusItem)
Qt.callLater(() => parentFocusItem.forceActiveFocus());
}
onOpened: outsideClickTimer.start()
Timer {
id: outsideClickTimer
interval: 100
onTriggered: root.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
color: Theme.floatingSurface
radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Repeater {
model: root.menuItems
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
opacity: modelData.enabled ? 1 : 0.5
color: {
if (!modelData.enabled || !area.containsMouse)
return "transparent";
if (modelData.dangerous)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
return BlurService.hoverColor(Theme.widgetBaseHoverColor);
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: modelData.icon
size: 16
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData.text
font.pixelSize: Theme.fontSizeSmall
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
elide: Text.ElideRight
}
}
MouseArea {
id: area
anchors.fill: parent
hoverEnabled: true
enabled: modelData.enabled
cursorShape: Qt.PointingHandCursor
onClicked: modelData.action()
}
}
}
}
}
}
@@ -18,7 +18,6 @@ StyledRect {
signal itemClicked(int index, string path, string name, bool isDir) signal itemClicked(int index, string path, string name, bool isDir)
signal itemSelected(int index, string path, string name, bool isDir) signal itemSelected(int index, string path, string name, bool isDir)
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
function getFileExtension(fileName) { function getFileExtension(fileName) {
const parts = fileName.split('.'); const parts = fileName.split('.');
@@ -103,11 +102,11 @@ StyledRect {
const thumbPath = videoThumbnailPath; const thumbPath = videoThumbnailPath;
const fp = listDelegateRoot.filePath; const fp = listDelegateRoot.filePath;
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal"); Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) { Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
if (exitCode === 0) { if (exitCode === 0) {
_videoThumb = thumbPath; _videoThumb = thumbPath;
} else { } else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function (output, exitCode) { Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) {
if (exitCode === 0) if (exitCode === 0)
_videoThumb = thumbPath; _videoThumb = thumbPath;
}); });
@@ -252,16 +251,8 @@ StyledRect {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: {
onClicked: mouse => { itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
switch (mouse.button) {
case Qt.LeftButton:
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
break;
case Qt.RightButton:
itemContextMenuRequested(listDelegateRoot, mouse.x, mouse.y, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
break;
}
} }
} }
} }
+29 -28
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtCore
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -21,9 +22,9 @@ Rectangle {
onShowFileInfoChanged: { onShowFileInfoChanged: {
if (showFileInfo && currentFileName && currentPath) { if (showFileInfo && currentFileName && currentPath) {
const fullPath = currentPath + "/" + currentFileName; const fullPath = currentPath + "/" + currentFileName
fileStatProcess.selectedFilePath = fullPath; fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true; fileStatProcess.running = true
} }
} }
@@ -37,14 +38,14 @@ Rectangle {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text && text.trim()) { if (text && text.trim()) {
const parts = text.trim().split('|'); const parts = text.trim().split('|')
if (parts.length >= 4) { if (parts.length >= 4) {
fileStatProcess.fileStats = { fileStatProcess.fileStats = {
"modifiedTime": parts[0], "modifiedTime": parts[0],
"permissions": parts[1], "permissions": parts[1],
"size": parseInt(parts[2]) || 0, "size": parseInt(parts[2]) || 0,
"fullPath": parts[3] "fullPath": parts[3]
}; }
} }
} }
} }
@@ -59,31 +60,31 @@ Rectangle {
onCurrentFileNameChanged: { onCurrentFileNameChanged: {
if (showFileInfo && currentFileName && currentPath) { if (showFileInfo && currentFileName && currentPath) {
const fullPath = currentPath + "/" + currentFileName; const fullPath = currentPath + "/" + currentFileName
if (fullPath !== fileStatProcess.selectedFilePath) { if (fullPath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = fullPath; fileStatProcess.selectedFilePath = fullPath
fileStatProcess.running = true; fileStatProcess.running = true
} }
} }
} }
function updateFileInfo(filePath, fileName, isDirectory) { function updateFileInfo(filePath, fileName, isDirectory) {
if (filePath && filePath !== fileStatProcess.selectedFilePath) { if (filePath && filePath !== fileStatProcess.selectedFilePath) {
fileStatProcess.selectedFilePath = filePath; fileStatProcess.selectedFilePath = filePath
currentFileName = fileName || ""; currentFileName = fileName || ""
currentFileIsDir = isDirectory || false; currentFileIsDir = isDirectory || false
let ext = ""; let ext = ""
if (!isDirectory && fileName) { if (!isDirectory && fileName) {
const lastDot = fileName.lastIndexOf('.'); const lastDot = fileName.lastIndexOf('.')
if (lastDot > 0) { if (lastDot > 0) {
ext = fileName.substring(lastDot + 1).toLowerCase(); ext = fileName.substring(lastDot + 1).toLowerCase()
} }
} }
currentFileExtension = ext; currentFileExtension = ext
if (showFileInfo) { if (showFileInfo) {
fileStatProcess.running = true; fileStatProcess.running = true
} }
} }
} }
@@ -99,10 +100,10 @@ Rectangle {
"permissions": "", "permissions": "",
"extension": "", "extension": "",
"position": "N/A" "position": "N/A"
}; }
} }
const hasValidFile = currentFileName !== ""; const hasValidFile = currentFileName !== ""
return { return {
"exists": hasValidFile, "exists": hasValidFile,
"name": hasValidFile ? currentFileName : "Loading...", "name": hasValidFile ? currentFileName : "Loading...",
@@ -112,7 +113,7 @@ Rectangle {
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...", "permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
"extension": currentFileExtension, "extension": currentFileExtension,
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A" "position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
}; }
} }
Column { Column {
@@ -208,23 +209,23 @@ Rectangle {
function formatFileSize(bytes) { function formatFileSize(bytes) {
if (bytes === 0 || !bytes) { if (bytes === 0 || !bytes) {
return "0 B"; return "0 B"
} }
const k = 1024; const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
} }
function formatDateTime(dateTimeString) { function formatDateTime(dateTimeString) {
if (!dateTimeString) { if (!dateTimeString) {
return "Unknown"; return "Unknown"
} }
const parts = dateTimeString.split(' '); const parts = dateTimeString.split(' ')
if (parts.length >= 2) { if (parts.length >= 2) {
return parts[0] + " " + parts[1].split('.')[0]; return parts[0] + " " + parts[1].split('.')[0]
} }
return dateTimeString; return dateTimeString
} }
Behavior on opacity { Behavior on opacity {
+32 -24
View File
@@ -1,12 +1,10 @@
import QtQuick import QtQuick
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
readonly property var log: Log.scoped("GreeterDoctorPage")
property bool isRunning: false property bool isRunning: false
property bool hasRun: false property bool hasRun: false
@@ -61,22 +59,26 @@ Item {
border.color: Theme.primary border.color: Theme.primary
opacity: 0 opacity: 0
OpacityAnimator on opacity { SequentialAnimation on opacity {
running: root.isRunning running: root.isRunning
loops: Animation.Infinite loops: Animation.Infinite
from: 0.8 NumberAnimation {
to: 0 from: 0.8
duration: 1500 to: 0
easing.type: Easing.OutQuad duration: 1500
easing.type: Easing.OutQuad
}
} }
ScaleAnimator on scale { SequentialAnimation on scale {
running: root.isRunning running: root.isRunning
loops: Animation.Infinite loops: Animation.Infinite
from: 0.5 NumberAnimation {
to: 1.5 from: 0.5
duration: 1500 to: 1.5
easing.type: Easing.OutQuad duration: 1500
easing.type: Easing.OutQuad
}
} }
} }
@@ -91,22 +93,26 @@ Item {
border.color: Theme.secondary border.color: Theme.secondary
opacity: 0 opacity: 0
OpacityAnimator on opacity { SequentialAnimation on opacity {
running: root.isRunning running: root.isRunning
loops: Animation.Infinite loops: Animation.Infinite
from: 0.8 NumberAnimation {
to: 0 from: 0.8
duration: 1500 to: 0
easing.type: Easing.OutQuad duration: 1500
easing.type: Easing.OutQuad
}
} }
ScaleAnimator on scale { SequentialAnimation on scale {
running: root.isRunning running: root.isRunning
loops: Animation.Infinite loops: Animation.Infinite
from: 0.3 NumberAnimation {
to: 1.3 from: 0.3
duration: 1500 to: 1.3
easing.type: Easing.OutQuad duration: 1500
easing.type: Easing.OutQuad
}
} }
} }
@@ -222,7 +228,9 @@ Item {
text: { text: {
if (root.errorCount === 0) if (root.errorCount === 0)
return I18n.tr("All checks passed", "greeter doctor page success"); return I18n.tr("All checks passed", "greeter doctor page success");
return root.errorCount === 1 ? 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 font.pixelSize: Theme.fontSizeMedium
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
@@ -404,7 +412,7 @@ Item {
else else
root.selectedFilter = "ok"; root.selectedFilter = "ok";
} catch (e) { } catch (e) {
log.error("Failed to parse doctor output:", e); console.error("GreeterDoctorPage: Failed to parse doctor output:", e);
} }
} }
} }
+1 -2
View File
@@ -7,7 +7,6 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: root id: root
readonly property var log: Log.scoped("GreeterModal")
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
property int currentPage: 0 property int currentPage: 0
@@ -106,7 +105,7 @@ FloatingWindow {
root.cheatsheetData = JSON.parse(trimmed); root.cheatsheetData = JSON.parse(trimmed);
root.cheatsheetLoaded = true; root.cheatsheetLoaded = true;
} catch (e) { } catch (e) {
log.warn("Greeter: Failed to parse cheatsheet:", e); console.warn("Greeter: Failed to parse cheatsheet:", e);
} }
} }
} }
+3 -4
View File
@@ -9,7 +9,6 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: processListModal id: processListModal
readonly property var log: Log.scoped("ProcessListModal")
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
property int currentTab: 0 property int currentTab: 0
@@ -23,7 +22,7 @@ FloatingWindow {
function show() { function show() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
log.warn("dgop is not available"); console.warn("ProcessListModal: dgop is not available");
return; return;
} }
visible = true; visible = true;
@@ -37,7 +36,7 @@ FloatingWindow {
function toggle() { function toggle() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
log.warn("dgop is not available"); console.warn("ProcessListModal: dgop is not available");
return; return;
} }
visible = !visible; visible = !visible;
@@ -45,7 +44,7 @@ FloatingWindow {
function focusOrToggle() { function focusOrToggle() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
log.warn("dgop is not available"); console.warn("ProcessListModal: dgop is not available");
return; return;
} }
if (visible) { if (visible) {
@@ -112,9 +112,7 @@ FocusScope {
focus: active focus: active
sourceComponent: Component { sourceComponent: Component {
DockTab { DockTab {}
parentModal: root.parentModal
}
} }
onActiveChanged: { onActiveChanged: {
@@ -220,9 +218,7 @@ FocusScope {
visible: active visible: active
focus: active focus: active
sourceComponent: ThemeColorsTab { sourceComponent: ThemeColorsTab {}
parentModal: root.parentModal
}
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
@@ -170,8 +170,7 @@ Rectangle {
"id": "updater", "id": "updater",
"text": I18n.tr("System Updater"), "text": I18n.tr("System Updater"),
"icon": "refresh", "icon": "refresh",
"tabIndex": 20, "tabIndex": 20
"updaterOnly": true
}, },
{ {
"id": "desktop_widgets", "id": "desktop_widgets",
@@ -347,8 +346,6 @@ Rectangle {
return false; return false;
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23)) if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
return false; return false;
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
return false;
return true; return true;
} }
+1 -2
View File
@@ -7,7 +7,6 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: root id: root
readonly property var log: Log.scoped("WorkspaceRenameModal")
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
@@ -40,7 +39,7 @@ FloatingWindow {
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
HyprlandService.renameWorkspace(name); HyprlandService.renameWorkspace(name);
} else { } else {
log.warn("rename not supported for this compositor"); console.warn("WorkspaceRenameModal: rename not supported for this compositor");
} }
} }
@@ -34,9 +34,7 @@ PluginComponent {
id: detailRoot id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2 implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
DankActionButton { DankActionButton {
anchors.top: parent.top anchors.top: parent.top
@@ -27,12 +27,12 @@ Rectangle {
} }
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.ccPillInactiveBg readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
color: isActive ? _tileBgActive : _tileBgInactive color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : Theme.outlineMedium border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: isActive ? 1 : Theme.layerOutlineWidth border.width: isActive ? 1 : 1
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
function hoverTint(base) { function hoverTint(base) {
@@ -7,7 +7,6 @@ import "../utils/layout.js" as LayoutUtils
Column { Column {
id: root id: root
readonly property var log: Log.scoped("DragDropGrid")
property bool editMode: false property bool editMode: false
property string expandedSection: "" property string expandedSection: ""
@@ -266,7 +265,7 @@ Column {
} }
Behavior on height { Behavior on height {
enabled: true enabled: SettingsData.connectedFrameModeActive
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active) duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
@@ -576,8 +575,7 @@ Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
height: 14 height: 14
sliderTrackColor: Theme.ccSliderTrackColor property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
sliderTrackOpacity: Theme.ccSliderTrackOpacity
} }
} }
} }
@@ -599,8 +597,7 @@ Column {
instanceId: widgetData.instanceId || "" instanceId: widgetData.instanceId || ""
screenName: root.screenName screenName: root.screenName
parentScreen: root.parentScreen parentScreen: root.parentScreen
sliderTrackColor: Theme.ccSliderTrackColor property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
sliderTrackOpacity: Theme.ccSliderTrackOpacity
onIconClicked: { onIconClicked: {
if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) { if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
@@ -623,8 +620,7 @@ Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
height: 14 height: 14
sliderTrackColor: Theme.ccSliderTrackColor property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
sliderTrackOpacity: Theme.ccSliderTrackOpacity
} }
} }
} }
@@ -1055,7 +1051,7 @@ Column {
return true; return true;
} }
} catch (e) { } catch (e) {
log.warn("stale plugin component for", pluginId, "- reloading"); console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
PluginService.reloadPlugin(pluginId); PluginService.reloadPlugin(pluginId);
} }
return false; return false;
@@ -23,15 +23,11 @@ Item {
signal toggleWidgetSize(int index) signal toggleWidgetSize(int index)
width: { width: {
const widgetWidth = widgetData?.width || 50; const widgetWidth = widgetData?.width || 50
if (widgetWidth <= 25) if (widgetWidth <= 25) return gridCellWidth
return gridCellWidth; else if (widgetWidth <= 50) return gridCellWidth * 2
else if (widgetWidth <= 50) else if (widgetWidth <= 75) return gridCellWidth * 3
return gridCellWidth * 2; else return gridCellWidth * 4
else if (widgetWidth <= 75)
return gridCellWidth * 3;
else
return gridCellWidth * 4;
} }
height: isSlider ? 16 : gridCellHeight height: isSlider ? 16 : gridCellHeight
@@ -46,14 +42,10 @@ Item {
z: dragArea.drag.active ? 10000 : 1 z: dragArea.drag.active ? 10000 : 1
Behavior on border.width { Behavior on border.width {
NumberAnimation { NumberAnimation { duration: 150 }
duration: 150
}
} }
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation { duration: 150 }
duration: 150
}
} }
} }
@@ -66,17 +58,14 @@ Item {
property int globalWidgetIndex: root.widgetIndex property int globalWidgetIndex: root.widgetIndex
property int widgetWidth: root.widgetData?.width || 50 property int widgetWidth: root.widgetData?.width || 50
MouseArea { MouseArea {
id: editModeBlocker id: editModeBlocker
anchors.fill: parent anchors.fill: parent
enabled: root.editMode enabled: root.editMode
acceptedButtons: Qt.AllButtons acceptedButtons: Qt.AllButtons
onPressed: function (mouse) { onPressed: function(mouse) { mouse.accepted = true }
mouse.accepted = true; onWheel: function(wheel) { wheel.accepted = true }
}
onWheel: function (wheel) {
wheel.accepted = true;
}
z: 100 z: 100
} }
} }
@@ -90,19 +79,19 @@ Item {
drag.axis: Drag.XAndYAxis drag.axis: Drag.XAndYAxis
drag.smoothed: true drag.smoothed: true
onPressed: function (mouse) { onPressed: function(mouse) {
if (editMode) { if (editMode) {
cursorShape = Qt.ClosedHandCursor; cursorShape = Qt.ClosedHandCursor
if (root.gridLayout && root.gridLayout.moveToTop) { if (root.gridLayout && root.gridLayout.moveToTop) {
root.gridLayout.moveToTop(root); root.gridLayout.moveToTop(root)
} }
} }
} }
onReleased: function (mouse) { onReleased: function(mouse) {
if (editMode) { if (editMode) {
cursorShape = Qt.OpenHandCursor; cursorShape = Qt.OpenHandCursor
root.snapToGrid(); root.snapToGrid()
} }
} }
} }
@@ -112,11 +101,9 @@ Item {
Drag.hotSpot.y: height / 2 Drag.hotSpot.y: height / 2
function swapIndices(i, j) { function swapIndices(i, j) {
if (i === j) if (i === j) return;
return;
const arr = SettingsData.controlCenterWidgets; const arr = SettingsData.controlCenterWidgets;
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
return;
const copy = arr.slice(); const copy = arr.slice();
const tmp = copy[i]; const tmp = copy[i];
@@ -127,41 +114,37 @@ Item {
} }
function snapToGrid() { function snapToGrid() {
if (!editMode || !gridLayout) if (!editMode || !gridLayout) return
return;
const globalPos = root.mapToItem(gridLayout, 0, 0);
const cellWidth = gridLayout.width / gridColumns;
const cellHeight = gridCellHeight + Theme.spacingS;
const centerX = globalPos.x + (root.width / 2); const globalPos = root.mapToItem(gridLayout, 0, 0)
const centerY = globalPos.y + (root.height / 2); const cellWidth = gridLayout.width / gridColumns
const cellHeight = gridCellHeight + Theme.spacingS
let targetCol = Math.max(0, Math.floor(centerX / cellWidth)); const centerX = globalPos.x + (root.width / 2)
let targetRow = Math.max(0, Math.floor(centerY / cellHeight)); const centerY = globalPos.y + (root.height / 2)
targetCol = Math.min(targetCol, gridColumns - 1); let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
const newIndex = findBestInsertionIndex(targetRow, targetCol); targetCol = Math.min(targetCol, gridColumns - 1)
const newIndex = findBestInsertionIndex(targetRow, targetCol)
if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) { if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
swapIndices(widgetIndex, newIndex); swapIndices(widgetIndex, newIndex)
} }
} }
function findBestInsertionIndex(targetRow, targetCol) { function findBestInsertionIndex(targetRow, targetCol) {
const widgets = SettingsData.controlCenterWidgets || []; const widgets = SettingsData.controlCenterWidgets || [];
const n = widgets.length; const n = widgets.length;
if (!n || widgetIndex < 0 || widgetIndex >= n) if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
return -1;
function spanFor(width) { function spanFor(width) {
const w = width ?? 50; const w = width ?? 50;
if (w <= 25) if (w <= 25) return 1;
return 1; if (w <= 50) return 2;
if (w <= 50) if (w <= 75) return 3;
return 2;
if (w <= 75)
return 3;
return 4; return 4;
} }
@@ -186,13 +169,7 @@ Item {
if (i === widgetIndex) { if (i === widgetIndex) {
draggedOrigKey = centerKey; draggedOrigKey = centerKey;
} else { } else {
pos.push({ pos.push({ index: i, row, startCol, span, centerKey });
index: i,
row,
startCol,
span,
centerKey
});
} }
col += span; col += span;
@@ -202,8 +179,7 @@ Item {
} }
} }
if (pos.length === 0) if (pos.length === 0) return -1;
return -1;
const centerColCoord = targetCol + 0.5; const centerColCoord = targetCol + 0.5;
const targetKey = targetRow * cols + centerColCoord; const targetKey = targetRow * cols + centerColCoord;
@@ -216,20 +192,15 @@ Item {
} }
let lo = 0, hi = pos.length - 1; let lo = 0, hi = pos.length - 1;
if (targetKey <= pos[0].centerKey) if (targetKey <= pos[0].centerKey) return pos[0].index;
return pos[0].index; if (targetKey >= pos[hi].centerKey) return pos[hi].index;
if (targetKey >= pos[hi].centerKey)
return pos[hi].index;
while (lo <= hi) { while (lo <= hi) {
const mid = (lo + hi) >> 1; const mid = (lo + hi) >> 1;
const mk = pos[mid].centerKey; const mk = pos[mid].centerKey;
if (targetKey < mk) if (targetKey < mk) hi = mid - 1;
hi = mid - 1; else if (targetKey > mk) lo = mid + 1;
else if (targetKey > mk) else return pos[mid].index;
lo = mid + 1;
else
return pos[mid].index;
} }
const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false; const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
return (movingUp ? pos[lo].index : pos[hi].index); return (movingUp ? pos[lo].index : pos[hi].index);
@@ -269,11 +240,11 @@ Item {
currentSize: root.widgetData?.width || 50 currentSize: root.widgetData?.width || 50
isSlider: root.isSlider isSlider: root.isSlider
widgetIndex: root.widgetIndex widgetIndex: root.widgetIndex
onSizeChanged: newSize => { onSizeChanged: (newSize) => {
var widgets = SettingsData.controlCenterWidgets.slice(); var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) { if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = newSize; widgets[widgetIndex].width = newSize
SettingsData.set("controlCenterWidgets", widgets); SettingsData.set("controlCenterWidgets", widgets)
} }
} }
} }
@@ -299,9 +270,7 @@ Item {
} }
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation { duration: 150 }
duration: 150
}
} }
} }
@@ -314,9 +283,7 @@ Item {
z: -1 z: -1
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation { duration: Theme.shortDuration }
duration: Theme.shortDuration
}
} }
} }
} }
@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import QtQuick.Controls
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -11,11 +10,7 @@ Row {
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
property var availableWidgets: [] property var availableWidgets: []
property var popupScreen: null property Item popoutContent: null
property real popoutX: 0
property real popoutY: 0
property real popoutWidth: 0
property real popoutHeight: 0
signal addWidget(string widgetId) signal addWidget(string widgetId)
signal resetToDefault signal resetToDefault
@@ -24,190 +19,121 @@ Row {
height: 48 height: 48
spacing: Theme.spacingS spacing: Theme.spacingS
function openWidgetLibrary() { onAddWidget: addWidgetPopup.close()
if (popupScreen)
addWidgetWindow.screen = popupScreen;
addWidgetWindow.visible = true;
}
function closeWidgetLibrary() { Popup {
addWidgetWindow.visible = false; id: addWidgetPopup
} parent: popoutContent
x: parent ? Math.round((parent.width - width) / 2) : 0
y: parent ? Math.round((parent.height - height) / 2) : 0
width: 400
height: 300
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onAddWidget: closeWidgetLibrary() background: Rectangle {
onVisibleChanged: { color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
if (!visible) border.color: Theme.primarySelected
closeWidgetLibrary(); border.width: 0
}
PanelWindow {
id: addWidgetWindow
screen: root.popupScreen
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:control-center-widget-library"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
readonly property bool blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real surfaceAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.24 : 0.72) : Theme.popupTransparency
readonly property real rowAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.10 : 0.52) : Theme.popupTransparency
readonly property int panelWidth: 400
readonly property int panelHeight: 300
WindowBlur {
targetWindow: addWidgetWindow
blurX: widgetLibraryPanel.x
blurY: widgetLibraryPanel.y
blurWidth: addWidgetWindow.visible ? widgetLibraryPanel.width : 0
blurHeight: addWidgetWindow.visible ? widgetLibraryPanel.height : 0
blurRadius: Theme.cornerRadius
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.closeWidgetLibrary()
}
FocusScope {
anchors.fill: parent
focus: addWidgetWindow.visible
Keys.onEscapePressed: event => {
root.closeWidgetLibrary();
event.accepted = true;
}
}
Rectangle {
id: widgetLibraryPanel
width: addWidgetWindow.panelWidth
height: addWidgetWindow.panelHeight
x: Math.round((root.popoutWidth > 0 ? root.popoutX + (root.popoutWidth - width) / 2 : (addWidgetWindow.width - width) / 2))
y: Math.round((root.popoutHeight > 0 ? root.popoutY + (root.popoutHeight - height) / 2 : (addWidgetWindow.height - height) / 2))
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, addWidgetWindow.surfaceAlpha) }
border.color: addWidgetWindow.blurActive ? Theme.outlineMedium : Theme.primarySelected
border.width: addWidgetWindow.blurActive ? Theme.layerOutlineWidth : 0
antialiasing: true
MouseArea { contentItem: Item {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton anchors.margins: Theme.spacingL
onClicked: mouse => mouse.accepted = true
}
Item { Row {
anchors.fill: parent id: headerRow
anchors.margins: Theme.spacingL anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingM
Row { DankIcon {
id: headerRow name: "add_circle"
anchors.top: parent.top size: Theme.iconSize
anchors.left: parent.left color: Theme.primary
anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "add_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("Add Widget")
style: Typography.Style.Subtitle
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
} }
DankListView { Typography {
id: widgetList text: I18n.tr("Add Widget")
style: Typography.Style.Subtitle
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
anchors.top: headerRow.bottom DankListView {
anchors.topMargin: Theme.spacingM anchors.top: headerRow.bottom
anchors.left: parent.left anchors.topMargin: Theme.spacingM
anchors.right: parent.right anchors.left: parent.left
anchors.bottom: parent.bottom anchors.right: parent.right
spacing: Theme.spacingS anchors.bottom: parent.bottom
clip: true spacing: Theme.spacingS
model: root.availableWidgets clip: true
model: root.availableWidgets
delegate: Rectangle { delegate: Rectangle {
width: widgetList.width width: 400 - Theme.spacingL * 2
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: widgetMouseArea.containsMouse ? Theme.withAlpha(Theme.primary, addWidgetWindow.blurActive ? 0.12 : 0.08) : Theme.withAlpha(Theme.surfaceContainerHigh, addWidgetWindow.rowAlpha) color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: Theme.layerOutlineWidth border.width: 0
antialiasing: true
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: modelData.icon name: modelData.icon
size: Theme.iconSize size: Theme.iconSize
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize
Typography {
text: modelData.text
style: Typography.Style.Body
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
} }
Column { Typography {
anchors.verticalCenter: parent.verticalCenter text: modelData.description
spacing: 2 style: Typography.Style.Caption
width: parent.width - Theme.iconSize * 2 - Theme.spacingM * 3 color: Theme.outline
elide: Text.ElideRight
Typography { width: parent.width
text: modelData.text horizontalAlignment: Text.AlignLeft
style: Typography.Style.Body
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Typography {
text: modelData.description
style: Typography.Style.Caption
color: Theme.outline
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
DankIcon {
name: "add"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { DankIcon {
id: widgetMouseArea name: "add"
anchors.fill: parent size: Theme.iconSize - 4
hoverEnabled: true color: Theme.primary
cursorShape: Qt.PointingHandCursor anchors.verticalCenter: parent.verticalCenter
onClicked: { }
root.addWidget(modelData.id); }
}
MouseArea {
id: widgetMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(modelData.id);
} }
} }
} }
@@ -245,7 +171,7 @@ Row {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.openWidgetLibrary() onClicked: addWidgetPopup.open()
} }
} }
@@ -21,9 +21,9 @@ Rectangle {
implicitHeight: 70 implicitHeight: 70
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: Theme.layerOutlineWidth border.width: 0
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -75,7 +75,7 @@ DankPopout {
} }
} }
readonly property color _containerBg: Theme.nestedSurface readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
function openWithSection(section) { function openWithSection(section) {
StateUtils.openWithSection(root, section); StateUtils.openWithSection(root, section);
@@ -86,7 +86,14 @@ DankPopout {
} }
popupWidth: 550 popupWidth: 550
popupHeight: targetPopupHeight popupHeight: {
if (SettingsData.connectedFrameModeActive)
return targetPopupHeight;
const screenHeight = (triggerScreen?.height ?? 1080);
const maxHeight = screenHeight - 100;
const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400;
return Math.min(maxHeight, contentHeight);
}
triggerWidth: 80 triggerWidth: 80
positioning: "" positioning: ""
screen: triggerScreen screen: triggerScreen
@@ -279,11 +286,7 @@ DankPopout {
id: editControls id: editControls
width: parent.width width: parent.width
visible: editMode visible: editMode
popupScreen: root.screen popoutContent: controlContent
popoutX: root.alignedX
popoutY: root.alignedY
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
availableWidgets: { availableWidgets: {
if (!editMode) if (!editMode)
return []; return [];
@@ -18,9 +18,9 @@ Rectangle {
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: Theme.layerOutlineWidth border.width: 0
Row { Row {
id: headerRow id: headerRow
@@ -123,8 +123,6 @@ Rectangle {
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant thumbOutlineColor: Theme.surfaceVariant
trackColor: Theme.ccSliderTrackColor
trackOpacity: Theme.ccSliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (AudioService.source && AudioService.source.audio) { if (AudioService.source && AudioService.source.audio) {
@@ -18,9 +18,9 @@ Rectangle {
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: Theme.layerOutlineWidth border.width: 0
Row { Row {
id: headerRow id: headerRow
@@ -132,8 +132,6 @@ Rectangle {
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant thumbOutlineColor: Theme.surfaceVariant
trackColor: Theme.ccSliderTrackColor
trackOpacity: Theme.ccSliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (AudioService.sink && AudioService.sink.audio) { if (AudioService.sink && AudioService.sink.audio) {
@@ -450,7 +448,6 @@ Rectangle {
Item { Item {
id: appVolumeRow id: appVolumeRow
property color sliderTrackColor: "transparent" property color sliderTrackColor: "transparent"
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
anchors.centerIn: parent anchors.centerIn: parent
height: 40 height: 40
@@ -522,8 +519,7 @@ Rectangle {
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer thumbOutlineColor: Theme.surfaceContainer
trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.ccSliderTrackColor trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
trackOpacity: appVolumeRow.sliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (modelData) { if (modelData) {
@@ -12,9 +12,9 @@ Rectangle {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: Theme.layerOutlineWidth border.width: 0
function isActiveProfile(profile) { function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
@@ -153,9 +153,9 @@ Item {
width: 320 width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2 height: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.floatingSurface color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: Theme.layerOutlineWidth border.width: 0
opacity: modalVisible ? 1 : 0 opacity: modalVisible ? 1 : 0
scale: modalVisible ? 1 : 0.9 scale: modalVisible ? 1 : 0.9
@@ -20,9 +20,9 @@ Rectangle {
return headerRow.height + bluetoothContent.height + Theme.spacingM; return headerRow.height + bluetoothContent.height + Theme.spacingM;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.nestedSurface color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: Theme.layerOutlineWidth border.width: 0
property var bluetoothCodecModalRef: null property var bluetoothCodecModalRef: null
property var devicesBeingPaired: new Set() property var devicesBeingPaired: new Set()
@@ -115,7 +115,7 @@ Rectangle {
height: 36 height: 36
radius: 18 radius: 18
color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent" color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
border.color: adapterEnabled ? Theme.primary : Theme.outlineStrong border.color: adapterEnabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0 border.width: 0
visible: adapterEnabled visible: adapterEnabled
@@ -434,7 +434,7 @@ Rectangle {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1
color: Theme.outlineStrong color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: pairedRepeater.count > 0 && availableRepeater.count > 0 visible: pairedRepeater.count > 0 && availableRepeater.count > 0
} }
@@ -449,7 +449,7 @@ Rectangle {
size: 24 size: 24
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4) color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
RotationAnimator on rotation { RotationAnimation on rotation {
running: parent.visible running: parent.visible
loops: Animation.Infinite loops: Animation.Infinite
from: 0 from: 0
@@ -609,7 +609,7 @@ Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 0 border.width: 0
border.color: Theme.outlineStrong border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
} }
MenuItem { MenuItem {

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