mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e9b6e0bdc | |||
| a6303e938b | |||
| f8b345938a | |||
| 3606166b66 | |||
| 720e2ee403 | |||
| 5562f82a85 | |||
| 9d37a907f2 | |||
| 348f3b4239 | |||
| acec4ea26b | |||
| 0c4289715c | |||
| 5b9ef67bb1 | |||
| f27aedf7c4 | |||
| b88ae7c53e | |||
| dd48801429 | |||
| 48616b64b4 | |||
| 2c7add54ad | |||
| e8bf2c86e6 | |||
| ee440f2ca1 |
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +13,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
|||||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://archlinux.org/packages/extra/x86_64/dms-shell/)
|
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||||
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/danklinux)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ var authCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
var authSyncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync DMS authentication configuration",
|
Short: "Sync DMS authentication configuration",
|
||||||
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
||||||
PreRunE: preRunPrivileged,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
@@ -91,7 +90,6 @@ var (
|
|||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -470,7 +468,6 @@ func checkWindowManagers() []checkResult {
|
|||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -503,7 +500,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
"Install Hyprland, niri, Sway, River, or Wayfire",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -512,24 +509,9 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, checkCompositorBlurSupport())
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCompositorBlurSupport() checkResult {
|
|
||||||
supported, err := blur.ProbeSupport()
|
|
||||||
if err != nil {
|
|
||||||
return checkResult{catCompositor, "Background Blur", statusInfo, "Unable to verify", err.Error(), doctorDocsURL + "#compositor-checks"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if supported {
|
|
||||||
return checkResult{catCompositor, "Background Blur", statusOK, "Supported", "Compositor supports ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil && len(output) == 0 {
|
if err != nil && len(output) == 0 {
|
||||||
@@ -553,8 +535,6 @@ func detectRunningWM() string {
|
|||||||
return "Hyprland"
|
return "Hyprland"
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
return "niri"
|
return "niri"
|
||||||
case os.Getenv("MIRACLESOCK") != "":
|
|
||||||
return "Miracle WM"
|
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||||
}
|
}
|
||||||
@@ -573,7 +553,6 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
|||||||
qmlContent := `
|
qmlContent := `
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -582,7 +561,6 @@ ShellRoot {
|
|||||||
property bool idleMonitorAvailable: false
|
property bool idleMonitorAvailable: false
|
||||||
property bool idleInhibitorAvailable: false
|
property bool idleInhibitorAvailable: false
|
||||||
property bool shortcutInhibitorAvailable: false
|
property bool shortcutInhibitorAvailable: false
|
||||||
property bool backgroundBlurAvailable: false
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 50
|
interval: 50
|
||||||
@@ -600,18 +578,16 @@ ShellRoot {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var testItem = Qt.createQmlObject(
|
var testItem = Qt.createQmlObject(
|
||||||
'import Quickshell; import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||||
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined"; ' +
|
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
||||||
'readonly property bool hasBackgroundBlur: typeof BackgroundEffect !== "undefined" ' +
|
|
||||||
'}',
|
'}',
|
||||||
root
|
root
|
||||||
)
|
)
|
||||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||||
root.backgroundBlurAvailable = testItem.hasBackgroundBlur
|
|
||||||
testItem.destroy()
|
testItem.destroy()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
@@ -620,8 +596,6 @@ ShellRoot {
|
|||||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
||||||
|
|
||||||
console.warn(root.backgroundBlurAvailable ? "FEATURE:BackgroundBlur:OK" : "FEATURE:BackgroundBlur:UNAVAILABLE")
|
|
||||||
|
|
||||||
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,7 +616,6 @@ ShellRoot {
|
|||||||
{"IdleMonitor", "Idle detection"},
|
{"IdleMonitor", "Idle detection"},
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,7 +15,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"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/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -132,8 +130,12 @@ func updateArchLinux() error {
|
|||||||
return errdefs.ErrUpdateCancelled
|
return errdefs.ErrUpdateCancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
|
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
|
||||||
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -477,7 +479,11 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
fmt.Printf("Installing to %s...\n", currentPath)
|
fmt.Printf("Installing to %s...\n", currentPath)
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
||||||
|
replaceCmd.Stdin = os.Stdin
|
||||||
|
replaceCmd.Stdout = os.Stdout
|
||||||
|
replaceCmd.Stderr = os.Stderr
|
||||||
|
if err := replaceCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to replace binary: %w", err)
|
return fmt.Errorf("failed to replace binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -15,7 +14,6 @@ import (
|
|||||||
"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"
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
"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"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
@@ -37,7 +35,7 @@ var greeterInstallCmd = &cobra.Command{
|
|||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -59,10 +57,9 @@ var greeterInstallCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var greeterSyncCmd = &cobra.Command{
|
var greeterSyncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync DMS theme and settings with greeter",
|
Short: "Sync DMS theme and settings with greeter",
|
||||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||||
PreRunE: preRunPrivileged,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
auth, _ := cmd.Flags().GetBool("auth")
|
auth, _ := cmd.Flags().GetBool("auth")
|
||||||
@@ -91,7 +88,7 @@ var greeterEnableCmd = &cobra.Command{
|
|||||||
Use: "enable",
|
Use: "enable",
|
||||||
Short: "Enable DMS greeter in greetd config",
|
Short: "Enable DMS greeter in greetd config",
|
||||||
Long: "Configure greetd to use DMS as the greeter",
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -127,7 +124,7 @@ var greeterUninstallCmd = &cobra.Command{
|
|||||||
Use: "uninstall",
|
Use: "uninstall",
|
||||||
Short: "Remove DMS greeter configuration and restore previous display manager",
|
Short: "Remove DMS greeter configuration and restore previous display manager",
|
||||||
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -309,7 +306,10 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nDisabling greetd...")
|
fmt.Println("\nDisabling greetd...")
|
||||||
if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil {
|
disableCmd := exec.Command("sudo", "systemctl", "disable", "greetd")
|
||||||
|
disableCmd.Stdout = os.Stdout
|
||||||
|
disableCmd.Stderr = os.Stderr
|
||||||
|
if err := disableCmd.Run(); err != nil {
|
||||||
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✓ greetd disabled")
|
fmt.Println(" ✓ greetd disabled")
|
||||||
@@ -375,10 +375,10 @@ func restorePreDMSGreetdConfig(sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||||
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
return fmt.Errorf("failed to restore %s: %w", candidate, err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath); err != nil {
|
if err := runSudoCommand(sudoPassword, "chmod", "644", configPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
|
||||||
@@ -406,14 +406,21 @@ command = "agreety --cmd /bin/bash"
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
|
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
|
||||||
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
return fmt.Errorf("failed to write fallback greetd config: %w", err)
|
||||||
}
|
}
|
||||||
_ = privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath)
|
_ = runSudoCommand(sudoPassword, "chmod", "644", configPath)
|
||||||
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runSudoCommand(_ string, command string, args ...string) error {
|
||||||
|
cmd := exec.Command("sudo", append([]string{command}, args...)...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
|
||||||
func suggestDisplayManagerRestore(nonInteractive bool) {
|
func suggestDisplayManagerRestore(nonInteractive bool) {
|
||||||
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
@@ -432,7 +439,10 @@ func suggestDisplayManagerRestore(nonInteractive bool) {
|
|||||||
|
|
||||||
enableDM := func(dm string) {
|
enableDM := func(dm string) {
|
||||||
fmt.Printf(" Enabling %s...\n", dm)
|
fmt.Printf(" Enabling %s...\n", dm)
|
||||||
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil {
|
cmd := exec.Command("sudo", "systemctl", "enable", "--force", dm)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
|
||||||
@@ -631,7 +641,10 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
|
|
||||||
if response != "n" && response != "no" {
|
if response != "n" && response != "no" {
|
||||||
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", greeterGroup, currentUser.Username); err != nil {
|
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
||||||
|
addUserCmd.Stdout = os.Stdout
|
||||||
|
addUserCmd.Stderr = os.Stderr
|
||||||
|
if err := addUserCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||||
@@ -856,19 +869,22 @@ func disableDisplayManager(dmName string) (bool, error) {
|
|||||||
actionTaken := false
|
actionTaken := false
|
||||||
|
|
||||||
if state.NeedsDisable {
|
if state.NeedsDisable {
|
||||||
var action, actionVerb string
|
var disableCmd *exec.Cmd
|
||||||
switch state.EnabledState {
|
var actionVerb string
|
||||||
case "static":
|
|
||||||
|
if state.EnabledState == "static" {
|
||||||
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
|
||||||
action = "mask"
|
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName)
|
||||||
actionVerb = "masked"
|
actionVerb = "masked"
|
||||||
default:
|
} else {
|
||||||
fmt.Printf(" Disabling %s...\n", dmName)
|
fmt.Printf(" Disabling %s...\n", dmName)
|
||||||
action = "disable"
|
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName)
|
||||||
actionVerb = "disabled"
|
actionVerb = "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), "", "systemctl", action, dmName); err != nil {
|
disableCmd.Stdout = os.Stdout
|
||||||
|
disableCmd.Stderr = os.Stderr
|
||||||
|
if err := disableCmd.Run(); err != nil {
|
||||||
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,7 +925,10 @@ func ensureGreetdEnabled() error {
|
|||||||
|
|
||||||
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
|
||||||
fmt.Println(" Unmasking greetd...")
|
fmt.Println(" Unmasking greetd...")
|
||||||
if err := privesc.Run(context.Background(), "", "systemctl", "unmask", "greetd"); err != nil {
|
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd")
|
||||||
|
unmaskCmd.Stdout = os.Stdout
|
||||||
|
unmaskCmd.Stderr = os.Stderr
|
||||||
|
if err := unmaskCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Println(" ✓ Unmasked greetd")
|
fmt.Println(" ✓ Unmasked greetd")
|
||||||
@@ -921,7 +940,10 @@ func ensureGreetdEnabled() error {
|
|||||||
fmt.Println(" Enabling greetd service...")
|
fmt.Println(" Enabling greetd service...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", "greetd"); err != nil {
|
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
||||||
|
enableCmd.Stdout = os.Stdout
|
||||||
|
enableCmd.Stderr = os.Stderr
|
||||||
|
if err := enableCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,7 +973,10 @@ func ensureGraphicalTarget() error {
|
|||||||
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
currentTargetStr := strings.TrimSpace(string(currentTarget))
|
||||||
if currentTargetStr != "graphical.target" {
|
if currentTargetStr != "graphical.target" {
|
||||||
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
|
||||||
if err := privesc.Run(context.Background(), "", "systemctl", "set-default", "graphical.target"); err != nil {
|
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target")
|
||||||
|
setDefaultCmd.Stdout = os.Stdout
|
||||||
|
setDefaultCmd.Stderr = os.Stderr
|
||||||
|
if err := setDefaultCmd.Run(); err != nil {
|
||||||
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
fmt.Println("⚠ Warning: Failed to set graphical.target as default")
|
||||||
fmt.Println(" Greeter may not start on boot. Run manually:")
|
fmt.Println(" Greeter may not start on boot. Run manually:")
|
||||||
fmt.Println(" sudo systemctl set-default graphical.target")
|
fmt.Println(" sudo systemctl set-default graphical.target")
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -21,7 +19,7 @@ var setupCmd = &cobra.Command{
|
|||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
PersistentPreRunE: preRunPrivileged,
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -270,16 +269,3 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
|||||||
|
|
||||||
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// preRunPrivileged combines the immutable-system check with a privesc tool
|
|
||||||
// selection prompt (shown only when multiple tools are available and the
|
|
||||||
// $DMS_PRIVESC env var isn't set).
|
|
||||||
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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=
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -293,7 +292,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
|
|||||||
LogOutput: "Installing base-devel development tools",
|
LogOutput: "Installing base-devel development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||||
}
|
}
|
||||||
@@ -464,7 +463,7 @@ func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPass
|
|||||||
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
||||||
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
||||||
}
|
}
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||||
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||||
}
|
}
|
||||||
@@ -502,7 +501,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,7 +779,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||||
installArgs = append(installArgs, files...)
|
installArgs = append(installArgs, files...)
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||||
|
|
||||||
fileNames := make([]string, len(files))
|
fileNames := make([]string, len(files))
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,6 +55,27 @@ func (b *BaseDistribution) logError(message string, err error) {
|
|||||||
b.log(errorMsg)
|
b.log(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||||
|
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||||
|
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||||
|
func escapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||||
|
// This helper escapes special characters in the password to prevent shell injection
|
||||||
|
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||||
|
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||||
|
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||||
|
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||||
|
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||||
|
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists(name) {
|
if b.commandExists(name) {
|
||||||
@@ -690,7 +710,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install to /usr/local/bin
|
// Install to /usr/local/bin
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -183,7 +182,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -200,7 +199,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -216,7 +215,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -442,7 +441,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
// Create keyrings directory if it doesn't exist
|
// Create keyrings directory if it doesn't exist
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
if err := mkdirCmd.Run(); err != nil {
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
}
|
}
|
||||||
@@ -456,7 +455,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
@@ -472,7 +471,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
@@ -492,7 +491,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
}
|
}
|
||||||
@@ -538,7 +537,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +625,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +643,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -683,7 +682,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -255,7 +254,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logError("failed to install prerequisites", err)
|
f.logError("failed to install prerequisites", err)
|
||||||
@@ -438,7 +437,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -462,7 +461,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -538,7 +537,7 @@ func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []st
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var GentooGlobalUseFlags = []string{
|
var GentooGlobalUseFlags = []string{
|
||||||
@@ -202,9 +201,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if hasUse {
|
if hasUse {
|
||||||
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
} else {
|
} else {
|
||||||
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
@@ -282,7 +281,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
if syncErr != nil {
|
if syncErr != nil {
|
||||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
@@ -303,7 +302,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logError("failed to install prerequisites", err)
|
g.logError("failed to install prerequisites", err)
|
||||||
@@ -504,14 +503,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||||
packageUseDir := "/etc/portage/package.use"
|
packageUseDir := "/etc/portage/package.use"
|
||||||
|
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -525,7 +524,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -533,7 +532,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -558,7 +557,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable GURU repository
|
// Enable GURU repository
|
||||||
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
|
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
output, err := enableCmd.CombinedOutput()
|
output, err := enableCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -590,7 +589,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
LogOutput: "Syncing GURU repository",
|
LogOutput: "Syncing GURU repository",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
|
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -623,7 +622,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
|
|
||||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||||
|
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -637,7 +636,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -645,7 +644,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -696,6 +695,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualPackageInstaller provides methods for installing packages from source
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
@@ -144,7 +143,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
m.logError("failed to install dgop", err)
|
m.logError("failed to install dgop", err)
|
||||||
@@ -214,7 +213,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "dpkg -i niri.deb",
|
CommandInfo: "dpkg -i niri.deb",
|
||||||
}
|
}
|
||||||
|
|
||||||
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
|
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||||
|
|
||||||
output, err := installDebCmd.CombinedOutput()
|
output, err := installDebCmd.CombinedOutput()
|
||||||
@@ -325,7 +324,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -388,7 +387,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
@@ -454,7 +453,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
@@ -493,11 +492,16 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
// Make it executable
|
||||||
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,11 +646,15 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||||
|
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := copyCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||||
|
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||||
|
if err := chmodCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -251,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
|
|||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
args := []string{"zypper", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logError("failed to install prerequisites", err)
|
o.logError("failed to install prerequisites", err)
|
||||||
@@ -487,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||||
@@ -508,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to refresh repositories: %w", err)
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
}
|
}
|
||||||
@@ -589,7 +588,7 @@ func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sud
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, alias := range aliases {
|
for _, alias := range aliases {
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
|
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
||||||
repoOutput, err := cmd.CombinedOutput()
|
repoOutput, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
@@ -647,7 +646,7 @@ func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packag
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -799,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
|||||||
CommandInfo: "sudo zypper install rustup",
|
CommandInfo: "sudo zypper install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -178,7 +177,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -196,7 +195,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
// Not installed, install it
|
// Not installed, install it
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -212,7 +211,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -399,7 +398,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y software-properties-common")
|
"apt-get install -y software-properties-common")
|
||||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||||
@@ -417,7 +416,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||||
@@ -438,7 +437,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||||
}
|
}
|
||||||
@@ -505,7 +504,7 @@ func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []st
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +591,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +609,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -650,7 +649,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||||
}
|
}
|
||||||
|
|
||||||
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||||
@@ -665,7 +664,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||||
}
|
}
|
||||||
@@ -679,7 +678,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
@@ -328,17 +327,56 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm greetd")
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"pacman -S --needed --noconfirm greetd")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y greetd")
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"dnf install -y greetd")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y greetd")
|
if sudoPassword != "" {
|
||||||
case distros.FamilyUbuntu, distros.FamilyDebian:
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
|
"zypper install -y greetd")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
|
||||||
|
}
|
||||||
|
|
||||||
|
case distros.FamilyUbuntu:
|
||||||
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"apt-get install -y greetd")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||||
|
}
|
||||||
|
|
||||||
|
case distros.FamilyDebian:
|
||||||
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"apt-get install -y greetd")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
"emerge --ask n sys-apps/greetd")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilyNix:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
||||||
}
|
}
|
||||||
@@ -399,56 +437,56 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
|||||||
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
|
||||||
if _, err := exec.LookPath("gpg"); err != nil {
|
if _, err := exec.LookPath("gpg"); err != nil {
|
||||||
logFunc("Installing gnupg for OBS repository key import...")
|
logFunc("Installing gnupg for OBS repository key import...")
|
||||||
installGPGCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y gnupg")
|
installGPGCmd := exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "gnupg")
|
||||||
installGPGCmd.Stdout = os.Stdout
|
installGPGCmd.Stdout = os.Stdout
|
||||||
installGPGCmd.Stderr = os.Stderr
|
installGPGCmd.Stderr = os.Stderr
|
||||||
if err := installGPGCmd.Run(); err != nil {
|
if err := installGPGCmd.Run(); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
mkdirCmd := exec.CommandContext(ctx, "sudo", "mkdir", "-p", "/etc/apt/keyrings")
|
||||||
mkdirCmd.Stdout = os.Stdout
|
mkdirCmd.Stdout = os.Stdout
|
||||||
mkdirCmd.Stderr = os.Stderr
|
mkdirCmd.Stderr = os.Stderr
|
||||||
mkdirCmd.Run()
|
mkdirCmd.Run()
|
||||||
addKeyCmd := privesc.ExecCommand(ctx, sudoPassword,
|
addKeyCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
fmt.Sprintf(`bash -c "curl -fsSL %s | gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg"`, keyURL))
|
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL))
|
||||||
addKeyCmd.Stdout = os.Stdout
|
addKeyCmd.Stdout = os.Stdout
|
||||||
addKeyCmd.Stderr = os.Stderr
|
addKeyCmd.Stderr = os.Stderr
|
||||||
addKeyCmd.Run()
|
addKeyCmd.Run()
|
||||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
addRepoCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
fmt.Sprintf(`bash -c "echo '%s' > /etc/apt/sources.list.d/danklinux.list"`, repoLine))
|
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine))
|
||||||
addRepoCmd.Stdout = os.Stdout
|
addRepoCmd.Stdout = os.Stdout
|
||||||
addRepoCmd.Stderr = os.Stderr
|
addRepoCmd.Stderr = os.Stderr
|
||||||
addRepoCmd.Run()
|
addRepoCmd.Run()
|
||||||
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
repoURL := getOpenSUSEOBSRepoURL(osInfo)
|
||||||
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
|
||||||
logFunc("Adding DankLinux OBS repository...")
|
logFunc("Adding DankLinux OBS repository...")
|
||||||
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo %s", repoURL))
|
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL)
|
||||||
addRepoCmd.Stdout = os.Stdout
|
addRepoCmd.Stdout = os.Stdout
|
||||||
addRepoCmd.Stderr = os.Stderr
|
addRepoCmd.Stderr = os.Stderr
|
||||||
addRepoCmd.Run()
|
addRepoCmd.Run()
|
||||||
privesc.ExecCommand(ctx, sudoPassword, "zypper refresh").Run()
|
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run()
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y dms-greeter")
|
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter")
|
||||||
case distros.FamilyUbuntu:
|
case distros.FamilyUbuntu:
|
||||||
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
|
||||||
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
logFunc("Enabling PPA ppa:avengemedia/danklinux...")
|
||||||
ppacmd := privesc.ExecCommand(ctx, sudoPassword, "add-apt-repository -y ppa:avengemedia/danklinux")
|
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux")
|
||||||
ppacmd.Stdout = os.Stdout
|
ppacmd.Stdout = os.Stdout
|
||||||
ppacmd.Stderr = os.Stderr
|
ppacmd.Stderr = os.Stderr
|
||||||
ppacmd.Run()
|
ppacmd.Run()
|
||||||
privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
|
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run()
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter")
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
|
||||||
logFunc("Enabling COPR avengemedia/danklinux...")
|
logFunc("Enabling COPR avengemedia/danklinux...")
|
||||||
coprcmd := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y avengemedia/danklinux")
|
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux")
|
||||||
coprcmd.Stdout = os.Stdout
|
coprcmd.Stdout = os.Stdout
|
||||||
coprcmd.Stderr = os.Stderr
|
coprcmd.Stderr = os.Stderr
|
||||||
coprcmd.Run()
|
coprcmd.Run()
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y dms-greeter")
|
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter")
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
aurHelper := ""
|
aurHelper := ""
|
||||||
for _, helper := range []string{"paru", "yay"} {
|
for _, helper := range []string{"paru", "yay"} {
|
||||||
@@ -501,25 +539,25 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
||||||
action = "Updated"
|
action = "Updated"
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
osInfo, err := distros.GetOSInfo()
|
osInfo, err := distros.GetOSInfo()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
||||||
} else {
|
} else {
|
||||||
logFunc("✓ Restored SELinux context for dms-greeter")
|
logFunc("✓ Restored SELinux context for dms-greeter")
|
||||||
@@ -545,7 +583,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to stat cache directory: %w", err)
|
return fmt.Errorf("failed to stat cache directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
created = true
|
created = true
|
||||||
@@ -557,17 +595,17 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
daemonUser := DetectGreeterUser()
|
daemonUser := DetectGreeterUser()
|
||||||
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
|
||||||
owner := preferredOwner
|
owner := preferredOwner
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||||
// Some setups may not have a matching daemon user at this moment; fall back
|
// Some setups may not have a matching daemon user at this moment; fall back
|
||||||
// to root:<group> while still allowing group-writable greeter runtime access.
|
// to root:<group> while still allowing group-writable greeter runtime access.
|
||||||
fallbackOwner := fmt.Sprintf("root:%s", group)
|
fallbackOwner := fmt.Sprintf("root:%s", group)
|
||||||
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
|
||||||
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
|
||||||
}
|
}
|
||||||
owner = fallbackOwner
|
owner = fallbackOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,13 +616,13 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
filepath.Join(cacheDir, ".cache"),
|
filepath.Join(cacheDir, ".cache"),
|
||||||
}
|
}
|
||||||
for _, dir := range runtimeDirs {
|
for _, dir := range runtimeDirs {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", dir); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, dir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil {
|
||||||
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", dir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil {
|
||||||
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,7 +634,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,13 +659,13 @@ func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPat
|
|||||||
info, err := os.Lstat(legacyPath)
|
info, err := os.Lstat(legacyPath)
|
||||||
if err == nil && info.Mode().IsRegular() {
|
if err == nil && info.Mode().IsRegular() {
|
||||||
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
|
||||||
if copyErr := privesc.Run(context.Background(), sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
|
||||||
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +692,7 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,15 +709,15 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
if err := runSudoCmd(sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.CommandExists("apparmor_parser") {
|
if utils.CommandExists("apparmor_parser") {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
if err := runSudoCmd(sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
|
||||||
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
|
||||||
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
|
||||||
} else {
|
} else {
|
||||||
@@ -707,9 +745,9 @@ func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if utils.CommandExists("apparmor_parser") {
|
if utils.CommandExists("apparmor_parser") {
|
||||||
_ = privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
_ = runSudoCmd(sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
if err := runSudoCmd(sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
|
||||||
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Removed DMS AppArmor profile")
|
logFunc(" ✓ Removed DMS AppArmor profile")
|
||||||
@@ -739,17 +777,50 @@ func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
switch config.Family {
|
switch config.Family {
|
||||||
case distros.FamilyArch:
|
case distros.FamilyArch:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilyFedora:
|
case distros.FamilyFedora:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y acl")
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilySUSE:
|
case distros.FamilySUSE:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y acl")
|
if sudoPassword != "" {
|
||||||
case distros.FamilyUbuntu, distros.FamilyDebian:
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl")
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y acl")
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
|
||||||
|
}
|
||||||
|
|
||||||
|
case distros.FamilyUbuntu:
|
||||||
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
||||||
|
}
|
||||||
|
|
||||||
|
case distros.FamilyDebian:
|
||||||
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilyGentoo:
|
case distros.FamilyGentoo:
|
||||||
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
if sudoPassword != "" {
|
||||||
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
|
||||||
|
} else {
|
||||||
|
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
|
||||||
|
}
|
||||||
|
|
||||||
case distros.FamilyNix:
|
case distros.FamilyNix:
|
||||||
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
|
||||||
}
|
}
|
||||||
@@ -806,7 +877,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
|
||||||
continue
|
continue
|
||||||
@@ -863,7 +934,7 @@ func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, user := range existingUsers {
|
for _, user := range existingUsers {
|
||||||
_ = privesc.Run(context.Background(), sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
|
||||||
cleaned = true
|
cleaned = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -903,7 +974,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
// Create the group if it doesn't exist yet (e.g. before greetd package is installed).
|
||||||
if !utils.HasGroup(group) {
|
if !utils.HasGroup(group) {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "groupadd", "-r", group); err != nil {
|
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil {
|
||||||
return fmt.Errorf("failed to create %s group: %w", group, err)
|
return fmt.Errorf("failed to create %s group: %w", group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
logFunc(fmt.Sprintf("✓ Created system group %s", group))
|
||||||
@@ -914,7 +985,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||||
} else {
|
} else {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||||
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||||
@@ -929,7 +1000,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
if strings.Contains(string(daemonGroupsOutput), group) {
|
if strings.Contains(string(daemonGroupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
|
||||||
} else {
|
} else {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
|
||||||
} else {
|
} else {
|
||||||
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
|
||||||
@@ -959,12 +1030,12 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1176,8 +1247,8 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
target := filepath.Join(cacheDir, "colors.json")
|
target := filepath.Join(cacheDir, "colors.json")
|
||||||
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", target)
|
_ = runSudoCmd(sudoPassword, "rm", "-f", target)
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", source, target); err != nil {
|
if err := runSudoCmd(sudoPassword, "ln", "-sf", source, target); err != nil {
|
||||||
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,9 +1300,9 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", link.target)
|
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||||
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,13 +1340,13 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
||||||
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
if state.ResolvedGreeterWallpaperPath == "" {
|
if state.ResolvedGreeterWallpaperPath == "" {
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Cleared greeter wallpaper override")
|
logFunc("✓ Cleared greeter wallpaper override")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
src := state.ResolvedGreeterWallpaperPath
|
src := state.ResolvedGreeterWallpaperPath
|
||||||
@@ -1286,17 +1357,17 @@ func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPas
|
|||||||
if st.IsDir() {
|
if st.IsDir() {
|
||||||
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", src, destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil {
|
||||||
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
daemonUser := DetectGreeterUser()
|
daemonUser := DetectGreeterUser()
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
|
||||||
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
|
||||||
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Synced greeter wallpaper override")
|
logFunc("✓ Synced greeter wallpaper override")
|
||||||
@@ -1351,13 +1422,13 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
|
|
||||||
greeterDir := "/etc/greetd/niri"
|
greeterDir := "/etc/greetd/niri"
|
||||||
greeterGroup := DetectGreeterGroup()
|
greeterGroup := DetectGreeterGroup()
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "755", greeterDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,7 +1450,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1402,7 +1473,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
||||||
}
|
}
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,7 +1549,7 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
|
|||||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
||||||
return fmt.Errorf("failed to update greetd config: %w", err)
|
return fmt.Errorf("failed to update greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1494,10 +1565,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "cp", path, backupPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return privesc.Run(context.Background(), sudoPassword, "chmod", "644", backupPath)
|
return runSudoCmd(sudoPassword, "chmod", "644", backupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *niriGreeterSync) processFile(filePath string) error {
|
func (s *niriGreeterSync) processFile(filePath string) error {
|
||||||
@@ -1733,11 +1804,11 @@ vt = 1
|
|||||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd config: %w", err)
|
return fmt.Errorf("failed to install greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1841,6 +1912,27 @@ func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
|
|||||||
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
if sudoPassword != "" {
|
||||||
|
fullArgs := append([]string{command}, args...)
|
||||||
|
quotedArgs := make([]string, len(fullArgs))
|
||||||
|
for i, arg := range fullArgs {
|
||||||
|
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
||||||
|
}
|
||||||
|
cmdStr := strings.Join(quotedArgs, " ")
|
||||||
|
|
||||||
|
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func checkSystemdEnabled(service string) (string, error) {
|
func checkSystemdEnabled(service string) (string, error) {
|
||||||
cmd := exec.Command("systemctl", "is-enabled", service)
|
cmd := exec.Command("systemctl", "is-enabled", service)
|
||||||
output, _ := cmd.Output()
|
output, _ := cmd.Output()
|
||||||
@@ -1857,7 +1949,7 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
|
|||||||
switch state {
|
switch state {
|
||||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||||
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "disable", dm); err != nil {
|
if err := runSudoCmd(sudoPassword, "systemctl", "disable", dm); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
|
||||||
} else {
|
} else {
|
||||||
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
|
||||||
@@ -1878,13 +1970,13 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
|
|||||||
}
|
}
|
||||||
if state == "masked" || state == "masked-runtime" {
|
if state == "masked" || state == "masked-runtime" {
|
||||||
logFunc(" Unmasking greetd...")
|
logFunc(" Unmasking greetd...")
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to unmask greetd: %w", err)
|
return fmt.Errorf("failed to unmask greetd: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(" ✓ Unmasked greetd")
|
logFunc(" ✓ Unmasked greetd")
|
||||||
}
|
}
|
||||||
logFunc(" Enabling greetd service (--force)...")
|
logFunc(" Enabling greetd service (--force)...")
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
}
|
}
|
||||||
logFunc("✓ greetd enabled")
|
logFunc("✓ greetd enabled")
|
||||||
@@ -1904,7 +1996,7 @@ func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
|
||||||
if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
|
||||||
return fmt.Errorf("failed to set graphical target: %w", err)
|
return fmt.Errorf("failed to set graphical target: %w", err)
|
||||||
}
|
}
|
||||||
logFunc("✓ Default target set to graphical.target")
|
logFunc("✓ Default target set to graphical.target")
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrConfirmationRequired is returned when --yes is not set and the user
|
// ErrConfirmationRequired is returned when --yes is not set and the user
|
||||||
@@ -383,41 +383,20 @@ func (r *Runner) parseTerminal() (deps.Terminal, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) resolveSudoPassword() (string, error) {
|
func (r *Runner) resolveSudoPassword() (string, error) {
|
||||||
tool, err := privesc.Detect()
|
// Check if sudo credentials are cached (via sudo -v or NOPASSWD)
|
||||||
if err != nil {
|
cmd := exec.Command("sudo", "-n", "true")
|
||||||
return "", err
|
if err := cmd.Run(); err == nil {
|
||||||
}
|
r.log("sudo cache is valid, no password needed")
|
||||||
|
fmt.Fprintln(os.Stdout, "sudo: using cached credentials")
|
||||||
if err := privesc.CheckCached(context.Background()); err == nil {
|
|
||||||
r.log(fmt.Sprintf("%s cache is valid, no password needed", tool.Name()))
|
|
||||||
fmt.Fprintf(os.Stdout, "%s: using cached credentials\n", tool.Name())
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch tool {
|
return "", fmt.Errorf(
|
||||||
case privesc.ToolSudo:
|
"sudo authentication required but no cached credentials found\n" +
|
||||||
return "", fmt.Errorf(
|
"Options:\n" +
|
||||||
"sudo authentication required but no cached credentials found\n" +
|
" 1. Run 'sudo -v' before dankinstall to cache credentials\n" +
|
||||||
"Options:\n" +
|
" 2. Configure passwordless sudo for your user",
|
||||||
" 1. Run 'sudo -v' before dankinstall to cache credentials\n" +
|
)
|
||||||
" 2. Configure passwordless sudo for your user",
|
|
||||||
)
|
|
||||||
case privesc.ToolDoas:
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"doas authentication required but no cached credentials found\n" +
|
|
||||||
"Options:\n" +
|
|
||||||
" 1. Run 'doas true' before dankinstall to cache credentials (requires 'persist' in /etc/doas.conf)\n" +
|
|
||||||
" 2. Configure a 'nopass' rule in /etc/doas.conf for your user",
|
|
||||||
)
|
|
||||||
case privesc.ToolRun0:
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"run0 authentication required but no cached credentials found\n" +
|
|
||||||
"Configure a polkit rule granting your user passwordless privilege\n" +
|
|
||||||
"(see `man polkit` for details on rules in /etc/polkit-1/rules.d/)",
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unsupported privilege tool: %s", tool)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) anyConfigEnabled(m map[string]bool) bool {
|
func (r *Runner) anyConfigEnabled(m map[string]bool) bool {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,7 +292,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
parser := NewNiriParser(filepath.Dir(overridePath))
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
parser.currentSource = overridePath
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
doc, err := parseKDL(data)
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,103 +50,6 @@ type NiriParser struct {
|
|||||||
conflictingConfigs map[string]*NiriKeyBinding
|
conflictingConfigs map[string]*NiriKeyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseKDL(data []byte) (*document.Document, error) {
|
|
||||||
return kdl.Parse(strings.NewReader(normalizeKDLBraces(string(data))))
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeKDLBraces(input string) string {
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.Grow(len(input))
|
|
||||||
|
|
||||||
var prev byte
|
|
||||||
n := len(input)
|
|
||||||
for i := 0; i < n; {
|
|
||||||
c := input[i]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case c == '"':
|
|
||||||
end := findStringEnd(input, i)
|
|
||||||
sb.WriteString(input[i:end])
|
|
||||||
prev = '"'
|
|
||||||
i = end
|
|
||||||
case c == '/' && i+1 < n && input[i+1] == '/':
|
|
||||||
end := findLineCommentEnd(input, i)
|
|
||||||
sb.WriteString(input[i:end])
|
|
||||||
prev = '\n'
|
|
||||||
i = end
|
|
||||||
case c == '/' && i+1 < n && input[i+1] == '*':
|
|
||||||
end := findBlockCommentEnd(input, i)
|
|
||||||
sb.WriteString(input[i:end])
|
|
||||||
prev = '/'
|
|
||||||
i = end
|
|
||||||
case c == '{' && prev != 0 && !isBraceAdjacentSpace(prev):
|
|
||||||
sb.WriteByte(' ')
|
|
||||||
sb.WriteByte(c)
|
|
||||||
prev = c
|
|
||||||
i++
|
|
||||||
default:
|
|
||||||
sb.WriteByte(c)
|
|
||||||
prev = c
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func findStringEnd(s string, start int) int {
|
|
||||||
n := len(s)
|
|
||||||
for i := start + 1; i < n; {
|
|
||||||
switch s[i] {
|
|
||||||
case '\\':
|
|
||||||
i += 2
|
|
||||||
case '"':
|
|
||||||
return i + 1
|
|
||||||
default:
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLineCommentEnd(s string, start int) int {
|
|
||||||
for i := start + 2; i < len(s); i++ {
|
|
||||||
if s[i] == '\n' {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findBlockCommentEnd(s string, start int) int {
|
|
||||||
n := len(s)
|
|
||||||
depth := 1
|
|
||||||
for i := start + 2; i < n && depth > 0; {
|
|
||||||
switch {
|
|
||||||
case i+1 < n && s[i] == '/' && s[i+1] == '*':
|
|
||||||
depth++
|
|
||||||
i += 2
|
|
||||||
case i+1 < n && s[i] == '*' && s[i+1] == '/':
|
|
||||||
depth--
|
|
||||||
i += 2
|
|
||||||
if depth == 0 {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBraceAdjacentSpace(b byte) bool {
|
|
||||||
switch b {
|
|
||||||
case ' ', '\t', '\n', '\r', '{':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNiriParser(configDir string) *NiriParser {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
@@ -188,7 +91,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := parseKDL(data)
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -256,7 +159,7 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
|||||||
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := parseKDL(data)
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,74 +3,9 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
|
||||||
config := `recent-windows {
|
|
||||||
binds {
|
|
||||||
Alt+Tab { next-window scope="output"; }
|
|
||||||
Alt+Shift+Tab { previous-window scope="output"; }
|
|
||||||
Alt+grave { next-window filter="app-id"; }
|
|
||||||
Alt+Shift+grave { previous-window filter="app-id"; }
|
|
||||||
Alt+Escape { next-window scope="all"; }
|
|
||||||
Alt+Shift+Escape{ previous-window scope="all"; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := ParseNiriKeys(tmpDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ParseNiriKeys failed on valid niri config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var found *NiriKeyBinding
|
|
||||||
for i := range result.Section.Keybinds {
|
|
||||||
kb := &result.Section.Keybinds[i]
|
|
||||||
if kb.Key == "Escape" && slices.Contains(kb.Mods, "Alt") && slices.Contains(kb.Mods, "Shift") {
|
|
||||||
found = kb
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found == nil {
|
|
||||||
t.Fatal("Alt+Shift+Escape bind missing — '{' without preceding space was not handled")
|
|
||||||
}
|
|
||||||
if found.Action != "previous-window" {
|
|
||||||
t.Errorf("Action = %q, want %q", found.Action, "previous-window")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeKDLBraces(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
out string
|
|
||||||
}{
|
|
||||||
{"already spaced", "node { child }\n", "node { child }\n"},
|
|
||||||
{"missing space", "node{ child }\n", "node { child }\n"},
|
|
||||||
{"niri keybind", "Alt+Shift+Escape{ previous-window; }", "Alt+Shift+Escape { previous-window; }"},
|
|
||||||
{"brace inside string", `node "a{b" { child }`, `node "a{b" { child }`},
|
|
||||||
{"brace in line comment", "// foo{bar\nnode { }", "// foo{bar\nnode { }"},
|
|
||||||
{"brace in block comment", "/* foo{bar */ node{ }", "/* foo{bar */ node { }"},
|
|
||||||
{"escaped quote in string", `node "a\"b{c" { }`, `node "a\"b{c" { }`},
|
|
||||||
{"leading brace", "{ child }", "{ child }"},
|
|
||||||
{"nested missing space", "a{b{ c }}", "a {b { c }}"},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
got := normalizeKDLBraces(tc.in)
|
|
||||||
if got != tc.out {
|
|
||||||
t.Errorf("normalizeKDLBraces(%q) = %q, want %q", tc.in, got, tc.out)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNiriParseKeyCombo(t *testing.T) {
|
func TestNiriParseKeyCombo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
combo string
|
combo string
|
||||||
|
|||||||
@@ -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...) }
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
+31
-13
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -81,18 +80,16 @@ type lockscreenPamResolver struct {
|
|||||||
|
|
||||||
func defaultSyncDeps() syncDeps {
|
func defaultSyncDeps() syncDeps {
|
||||||
return syncDeps{
|
return syncDeps{
|
||||||
pamDir: "/etc/pam.d",
|
pamDir: "/etc/pam.d",
|
||||||
greetdPath: GreetdPamPath,
|
greetdPath: GreetdPamPath,
|
||||||
dankshellPath: DankshellPamPath,
|
dankshellPath: DankshellPamPath,
|
||||||
dankshellU2fPath: DankshellU2FPamPath,
|
dankshellU2fPath: DankshellU2FPamPath,
|
||||||
isNixOS: IsNixOS,
|
isNixOS: IsNixOS,
|
||||||
readFile: os.ReadFile,
|
readFile: os.ReadFile,
|
||||||
stat: os.Stat,
|
stat: os.Stat,
|
||||||
createTemp: os.CreateTemp,
|
createTemp: os.CreateTemp,
|
||||||
removeFile: os.Remove,
|
removeFile: os.Remove,
|
||||||
runSudoCmd: func(password, command string, args ...string) error {
|
runSudoCmd: runSudoCmd,
|
||||||
return privesc.Run(context.Background(), password, append([]string{command}, args...)...)
|
|
||||||
},
|
|
||||||
pamModuleExists: pamModuleExists,
|
pamModuleExists: pamModuleExists,
|
||||||
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
|
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
|
||||||
}
|
}
|
||||||
@@ -872,3 +869,24 @@ func fingerprintAuthAvailableForUser(username string) bool {
|
|||||||
}
|
}
|
||||||
return hasEnrolledFingerprintOutput(string(out))
|
return hasEnrolledFingerprintOutput(string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
if sudoPassword != "" {
|
||||||
|
fullArgs := append([]string{command}, args...)
|
||||||
|
quotedArgs := make([]string, len(fullArgs))
|
||||||
|
for i, arg := range fullArgs {
|
||||||
|
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
||||||
|
}
|
||||||
|
cmdStr := strings.Join(quotedArgs, " ")
|
||||||
|
|
||||||
|
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,385 +0,0 @@
|
|||||||
package privesc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tool identifies a privilege-escalation binary.
|
|
||||||
type Tool string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ToolSudo Tool = "sudo"
|
|
||||||
ToolDoas Tool = "doas"
|
|
||||||
ToolRun0 Tool = "run0"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnvVar selects a specific tool when set to one of: sudo, doas, run0.
|
|
||||||
const EnvVar = "DMS_PRIVESC"
|
|
||||||
|
|
||||||
var detectionOrder = []Tool{ToolSudo, ToolDoas, ToolRun0}
|
|
||||||
|
|
||||||
var (
|
|
||||||
detectOnce sync.Once
|
|
||||||
detected Tool
|
|
||||||
detectErr error
|
|
||||||
userSelected bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// Detect returns the tool that should be used for privilege escalation.
|
|
||||||
// The result is cached after the first call.
|
|
||||||
func Detect() (Tool, error) {
|
|
||||||
detectOnce.Do(func() {
|
|
||||||
detected, detectErr = detectTool()
|
|
||||||
})
|
|
||||||
return detected, detectErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetForTesting clears cached detection state.
|
|
||||||
func ResetForTesting() {
|
|
||||||
detectOnce = sync.Once{}
|
|
||||||
detected = ""
|
|
||||||
detectErr = nil
|
|
||||||
userSelected = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AvailableTools returns the set of supported tools that are installed on
|
|
||||||
// PATH, in detection-precedence order.
|
|
||||||
func AvailableTools() []Tool {
|
|
||||||
var out []Tool
|
|
||||||
for _, t := range detectionOrder {
|
|
||||||
if t.Available() {
|
|
||||||
out = append(out, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvOverride returns the tool selected by the $DMS_PRIVESC env var (if any)
|
|
||||||
// along with ok=true when the variable is set. An empty or unset variable
|
|
||||||
// returns ok=false.
|
|
||||||
func EnvOverride() (Tool, bool) {
|
|
||||||
v := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar)))
|
|
||||||
if v == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return Tool(v), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTool forces the detected tool to t, bypassing autodetection. Intended
|
|
||||||
// for use after the caller has prompted the user for a selection.
|
|
||||||
func SetTool(t Tool) error {
|
|
||||||
if !t.Available() {
|
|
||||||
return fmt.Errorf("%q is not installed", t.Name())
|
|
||||||
}
|
|
||||||
detectOnce = sync.Once{}
|
|
||||||
detectOnce.Do(func() {
|
|
||||||
detected = t
|
|
||||||
detectErr = nil
|
|
||||||
})
|
|
||||||
userSelected = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectTool() (Tool, error) {
|
|
||||||
switch override := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar))); override {
|
|
||||||
case "":
|
|
||||||
// fall through to autodetect
|
|
||||||
case string(ToolSudo), string(ToolDoas), string(ToolRun0):
|
|
||||||
t := Tool(override)
|
|
||||||
if !t.Available() {
|
|
||||||
return "", fmt.Errorf("%s=%s but %q is not installed", EnvVar, override, t.Name())
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("invalid %s=%q: must be one of sudo, doas, run0", EnvVar, override)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range detectionOrder {
|
|
||||||
if t.Available() {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("no supported privilege escalation tool found (tried: sudo, doas, run0)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the binary name.
|
|
||||||
func (t Tool) Name() string { return string(t) }
|
|
||||||
|
|
||||||
// Available reports whether this tool's binary is on PATH.
|
|
||||||
func (t Tool) Available() bool {
|
|
||||||
if t == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := exec.LookPath(string(t))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportsStdinPassword reports whether the tool can accept a password via
|
|
||||||
// stdin. Only sudo (-S) supports this.
|
|
||||||
func (t Tool) SupportsStdinPassword() bool {
|
|
||||||
return t == ToolSudo
|
|
||||||
}
|
|
||||||
|
|
||||||
// EscapeSingleQuotes escapes single quotes for safe inclusion inside a
|
|
||||||
// bash single-quoted string.
|
|
||||||
func EscapeSingleQuotes(s string) string {
|
|
||||||
return strings.ReplaceAll(s, "'", "'\\''")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeCommand returns a bash command string that runs `command` with the
|
|
||||||
// detected tool. When the tool supports stdin passwords and password is
|
|
||||||
// non-empty, the password is piped in. Otherwise the tool is invoked with
|
|
||||||
// no non-interactive flag so that an interactive TTY prompt is still
|
|
||||||
// possible for CLI callers.
|
|
||||||
//
|
|
||||||
// If detection fails, the returned shell string exits 1 with an error
|
|
||||||
// message so callers that treat the *exec.Cmd as infallible still fail
|
|
||||||
// deterministically.
|
|
||||||
func MakeCommand(password, command string) string {
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return failingShell(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t {
|
|
||||||
case ToolSudo:
|
|
||||||
if password != "" {
|
|
||||||
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("sudo %s", command)
|
|
||||||
case ToolDoas:
|
|
||||||
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
|
|
||||||
case ToolRun0:
|
|
||||||
return fmt.Sprintf("run0 sh -c '%s'", EscapeSingleQuotes(command))
|
|
||||||
default:
|
|
||||||
return failingShell(fmt.Errorf("unsupported privilege tool: %q", t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecCommand builds an exec.Cmd that runs `command` as root via the
|
|
||||||
// detected tool. Detection errors surface at Run() time as a failing
|
|
||||||
// command writing a clear error to stderr.
|
|
||||||
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
|
|
||||||
// No stdin password is supplied; callers relying on non-interactive success
|
|
||||||
// should ensure cached credentials are present (see CheckCached).
|
|
||||||
func ExecArgv(ctx context.Context, argv ...string) *exec.Cmd {
|
|
||||||
if len(argv) == 0 {
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("privesc.ExecArgv: argv must not be empty")))
|
|
||||||
}
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t {
|
|
||||||
case ToolSudo, ToolDoas:
|
|
||||||
return exec.CommandContext(ctx, string(t), argv...)
|
|
||||||
case ToolRun0:
|
|
||||||
return exec.CommandContext(ctx, "run0", argv...)
|
|
||||||
default:
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("unsupported privilege tool: %q", t)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func failingShell(err error) string {
|
|
||||||
return fmt.Sprintf("printf 'privesc: %%s\\n' '%s' >&2; exit 1", EscapeSingleQuotes(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckCached runs a non-interactive credential probe. Returns nil if the
|
|
||||||
// tool will run commands without prompting (cached credentials, nopass, or
|
|
||||||
// polkit rule).
|
|
||||||
func CheckCached(ctx context.Context) error {
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
switch t {
|
|
||||||
case ToolSudo:
|
|
||||||
cmd = exec.CommandContext(ctx, "sudo", "-n", "true")
|
|
||||||
case ToolDoas:
|
|
||||||
cmd = exec.CommandContext(ctx, "doas", "-n", "true")
|
|
||||||
case ToolRun0:
|
|
||||||
cmd = exec.CommandContext(ctx, "run0", "--no-ask-password", "true")
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported privilege tool: %q", t)
|
|
||||||
}
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearCache invalidates any cached credentials. No-op for tools that do
|
|
||||||
// not expose a cache-clear operation.
|
|
||||||
func ClearCache(ctx context.Context) error {
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch t {
|
|
||||||
case ToolSudo:
|
|
||||||
return exec.CommandContext(ctx, "sudo", "-k").Run()
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateWithAskpass validates cached credentials using an askpass helper
|
|
||||||
// script. Only sudo supports this mechanism; the TUI uses it to trigger
|
|
||||||
// fingerprint authentication via PAM.
|
|
||||||
func ValidateWithAskpass(ctx context.Context, askpassScript string) error {
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if t != ToolSudo {
|
|
||||||
return fmt.Errorf("askpass validation requires sudo (detected: %s)", t)
|
|
||||||
}
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidatePassword validates the given password. Only sudo supports this
|
|
||||||
// (via `sudo -S -v`); for other tools the caller should fall back to
|
|
||||||
// CheckCached.
|
|
||||||
func ValidatePassword(ctx context.Context, password string) error {
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if t != ToolSudo {
|
|
||||||
return fmt.Errorf("password validation requires sudo (detected: %s)", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := fmt.Fprintf(stdin, "%s\n", password); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
_ = cmd.Wait()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
return cmd.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuoteArgsForShell wraps each argv element in single quotes so the result
|
|
||||||
// can be safely passed to bash -c.
|
|
||||||
func QuoteArgsForShell(argv []string) string {
|
|
||||||
parts := make([]string, len(argv))
|
|
||||||
for i, a := range argv {
|
|
||||||
parts[i] = "'" + EscapeSingleQuotes(a) + "'"
|
|
||||||
}
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run invokes argv with privilege escalation. When the tool supports stdin
|
|
||||||
// passwords and password is non-empty, the password is piped in. Otherwise
|
|
||||||
// argv is invoked directly, which may prompt on a TTY.
|
|
||||||
// Stdout and Stderr are inherited from the current process.
|
|
||||||
func Run(ctx context.Context, password string, argv ...string) error {
|
|
||||||
if len(argv) == 0 {
|
|
||||||
return fmt.Errorf("privesc.Run: argv must not be empty")
|
|
||||||
}
|
|
||||||
t, err := Detect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
switch {
|
|
||||||
case t == ToolSudo && password != "":
|
|
||||||
cmd = ExecCommand(ctx, password, QuoteArgsForShell(argv))
|
|
||||||
default:
|
|
||||||
cmd = ExecArgv(ctx, argv...)
|
|
||||||
}
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stdinIsTTY reports whether stdin is a character device (interactive
|
|
||||||
// terminal) rather than a pipe or file.
|
|
||||||
func stdinIsTTY() bool {
|
|
||||||
fi, err := os.Stdin.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// PromptCLI interactively prompts the user to pick a privilege tool when more
|
|
||||||
// than one is installed and $DMS_PRIVESC is not set. If stdin is not a TTY,
|
|
||||||
// or only one tool is available, or the env var is set, the detected tool is
|
|
||||||
// returned without any prompt.
|
|
||||||
//
|
|
||||||
// The prompt is written to out (typically os.Stdout/os.Stderr) and input is
|
|
||||||
// read from in. EOF or empty input selects the first option.
|
|
||||||
func PromptCLI(out io.Writer, in io.Reader) (Tool, error) {
|
|
||||||
if userSelected {
|
|
||||||
return Detect()
|
|
||||||
}
|
|
||||||
if _, envSet := EnvOverride(); envSet {
|
|
||||||
return Detect()
|
|
||||||
}
|
|
||||||
|
|
||||||
tools := AvailableTools()
|
|
||||||
switch len(tools) {
|
|
||||||
case 0:
|
|
||||||
return "", fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
|
||||||
case 1:
|
|
||||||
if err := SetTool(tools[0]); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return tools[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !stdinIsTTY() {
|
|
||||||
return Detect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(out, "Multiple privilege escalation tools detected:")
|
|
||||||
for i, t := range tools {
|
|
||||||
fmt.Fprintf(out, " [%d] %s\n", i+1, t.Name())
|
|
||||||
}
|
|
||||||
fmt.Fprintf(out, "Choose one [1-%d] (default 1, or set %s=<tool> to skip): ", len(tools), EnvVar)
|
|
||||||
|
|
||||||
reader := bufio.NewReader(in)
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return "", fmt.Errorf("failed to read selection: %w", err)
|
|
||||||
}
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
|
|
||||||
idx := 1
|
|
||||||
if line != "" {
|
|
||||||
n, convErr := strconv.Atoi(line)
|
|
||||||
if convErr != nil || n < 1 || n > len(tools) {
|
|
||||||
return "", fmt.Errorf("invalid selection %q", line)
|
|
||||||
}
|
|
||||||
idx = n
|
|
||||||
}
|
|
||||||
|
|
||||||
chosen := tools[idx-1]
|
|
||||||
if err := SetTool(chosen); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return chosen, nil
|
|
||||||
}
|
|
||||||
@@ -215,34 +215,33 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, exists := b.debounceTimers[id]; exists {
|
if timer, exists := b.debounceTimers[id]; exists {
|
||||||
if existing.Stop() {
|
timer.Reset(200 * time.Millisecond)
|
||||||
b.debounceWg.Done()
|
} else {
|
||||||
}
|
b.debounceWg.Add(1)
|
||||||
|
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||||
|
defer b.debounceWg.Done()
|
||||||
|
b.debounceMutex.Lock()
|
||||||
|
pending, exists := b.debouncePending[id]
|
||||||
|
if exists {
|
||||||
|
delete(b.debouncePending, id)
|
||||||
|
}
|
||||||
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := b.setBrightnessImmediateWithExponent(id, pending.percent)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending.callback != nil {
|
||||||
|
pending.callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
b.debounceWg.Add(1)
|
|
||||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
|
||||||
defer b.debounceWg.Done()
|
|
||||||
|
|
||||||
b.debounceMutex.Lock()
|
|
||||||
pending, hasPending := b.debouncePending[id]
|
|
||||||
delete(b.debouncePending, id)
|
|
||||||
delete(b.debounceTimers, id)
|
|
||||||
b.debounceMutex.Unlock()
|
|
||||||
|
|
||||||
if !hasPending {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.setBrightnessImmediateWithExponent(id, pending.percent); err != nil {
|
|
||||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pending.callback != nil {
|
|
||||||
pending.callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
b.debounceMutex.Unlock()
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ type SessionState struct {
|
|||||||
type EventType string
|
type EventType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EventStateChanged EventType = "state_changed"
|
EventStateChanged EventType = "state_changed"
|
||||||
|
EventLock EventType = "lock"
|
||||||
|
EventUnlock EventType = "unlock"
|
||||||
|
EventPrepareForSleep EventType = "prepare_for_sleep"
|
||||||
|
EventIdleHintChanged EventType = "idle_hint_changed"
|
||||||
|
EventLockedHintChanged EventType = "locked_hint_changed"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionEvent struct {
|
type SessionEvent struct {
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import (
|
|||||||
|
|
||||||
func TestEventType_Constants(t *testing.T) {
|
func TestEventType_Constants(t *testing.T) {
|
||||||
assert.Equal(t, EventType("state_changed"), EventStateChanged)
|
assert.Equal(t, EventType("state_changed"), EventStateChanged)
|
||||||
|
assert.Equal(t, EventType("lock"), EventLock)
|
||||||
|
assert.Equal(t, EventType("unlock"), EventUnlock)
|
||||||
|
assert.Equal(t, EventType("prepare_for_sleep"), EventPrepareForSleep)
|
||||||
|
assert.Equal(t, EventType("idle_hint_changed"), EventIdleHintChanged)
|
||||||
|
assert.Equal(t, EventType("locked_hint_changed"), EventLockedHintChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSessionState_Struct(t *testing.T) {
|
func TestSessionState_Struct(t *testing.T) {
|
||||||
@@ -35,11 +40,11 @@ func TestSessionEvent_Struct(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event := SessionEvent{
|
event := SessionEvent{
|
||||||
Type: EventStateChanged,
|
Type: EventLock,
|
||||||
Data: state,
|
Data: state,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, EventStateChanged, event.Type)
|
assert.Equal(t, EventLock, event.Type)
|
||||||
assert.Equal(t, "1", event.Data.SessionID)
|
assert.Equal(t, "1", event.Data.SessionID)
|
||||||
assert.True(t, event.Data.Locked)
|
assert.True(t, event.Data.Locked)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const resumeDelay = 3 * time.Second
|
const resumeDelay = 3 * time.Second
|
||||||
@@ -30,14 +29,11 @@ func NewManager() (*Manager, error) {
|
|||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only run a startup scan when the system has been suspended at least once.
|
// Run a startup scan after a delay — covers the case where the process
|
||||||
// On a fresh boot CLOCK_BOOTTIME ≈ CLOCK_MONOTONIC (difference ~0).
|
// was killed during suspend and restarted by systemd (Type=dbus).
|
||||||
// After any suspend/resume cycle the difference grows by the time spent
|
// The fresh process never sees the PrepareForSleep true→false transition,
|
||||||
// sleeping. This avoids duplicate registrations on normal boot where apps
|
// so the loginctl watcher alone is not enough.
|
||||||
// are still starting up and will register their own tray icons shortly.
|
go m.scheduleRecovery()
|
||||||
if timeSuspended() > 5*time.Second {
|
|
||||||
go m.scheduleRecovery()
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -95,21 +91,3 @@ func (m *Manager) Close() {
|
|||||||
}
|
}
|
||||||
log.Info("TrayRecovery manager closed")
|
log.Info("TrayRecovery manager closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeSuspended returns how long the system has spent in suspend since boot.
|
|
||||||
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
|
||||||
// CLOCK_MONOTONIC (excludes suspend).
|
|
||||||
func timeSuspended() time.Duration {
|
|
||||||
var bt, mt unix.Timespec
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
|
||||||
if diff < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return time.Duration(diff)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -43,9 +42,6 @@ type Model struct {
|
|||||||
sudoPassword string
|
sudoPassword string
|
||||||
existingConfigs []ExistingConfigInfo
|
existingConfigs []ExistingConfigInfo
|
||||||
fingerprintFailed bool
|
fingerprintFailed bool
|
||||||
|
|
||||||
availablePrivesc []privesc.Tool
|
|
||||||
selectedPrivesc int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(version string, logFilePath string) Model {
|
func NewModel(version string, logFilePath string) Model {
|
||||||
@@ -151,8 +147,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.updateGentooUseFlagsState(msg)
|
return m.updateGentooUseFlagsState(msg)
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.updateGentooGCCCheckState(msg)
|
return m.updateGentooGCCCheckState(msg)
|
||||||
case StateSelectPrivesc:
|
|
||||||
return m.updateSelectPrivescState(msg)
|
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.updateAuthMethodChoiceState(msg)
|
return m.updateAuthMethodChoiceState(msg)
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
@@ -195,8 +189,6 @@ func (m Model) View() string {
|
|||||||
return m.viewGentooUseFlags()
|
return m.viewGentooUseFlags()
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.viewGentooGCCCheck()
|
return m.viewGentooGCCCheck()
|
||||||
case StateSelectPrivesc:
|
|
||||||
return m.viewSelectPrivesc()
|
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.viewAuthMethodChoice()
|
return m.viewAuthMethodChoice()
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const (
|
|||||||
StateDependencyReview
|
StateDependencyReview
|
||||||
StateGentooUseFlags
|
StateGentooUseFlags
|
||||||
StateGentooGCCCheck
|
StateGentooGCCCheck
|
||||||
StateSelectPrivesc
|
|
||||||
StateAuthMethodChoice
|
StateAuthMethodChoice
|
||||||
StateFingerprintAuth
|
StateFingerprintAuth
|
||||||
StatePasswordPrompt
|
StatePasswordPrompt
|
||||||
|
|||||||
@@ -180,7 +180,16 @@ func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m.enterAuthPhase()
|
// Check if fingerprint is enabled
|
||||||
|
if checkFingerprintEnabled() {
|
||||||
|
m.state = StateAuthMethodChoice
|
||||||
|
m.selectedConfig = 0 // Default to fingerprint
|
||||||
|
return m, nil
|
||||||
|
} else {
|
||||||
|
m.state = StatePasswordPrompt
|
||||||
|
m.passwordInput.Focus()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateSelectWindowManager
|
m.state = StateSelectWindowManager
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -56,7 +56,14 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.state = StateGentooGCCCheck
|
m.state = StateGentooGCCCheck
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m.enterAuthPhase()
|
if checkFingerprintEnabled() {
|
||||||
|
m.state = StateAuthMethodChoice
|
||||||
|
m.selectedConfig = 0
|
||||||
|
} else {
|
||||||
|
m.state = StatePasswordPrompt
|
||||||
|
m.passwordInput.Focus()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||||
@@ -68,7 +75,14 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.selectedWM == 1 {
|
if m.selectedWM == 1 {
|
||||||
return m, m.checkGCCVersion()
|
return m, m.checkGCCVersion()
|
||||||
}
|
}
|
||||||
return m.enterAuthPhase()
|
if checkFingerprintEnabled() {
|
||||||
|
m.state = StateAuthMethodChoice
|
||||||
|
m.selectedConfig = 0
|
||||||
|
} else {
|
||||||
|
m.state = StatePasswordPrompt
|
||||||
|
m.passwordInput.Focus()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateDependencyReview
|
m.state = StateDependencyReview
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -275,7 +274,8 @@ func (m Model) delayThenReturn() tea.Cmd {
|
|||||||
|
|
||||||
func (m Model) tryFingerprint() tea.Cmd {
|
func (m Model) tryFingerprint() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
_ = privesc.ClearCache(context.Background())
|
clearCmd := exec.Command("sudo", "-k")
|
||||||
|
clearCmd.Run()
|
||||||
|
|
||||||
tmpDir := os.TempDir()
|
tmpDir := os.TempDir()
|
||||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||||
@@ -289,9 +289,15 @@ func (m Model) tryFingerprint() tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := privesc.ValidateWithAskpass(ctx, askpassScript); err != nil {
|
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
return passwordValidMsg{password: "", valid: true}
|
return passwordValidMsg{password: "", valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,9 +307,32 @@ func (m Model) validatePassword(password string) tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := privesc.ValidatePassword(ctx, password); err != nil {
|
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return passwordValidMsg{password: "", valid: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(stdin, "%s\n", password)
|
||||||
|
stdin.Close()
|
||||||
|
if err != nil {
|
||||||
|
return passwordValidMsg{password: "", valid: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return passwordValidMsg{password: "", valid: false}
|
||||||
|
}
|
||||||
|
return passwordValidMsg{password: "", valid: false}
|
||||||
|
}
|
||||||
|
|
||||||
return passwordValidMsg{password: password, valid: true}
|
return passwordValidMsg{password: password, valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
package tui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m Model) viewSelectPrivesc() string {
|
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
b.WriteString(m.renderBanner())
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(m.styles.Title.Render("Privilege Escalation Tool"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString(m.styles.Normal.Render("Multiple privilege tools are available. Choose one for installation:"))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
|
|
||||||
for i, t := range m.availablePrivesc {
|
|
||||||
label := fmt.Sprintf("%s — %s", t.Name(), privescToolDescription(t))
|
|
||||||
switch i {
|
|
||||||
case m.selectedPrivesc:
|
|
||||||
b.WriteString(m.styles.SelectedOption.Render("▶ " + label))
|
|
||||||
default:
|
|
||||||
b.WriteString(m.styles.Normal.Render(" " + label))
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Set %s=<tool> to skip this prompt in future runs.", privesc.EnvVar)))
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString(m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back"))
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) updateSelectPrivescState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
keyMsg, ok := msg.(tea.KeyMsg)
|
|
||||||
if !ok {
|
|
||||||
return m, m.listenForLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch keyMsg.String() {
|
|
||||||
case "up":
|
|
||||||
if m.selectedPrivesc > 0 {
|
|
||||||
m.selectedPrivesc--
|
|
||||||
}
|
|
||||||
case "down":
|
|
||||||
if m.selectedPrivesc < len(m.availablePrivesc)-1 {
|
|
||||||
m.selectedPrivesc++
|
|
||||||
}
|
|
||||||
case "enter":
|
|
||||||
chosen := m.availablePrivesc[m.selectedPrivesc]
|
|
||||||
if err := privesc.SetTool(chosen); err != nil {
|
|
||||||
m.err = fmt.Errorf("failed to select %s: %w", chosen.Name(), err)
|
|
||||||
m.state = StateError
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m.routeToAuthAfterPrivesc()
|
|
||||||
case "esc":
|
|
||||||
m.state = StateDependencyReview
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func privescToolDescription(t privesc.Tool) string {
|
|
||||||
switch t {
|
|
||||||
case privesc.ToolSudo:
|
|
||||||
return "classic sudo (supports password prompt in this installer)"
|
|
||||||
case privesc.ToolDoas:
|
|
||||||
return "OpenBSD-style doas (requires persist or nopass in /etc/doas.conf)"
|
|
||||||
case privesc.ToolRun0:
|
|
||||||
return "systemd run0 (authenticated via polkit)"
|
|
||||||
default:
|
|
||||||
return string(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// routeToAuthAfterPrivesc advances from the privesc-selection screen to the
|
|
||||||
// right auth flow. Sudo goes through the fingerprint/password path; doas and
|
|
||||||
// run0 skip password entry and proceed to install.
|
|
||||||
func (m Model) routeToAuthAfterPrivesc() (tea.Model, tea.Cmd) {
|
|
||||||
tool, err := privesc.Detect()
|
|
||||||
if err != nil {
|
|
||||||
m.err = err
|
|
||||||
m.state = StateError
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if tool == privesc.ToolSudo {
|
|
||||||
if checkFingerprintEnabled() {
|
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.sudoPassword = ""
|
|
||||||
m.packageProgress = packageInstallProgressMsg{}
|
|
||||||
m.state = StateInstallingPackages
|
|
||||||
m.isLoading = true
|
|
||||||
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
|
||||||
}
|
|
||||||
|
|
||||||
// enterAuthPhase is called when dependency review (or the Gentoo screens)
|
|
||||||
// finish. It either routes directly to the sudo/fingerprint flow or shows
|
|
||||||
// the privesc-tool selection screen when multiple tools are available and
|
|
||||||
// no $DMS_PRIVESC override is set.
|
|
||||||
func (m Model) enterAuthPhase() (tea.Model, tea.Cmd) {
|
|
||||||
tools := privesc.AvailableTools()
|
|
||||||
_, envSet := privesc.EnvOverride()
|
|
||||||
|
|
||||||
if len(tools) == 0 {
|
|
||||||
m.err = fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
|
||||||
m.state = StateError
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if envSet || len(tools) == 1 {
|
|
||||||
return m.routeToAuthAfterPrivesc()
|
|
||||||
}
|
|
||||||
|
|
||||||
m.availablePrivesc = tools
|
|
||||||
m.selectedPrivesc = 0
|
|
||||||
m.state = StateSelectPrivesc
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,12 @@ package version
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version"
|
mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCompareVersions(t *testing.T) {
|
func TestCompareVersions(t *testing.T) {
|
||||||
@@ -148,6 +150,76 @@ func TestGetCurrentDMSVersion_NotInstalled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentDMSVersion_GitTag(t *testing.T) {
|
||||||
|
if !utils.CommandExists("git") {
|
||||||
|
t.Skip("git not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
|
||||||
|
exec.Command("git", "init", dmsPath).Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||||
|
|
||||||
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "tag", "v0.1.0").Run()
|
||||||
|
|
||||||
|
version, err := GetCurrentDMSVersion()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCurrentDMSVersion() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if version != "v0.1.0" {
|
||||||
|
t.Errorf("Expected version v0.1.0, got %s", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentDMSVersion_GitBranch(t *testing.T) {
|
||||||
|
if !utils.CommandExists("git") {
|
||||||
|
t.Skip("git not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
|
||||||
|
exec.Command("git", "init", dmsPath).Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||||
|
|
||||||
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
|
version, err := GetCurrentDMSVersion()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCurrentDMSVersion() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
t.Error("Expected non-empty version")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(version) < 7 {
|
||||||
|
t.Errorf("Expected version with branch@commit format, got %s", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVersionInfo_IsGit(t *testing.T) {
|
func TestVersionInfo_IsGit(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
current string
|
current string
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
(import (
|
|
||||||
let
|
|
||||||
lock = builtins.fromJSON (builtins.readFile ../../flake.lock);
|
|
||||||
in
|
|
||||||
fetchTarball {
|
|
||||||
url =
|
|
||||||
lock.nodes.flake-compat.locked.url
|
|
||||||
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
|
||||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
|
||||||
}
|
|
||||||
) { src = ../..; }).defaultNix
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
(import (
|
|
||||||
let
|
|
||||||
lock = builtins.fromJSON (builtins.readFile ../../flake.lock);
|
|
||||||
in
|
|
||||||
fetchTarball {
|
|
||||||
url =
|
|
||||||
lock.nodes.flake-compat.locked.url
|
|
||||||
or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
|
||||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
|
||||||
}
|
|
||||||
) { src = ../..; }).shellNix
|
|
||||||
@@ -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
|
|
||||||
;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -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'")
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
-16
@@ -502,9 +502,6 @@ Notepad/scratchpad modal control for quick note-taking.
|
|||||||
- `open` - Show notepad modal
|
- `open` - Show notepad modal
|
||||||
- `close` - Hide notepad modal
|
- `close` - Hide notepad modal
|
||||||
- `toggle` - Toggle notepad modal visibility
|
- `toggle` - Toggle notepad modal visibility
|
||||||
- `expand` - Expand the active notepad width and open it if hidden
|
|
||||||
- `collapse` - Collapse the active notepad width without changing visibility
|
|
||||||
- `toggleExpand` - Toggle the active notepad width between collapsed and expanded
|
|
||||||
|
|
||||||
### Target: `dash`
|
### Target: `dash`
|
||||||
Dashboard popup control with tab selection for overview, media, and weather information.
|
Dashboard popup control with tab selection for overview, media, and weather information.
|
||||||
@@ -613,15 +610,6 @@ dms ipc call powermenu toggle
|
|||||||
# Open notepad
|
# Open notepad
|
||||||
dms ipc call notepad toggle
|
dms ipc call notepad toggle
|
||||||
|
|
||||||
# Open the active notepad expanded
|
|
||||||
dms ipc call notepad expand
|
|
||||||
|
|
||||||
# Collapse the active notepad width
|
|
||||||
dms ipc call notepad collapse
|
|
||||||
|
|
||||||
# Toggle the active notepad width
|
|
||||||
dms ipc call notepad toggleExpand
|
|
||||||
|
|
||||||
# Show dashboard with specific tabs
|
# Show dashboard with specific tabs
|
||||||
dms ipc call dash open overview
|
dms ipc call dash open overview
|
||||||
dms ipc call dash toggle media
|
dms ipc call dash toggle media
|
||||||
@@ -659,8 +647,6 @@ binds {
|
|||||||
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
|
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
|
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
|
||||||
Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; }
|
Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
Mod+Shift+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "expand"; }
|
|
||||||
Mod+Ctrl+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggleExpand"; }
|
|
||||||
Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; }
|
Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; }
|
||||||
XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; }
|
XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; }
|
||||||
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
|
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
|
||||||
@@ -672,8 +658,6 @@ binds {
|
|||||||
bind = SUPER, Space, exec, qs -c dms ipc call spotlight toggle
|
bind = SUPER, Space, exec, qs -c dms ipc call spotlight toggle
|
||||||
bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle
|
bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle
|
||||||
bind = SUPER, P, exec, qs -c dms ipc call notepad toggle
|
bind = SUPER, P, exec, qs -c dms ipc call notepad toggle
|
||||||
bind = SUPER SHIFT, P, exec, qs -c dms ipc call notepad expand
|
|
||||||
bind = SUPER CTRL, P, exec, qs -c dms ipc call notepad toggleExpand
|
|
||||||
bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle
|
bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle
|
||||||
bind = SUPER, slash, exec, qs -c dms ipc call hypr toggleBinds
|
bind = SUPER, slash, exec, qs -c dms ipc call hypr toggleBinds
|
||||||
bind = SUPER, Tab, exec, qs -c dms ipc call hypr toggleOverview
|
bind = SUPER, Tab, exec, qs -c dms ipc call hypr toggleOverview
|
||||||
|
|||||||
Generated
+8
-25
@@ -1,28 +1,12 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767039857,
|
|
||||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776169885,
|
"lastModified": 1771369470,
|
||||||
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
|
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
|
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -39,23 +23,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"quickshell": "quickshell"
|
"quickshell": "quickshell"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
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 = {
|
|
||||||
url = "github:NixOS/flake-compat";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
@@ -45,12 +41,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 +53,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 +69,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 +90,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 +236,5 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
nixosTests = forEachLinuxSystem (
|
|
||||||
system: pkgs:
|
|
||||||
import ./distro/nix/tests {
|
|
||||||
inherit
|
|
||||||
self
|
|
||||||
pkgs
|
|
||||||
;
|
|
||||||
lib = pkgs.lib;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,40 +5,121 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
|
||||||
// AnimVariants — central tuning for animation variants (Material/Fluent/Dynamic)
|
// AnimVariants — Central tuning for animation and Motion Effects variants
|
||||||
// and motion effects (Standard/Directional/Depth). Lookups are indexed by enum
|
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
|
||||||
// value: animationVariant 0=Material, 1=Fluent, 2=Dynamic; motionEffect
|
|
||||||
// 0=Standard, 1=Directional, 2=Depth.
|
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int _variant: (typeof SettingsData === "undefined") ? 0 : SettingsData.animationVariant
|
readonly property list<real> variantEnterCurve: {
|
||||||
readonly property int _effect: (typeof SettingsData === "undefined") ? 0 : SettingsData.motionEffect
|
if (typeof SettingsData === "undefined")
|
||||||
|
return Anims.expressiveDefaultSpatial;
|
||||||
|
switch (SettingsData.animationVariant) {
|
||||||
|
case 1:
|
||||||
|
return Anims.standardDecel;
|
||||||
|
case 2:
|
||||||
|
return Anims.expressiveFastSpatial;
|
||||||
|
default:
|
||||||
|
return Anims.expressiveDefaultSpatial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var _enterCurves: [Anims.expressiveDefaultSpatial, Anims.standardDecel, Anims.expressiveFastSpatial]
|
readonly property list<real> variantExitCurve: {
|
||||||
readonly property var _exitCurves: [Anims.emphasized, Anims.standard, Anims.emphasized]
|
if (typeof SettingsData === "undefined")
|
||||||
readonly property var _directionalExitCurves: [Anims.emphasized, Anims.emphasizedAccel, Anims.emphasizedAccel]
|
return Anims.emphasized;
|
||||||
readonly property var _enterDurationFactors: [1.0, 0.9, 1.08]
|
switch (SettingsData.animationVariant) {
|
||||||
readonly property var _exitDurationFactors: [1.0, 0.85, 0.92]
|
case 1:
|
||||||
readonly property var _cleanupPaddings: [50, 8, 24]
|
return Anims.standard;
|
||||||
readonly property var _effectScaleCollapsed: [0.96, 1.0, 0.88]
|
case 2:
|
||||||
readonly property var _effectAnimOffsets: [16, 144, 56]
|
return Anims.emphasized;
|
||||||
|
default:
|
||||||
|
return Anims.emphasized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readonly property list<real> variantEnterCurve: _enterCurves[_variant] || _enterCurves[0]
|
// Modal-specific entry curve
|
||||||
readonly property list<real> variantExitCurve: _exitCurves[_variant] || _exitCurves[0]
|
readonly property list<real> variantModalEnterCurve: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return Anims.expressiveDefaultSpatial;
|
||||||
|
if (isDirectionalEffect) {
|
||||||
|
if (SettingsData.animationVariant === 1)
|
||||||
|
return Anims.standardDecel;
|
||||||
|
if (SettingsData.animationVariant === 2)
|
||||||
|
return Anims.expressiveFastSpatial;
|
||||||
|
}
|
||||||
|
return variantEnterCurve;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property list<real> variantModalEnterCurve: isDirectionalEffect && _variant !== 0 ? (_enterCurves[_variant] || _enterCurves[0]) : variantEnterCurve
|
readonly property list<real> variantModalExitCurve: {
|
||||||
readonly property list<real> variantModalExitCurve: isDirectionalEffect ? (_directionalExitCurves[_variant] || _exitCurves[0]) : variantExitCurve
|
if (typeof SettingsData === "undefined")
|
||||||
|
return Anims.emphasized;
|
||||||
|
if (isDirectionalEffect) {
|
||||||
|
if (SettingsData.animationVariant === 1)
|
||||||
|
return Anims.emphasizedAccel;
|
||||||
|
if (SettingsData.animationVariant === 2)
|
||||||
|
return Anims.emphasizedAccel;
|
||||||
|
}
|
||||||
|
return variantExitCurve;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property list<real> variantPopoutEnterCurve: isDirectionalEffect ? (_variant === 0 ? Anims.standardDecel : (_enterCurves[_variant] || _enterCurves[0])) : variantEnterCurve
|
// Popout-specific entry curve
|
||||||
readonly property list<real> variantPopoutExitCurve: isDirectionalEffect ? (_directionalExitCurves[_variant] || _exitCurves[0]) : variantExitCurve
|
readonly property list<real> variantPopoutEnterCurve: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return Anims.expressiveDefaultSpatial;
|
||||||
|
if (isDirectionalEffect) {
|
||||||
|
if (SettingsData.animationVariant === 1)
|
||||||
|
return Anims.standardDecel;
|
||||||
|
if (SettingsData.animationVariant === 2)
|
||||||
|
return Anims.expressiveFastSpatial;
|
||||||
|
return Anims.standardDecel;
|
||||||
|
}
|
||||||
|
return variantEnterCurve;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property real variantEnterDurationFactor: _enterDurationFactors[_variant] !== undefined ? _enterDurationFactors[_variant] : 1.0
|
readonly property list<real> variantPopoutExitCurve: {
|
||||||
readonly property real variantExitDurationFactor: _exitDurationFactors[_variant] !== undefined ? _exitDurationFactors[_variant] : 1.0
|
if (typeof SettingsData === "undefined")
|
||||||
|
return Anims.emphasized;
|
||||||
|
if (isDirectionalEffect) {
|
||||||
|
if (SettingsData.animationVariant === 1)
|
||||||
|
return Anims.emphasizedAccel;
|
||||||
|
if (SettingsData.animationVariant === 2)
|
||||||
|
return Anims.emphasizedAccel;
|
||||||
|
}
|
||||||
|
return variantExitCurve;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real variantEnterDurationFactor: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 1.0;
|
||||||
|
switch (SettingsData.animationVariant) {
|
||||||
|
case 1:
|
||||||
|
return 0.9;
|
||||||
|
case 2:
|
||||||
|
return 1.08;
|
||||||
|
default:
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real variantExitDurationFactor: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 1.0;
|
||||||
|
switch (SettingsData.animationVariant) {
|
||||||
|
case 1:
|
||||||
|
return 0.85;
|
||||||
|
case 2:
|
||||||
|
return 0.92;
|
||||||
|
default:
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
|
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
|
||||||
readonly property real variantOpacityDurationScale: _variant === 1 ? 0.55 : 1.0
|
readonly property real variantOpacityDurationScale: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 1.0;
|
||||||
|
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
function variantDuration(baseDuration, entering) {
|
function variantDuration(baseDuration, entering) {
|
||||||
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
|
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
|
||||||
@@ -46,17 +127,53 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function variantExitCleanupPadding() {
|
function variantExitCleanupPadding() {
|
||||||
return _cleanupPaddings[_effect] !== undefined ? _cleanupPaddings[_effect] : 50;
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 50;
|
||||||
|
switch (SettingsData.motionEffect) {
|
||||||
|
case 1:
|
||||||
|
return 8;
|
||||||
|
case 2:
|
||||||
|
return 24;
|
||||||
|
default:
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function variantCloseInterval(baseDuration) {
|
function variantCloseInterval(baseDuration) {
|
||||||
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool isDirectionalEffect: isConnectedEffect || _effect === 1
|
readonly property bool isDirectionalEffect: isConnectedEffect
|
||||||
readonly property bool isDepthEffect: _effect === 2
|
|| (typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1)
|
||||||
readonly property bool isConnectedEffect: (typeof SettingsData !== "undefined") && SettingsData.connectedFrameModeActive
|
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
|
||||||
|
readonly property bool isConnectedEffect: typeof SettingsData !== "undefined"
|
||||||
|
&& SettingsData.frameEnabled
|
||||||
|
&& SettingsData.motionEffect === 1
|
||||||
|
&& SettingsData.directionalAnimationMode === 3
|
||||||
|
|
||||||
readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96
|
readonly property real effectScaleCollapsed: {
|
||||||
readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 0.96;
|
||||||
|
switch (SettingsData.motionEffect) {
|
||||||
|
case 1:
|
||||||
|
return 1.0;
|
||||||
|
case 2:
|
||||||
|
return 0.88;
|
||||||
|
default:
|
||||||
|
return 0.96;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real effectAnimOffset: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 16;
|
||||||
|
switch (SettingsData.motionEffect) {
|
||||||
|
case 1:
|
||||||
|
return 144;
|
||||||
|
case 2:
|
||||||
|
return 56;
|
||||||
|
default:
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ Singleton {
|
|||||||
property real popoutAnimX: 0
|
property real popoutAnimX: 0
|
||||||
property real popoutAnimY: 0
|
property real popoutAnimY: 0
|
||||||
property string popoutScreen: ""
|
property string popoutScreen: ""
|
||||||
property bool popoutOmitStartConnector: false
|
|
||||||
property bool popoutOmitEndConnector: false
|
|
||||||
|
|
||||||
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
|
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
|
||||||
property var dockStates: ({})
|
property var dockStates: ({})
|
||||||
@@ -38,13 +36,6 @@ Singleton {
|
|||||||
// Dock slide offsets — hot-path updates separated from full geometry state
|
// Dock slide offsets — hot-path updates separated from full geometry state
|
||||||
property var dockSlides: ({})
|
property var dockSlides: ({})
|
||||||
|
|
||||||
function _cloneDict(src) {
|
|
||||||
const next = {};
|
|
||||||
for (const k in src)
|
|
||||||
next[k] = src[k];
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasPopoutOwner(claimId) {
|
function hasPopoutOwner(claimId) {
|
||||||
return !!claimId && popoutOwnerId === claimId;
|
return !!claimId && popoutOwnerId === claimId;
|
||||||
}
|
}
|
||||||
@@ -79,10 +70,6 @@ Singleton {
|
|||||||
popoutAnimY = Number(state.animY);
|
popoutAnimY = Number(state.animY);
|
||||||
if (state.screen !== undefined)
|
if (state.screen !== undefined)
|
||||||
popoutScreen = state.screen || "";
|
popoutScreen = state.screen || "";
|
||||||
if (state.omitStartConnector !== undefined)
|
|
||||||
popoutOmitStartConnector = !!state.omitStartConnector;
|
|
||||||
if (state.omitEndConnector !== undefined)
|
|
||||||
popoutOmitEndConnector = !!state.omitEndConnector;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -101,8 +88,6 @@ Singleton {
|
|||||||
popoutAnimX = 0;
|
popoutAnimX = 0;
|
||||||
popoutAnimY = 0;
|
popoutAnimY = 0;
|
||||||
popoutScreen = "";
|
popoutScreen = "";
|
||||||
popoutOmitStartConnector = false;
|
|
||||||
popoutOmitEndConnector = false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,30 +107,11 @@ Singleton {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPopoutBody(claimId, bodyX, bodyY, bodyW, bodyH) {
|
function _cloneDockStates() {
|
||||||
if (!hasPopoutOwner(claimId))
|
const next = {};
|
||||||
return false;
|
for (const screenName in dockStates)
|
||||||
if (bodyX !== undefined) {
|
next[screenName] = dockStates[screenName];
|
||||||
const nextX = Number(bodyX);
|
return next;
|
||||||
if (!isNaN(nextX) && popoutBodyX !== nextX)
|
|
||||||
popoutBodyX = nextX;
|
|
||||||
}
|
|
||||||
if (bodyY !== undefined) {
|
|
||||||
const nextY = Number(bodyY);
|
|
||||||
if (!isNaN(nextY) && popoutBodyY !== nextY)
|
|
||||||
popoutBodyY = nextY;
|
|
||||||
}
|
|
||||||
if (bodyW !== undefined) {
|
|
||||||
const nextW = Number(bodyW);
|
|
||||||
if (!isNaN(nextW) && popoutBodyW !== nextW)
|
|
||||||
popoutBodyW = nextW;
|
|
||||||
}
|
|
||||||
if (bodyH !== undefined) {
|
|
||||||
const nextH = Number(bodyH);
|
|
||||||
if (!isNaN(nextH) && popoutBodyH !== nextH)
|
|
||||||
popoutBodyH = nextH;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _normalizeDockState(state) {
|
function _normalizeDockState(state) {
|
||||||
@@ -161,22 +127,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);
|
const next = _cloneDockStates();
|
||||||
if (_sameDockState(dockStates[screenName], normalized))
|
next[screenName] = _normalizeDockState(state);
|
||||||
return true;
|
|
||||||
|
|
||||||
const next = _cloneDict(dockStates);
|
|
||||||
next[screenName] = normalized;
|
|
||||||
dockStates = next;
|
dockStates = next;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -185,13 +141,15 @@ Singleton {
|
|||||||
if (!screenName || !dockStates[screenName])
|
if (!screenName || !dockStates[screenName])
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const next = _cloneDict(dockStates);
|
const next = _cloneDockStates();
|
||||||
delete next[screenName];
|
delete next[screenName];
|
||||||
dockStates = next;
|
dockStates = next;
|
||||||
|
|
||||||
// Also clear corresponding slide
|
// Also clear corresponding slide
|
||||||
if (dockSlides[screenName]) {
|
if (dockSlides[screenName]) {
|
||||||
const nextSlides = _cloneDict(dockSlides);
|
const nextSlides = {};
|
||||||
|
for (const k in dockSlides)
|
||||||
|
nextSlides[k] = dockSlides[k];
|
||||||
delete nextSlides[screenName];
|
delete nextSlides[screenName];
|
||||||
dockSlides = nextSlides;
|
dockSlides = nextSlides;
|
||||||
}
|
}
|
||||||
@@ -201,33 +159,34 @@ Singleton {
|
|||||||
function setDockSlide(screenName, x, y) {
|
function setDockSlide(screenName, x, y) {
|
||||||
if (!screenName)
|
if (!screenName)
|
||||||
return false;
|
return false;
|
||||||
const numX = Number(x);
|
const next = {};
|
||||||
const numY = Number(y);
|
for (const k in dockSlides)
|
||||||
const cur = dockSlides[screenName];
|
next[k] = dockSlides[k];
|
||||||
if (cur && Math.abs(cur.x - numX) < 0.5 && Math.abs(cur.y - numY) < 0.5)
|
next[screenName] = { "x": Number(x), "y": Number(y) };
|
||||||
return true;
|
|
||||||
const next = _cloneDict(dockSlides);
|
|
||||||
next[screenName] = {
|
|
||||||
"x": numX,
|
|
||||||
"y": numY
|
|
||||||
};
|
|
||||||
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",
|
||||||
"bodyX": 0,
|
"bodyX": 0,
|
||||||
"bodyY": 0,
|
"bodyY": 0,
|
||||||
"bodyW": 0,
|
"bodyW": 0,
|
||||||
"bodyH": 0,
|
"bodyH": 0
|
||||||
"omitStartConnector": false,
|
})
|
||||||
"omitEndConnector": false
|
|
||||||
})
|
|
||||||
|
|
||||||
property var notificationStates: ({})
|
property var notificationStates: ({})
|
||||||
|
|
||||||
|
function _cloneNotificationStates() {
|
||||||
|
const next = {};
|
||||||
|
for (const screenName in notificationStates)
|
||||||
|
next[screenName] = notificationStates[screenName];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function _normalizeNotificationState(state) {
|
function _normalizeNotificationState(state) {
|
||||||
return {
|
return {
|
||||||
"visible": !!(state && state.visible),
|
"visible": !!(state && state.visible),
|
||||||
@@ -235,34 +194,16 @@ Singleton {
|
|||||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
||||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
||||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
||||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
|
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0)
|
||||||
"omitStartConnector": !!(state && state.omitStartConnector),
|
|
||||||
"omitEndConnector": !!(state && state.omitEndConnector)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _sameNotificationGeometry(a, b) {
|
|
||||||
if (!a || !b)
|
|
||||||
return false;
|
|
||||||
return Math.abs(Number(a.bodyX) - Number(b.bodyX)) < 0.5 && Math.abs(Number(a.bodyY) - Number(b.bodyY)) < 0.5 && Math.abs(Number(a.bodyW) - Number(b.bodyW)) < 0.5 && Math.abs(Number(a.bodyH) - Number(b.bodyH)) < 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _sameNotificationState(a, b) {
|
|
||||||
if (!a || !b)
|
|
||||||
return false;
|
|
||||||
return a.visible === b.visible && a.barSide === b.barSide && a.omitStartConnector === b.omitStartConnector && a.omitEndConnector === b.omitEndConnector && _sameNotificationGeometry(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNotificationState(screenName, state) {
|
function setNotificationState(screenName, state) {
|
||||||
if (!screenName || !state)
|
if (!screenName || !state)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const normalized = _normalizeNotificationState(state);
|
const next = _cloneNotificationStates();
|
||||||
if (_sameNotificationState(notificationStates[screenName], normalized))
|
next[screenName] = _normalizeNotificationState(state);
|
||||||
return true;
|
|
||||||
|
|
||||||
const next = _cloneDict(notificationStates);
|
|
||||||
next[screenName] = normalized;
|
|
||||||
notificationStates = next;
|
notificationStates = next;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -271,211 +212,9 @@ Singleton {
|
|||||||
if (!screenName || !notificationStates[screenName])
|
if (!screenName || !notificationStates[screenName])
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const next = _cloneDict(notificationStates);
|
const next = _cloneNotificationStates();
|
||||||
delete next[screenName];
|
delete next[screenName];
|
||||||
notificationStates = next;
|
notificationStates = next;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DankModal / DankLauncherV2Modal State
|
|
||||||
readonly property var emptyModalState: ({
|
|
||||||
"visible": false,
|
|
||||||
"barSide": "bottom",
|
|
||||||
"bodyX": 0,
|
|
||||||
"bodyY": 0,
|
|
||||||
"bodyW": 0,
|
|
||||||
"bodyH": 0,
|
|
||||||
"animX": 0,
|
|
||||||
"animY": 0,
|
|
||||||
"omitStartConnector": false,
|
|
||||||
"omitEndConnector": false
|
|
||||||
})
|
|
||||||
|
|
||||||
property var modalStates: ({})
|
|
||||||
|
|
||||||
function _normalizeModalState(state) {
|
|
||||||
return {
|
|
||||||
"visible": !!(state && state.visible),
|
|
||||||
"barSide": state && state.barSide ? state.barSide : "bottom",
|
|
||||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
|
||||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
|
||||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
|
||||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
|
|
||||||
"animX": Number(state && state.animX !== undefined ? state.animX : 0),
|
|
||||||
"animY": Number(state && state.animY !== undefined ? state.animY : 0),
|
|
||||||
"omitStartConnector": !!(state && state.omitStartConnector),
|
|
||||||
"omitEndConnector": !!(state && state.omitEndConnector)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function _sameModalGeometry(a, b) {
|
|
||||||
if (!a || !b)
|
|
||||||
return false;
|
|
||||||
return Math.abs(Number(a.bodyX) - Number(b.bodyX)) < 0.5 && Math.abs(Number(a.bodyY) - Number(b.bodyY)) < 0.5 && Math.abs(Number(a.bodyW) - Number(b.bodyW)) < 0.5 && Math.abs(Number(a.bodyH) - Number(b.bodyH)) < 0.5 && Math.abs(Number(a.animX) - Number(b.animX)) < 0.5 && Math.abs(Number(a.animY) - Number(b.animY)) < 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _sameModalState(a, b) {
|
|
||||||
if (!a || !b)
|
|
||||||
return false;
|
|
||||||
return a.visible === b.visible && a.barSide === b.barSide && a.omitStartConnector === b.omitStartConnector && a.omitEndConnector === b.omitEndConnector && _sameModalGeometry(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setModalState(screenName, state) {
|
|
||||||
if (!screenName || !state)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const normalized = _normalizeModalState(state);
|
|
||||||
if (_sameModalState(modalStates[screenName], normalized))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const next = _cloneDict(modalStates);
|
|
||||||
next[screenName] = normalized;
|
|
||||||
modalStates = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearModalState(screenName) {
|
|
||||||
if (!screenName || !modalStates[screenName])
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const next = _cloneDict(modalStates);
|
|
||||||
delete next[screenName];
|
|
||||||
modalStates = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setModalAnim(screenName, animX, animY) {
|
|
||||||
const cur = screenName ? modalStates[screenName] : null;
|
|
||||||
if (!cur)
|
|
||||||
return false;
|
|
||||||
const nax = animX !== undefined ? Number(animX) : cur.animX;
|
|
||||||
const nay = animY !== undefined ? Number(animY) : cur.animY;
|
|
||||||
if (Math.abs(nax - cur.animX) < 0.5 && Math.abs(nay - cur.animY) < 0.5)
|
|
||||||
return false;
|
|
||||||
const next = _cloneDict(modalStates);
|
|
||||||
next[screenName] = Object.assign({}, cur, {
|
|
||||||
"animX": nax,
|
|
||||||
"animY": nay
|
|
||||||
});
|
|
||||||
modalStates = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setModalBody(screenName, bodyX, bodyY, bodyW, bodyH) {
|
|
||||||
const cur = screenName ? modalStates[screenName] : null;
|
|
||||||
if (!cur)
|
|
||||||
return false;
|
|
||||||
const nx = bodyX !== undefined ? Number(bodyX) : cur.bodyX;
|
|
||||||
const ny = bodyY !== undefined ? Number(bodyY) : cur.bodyY;
|
|
||||||
const nw = bodyW !== undefined ? Number(bodyW) : cur.bodyW;
|
|
||||||
const nh = bodyH !== undefined ? Number(bodyH) : cur.bodyH;
|
|
||||||
if (Math.abs(nx - cur.bodyX) < 0.5 && Math.abs(ny - cur.bodyY) < 0.5 && Math.abs(nw - cur.bodyW) < 0.5 && Math.abs(nh - cur.bodyH) < 0.5)
|
|
||||||
return false;
|
|
||||||
const next = _cloneDict(modalStates);
|
|
||||||
next[screenName] = Object.assign({}, cur, {
|
|
||||||
"bodyX": nx,
|
|
||||||
"bodyY": ny,
|
|
||||||
"bodyW": nw,
|
|
||||||
"bodyH": nh
|
|
||||||
});
|
|
||||||
modalStates = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
property var dockRetractRequests: ({})
|
|
||||||
|
|
||||||
function requestDockRetract(requesterId, screenName, side) {
|
|
||||||
if (!requesterId || !screenName || !side)
|
|
||||||
return false;
|
|
||||||
const existing = dockRetractRequests[requesterId];
|
|
||||||
if (existing && existing.screenName === screenName && existing.side === side)
|
|
||||||
return true;
|
|
||||||
const next = _cloneDict(dockRetractRequests);
|
|
||||||
next[requesterId] = {
|
|
||||||
"screenName": screenName,
|
|
||||||
"side": side
|
|
||||||
};
|
|
||||||
dockRetractRequests = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function releaseDockRetract(requesterId) {
|
|
||||||
if (!requesterId || !dockRetractRequests[requesterId])
|
|
||||||
return false;
|
|
||||||
const next = _cloneDict(dockRetractRequests);
|
|
||||||
delete next[requesterId];
|
|
||||||
dockRetractRequests = next;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dockRetractActiveForSide(screenName, side) {
|
|
||||||
if (!screenName || !side)
|
|
||||||
return false;
|
|
||||||
for (const k in dockRetractRequests) {
|
|
||||||
const r = dockRetractRequests[k];
|
|
||||||
if (r && r.screenName === screenName && r.side === side)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
// Geometry for connected-frame arc connectors.
|
|
||||||
// `barSide` is one of "top" | "bottom" | "left" | "right" — the edge where the
|
|
||||||
// host bar/dock sits. `placement` is "left" (start) or "right" (end) of the
|
|
||||||
// body's far edge. `radius` is the connector's arc radius. `spacing` is the
|
|
||||||
// gap between the host edge and the body.
|
|
||||||
|
|
||||||
function isVertical(barSide) {
|
|
||||||
return barSide === "left" || barSide === "right";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHorizontal(barSide) {
|
|
||||||
return barSide === "top" || barSide === "bottom";
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectorWidth(barSide, spacing, radius) {
|
|
||||||
return isVertical(barSide) ? (spacing + radius) : radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectorHeight(barSide, spacing, radius) {
|
|
||||||
return isVertical(barSide) ? radius : (spacing + radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
function seamX(barSide, baseX, bodyWidth, placement) {
|
|
||||||
if (!isVertical(barSide))
|
|
||||||
return placement === "left" ? baseX : baseX + bodyWidth;
|
|
||||||
return barSide === "left" ? baseX : baseX + bodyWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
function seamY(barSide, baseY, bodyHeight, placement) {
|
|
||||||
if (barSide === "top")
|
|
||||||
return baseY;
|
|
||||||
if (barSide === "bottom")
|
|
||||||
return baseY + bodyHeight;
|
|
||||||
return placement === "left" ? baseY : baseY + bodyHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectorX(barSide, baseX, bodyWidth, placement, spacing, radius) {
|
|
||||||
var s = seamX(barSide, baseX, bodyWidth, placement);
|
|
||||||
var w = connectorWidth(barSide, spacing, radius);
|
|
||||||
if (!isVertical(barSide))
|
|
||||||
return placement === "left" ? s - w : s;
|
|
||||||
return barSide === "left" ? s : s - w;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectorY(barSide, baseY, bodyHeight, placement, spacing, radius) {
|
|
||||||
var s = seamY(barSide, baseY, bodyHeight, placement);
|
|
||||||
var h = connectorHeight(barSide, spacing, radius);
|
|
||||||
if (barSide === "top")
|
|
||||||
return s;
|
|
||||||
if (barSide === "bottom")
|
|
||||||
return s - h;
|
|
||||||
return placement === "left" ? s - h : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which corner of the connector's bounding rect hosts the concave arc that
|
|
||||||
// carves into the body. Used for arc-sweep orientation.
|
|
||||||
function arcCorner(barSide, placement) {
|
|
||||||
var left = placement === "left";
|
|
||||||
if (barSide === "top")
|
|
||||||
return left ? "bottomLeft" : "bottomRight";
|
|
||||||
if (barSide === "bottom")
|
|
||||||
return left ? "topLeft" : "topRight";
|
|
||||||
if (barSide === "left")
|
|
||||||
return left ? "topRight" : "bottomRight";
|
|
||||||
return left ? "topLeft" : "bottomLeft";
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
|
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
|
||||||
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
|
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
|
||||||
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
|
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
|
||||||
{ id: "spawn dms ipc call notepad expand", label: "Notepad: Expand" },
|
|
||||||
{ id: "spawn dms ipc call notepad collapse", label: "Notepad: Collapse" },
|
|
||||||
{ id: "spawn dms ipc call notepad toggleExpand", label: "Notepad: Toggle Expand" },
|
|
||||||
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
|
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
|
||||||
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
|
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
|
||||||
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
|
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
|
||||||
@@ -161,16 +158,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)" },
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function expandTilde(path: string): string {
|
function expandTilde(path: string): string {
|
||||||
if (!path.startsWith("~"))
|
return strip(path.replace("~", stringify(root.home)));
|
||||||
return path;
|
|
||||||
return strip(root.home) + path.substring(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortenHome(path: string): string {
|
function shortenHome(path: string): string {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -30,60 +29,9 @@ Singleton {
|
|||||||
|
|
||||||
property bool isLightMode: false
|
property bool isLightMode: false
|
||||||
property bool doNotDisturb: false
|
property bool doNotDisturb: false
|
||||||
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 {
|
|
||||||
id: dndExpireTimer
|
|
||||||
repeat: false
|
|
||||||
running: false
|
|
||||||
onTriggered: root.setDoNotDisturb(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function _armDndExpireTimer() {
|
|
||||||
dndExpireTimer.stop();
|
|
||||||
if (!doNotDisturb || doNotDisturbUntil <= 0)
|
|
||||||
return;
|
|
||||||
const remaining = doNotDisturbUntil - Date.now();
|
|
||||||
if (remaining <= 0) {
|
|
||||||
setDoNotDisturb(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dndExpireTimer.interval = remaining;
|
|
||||||
dndExpireTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDoNotDisturbChanged: _armDndExpireTimer()
|
|
||||||
onDoNotDisturbUntilChanged: _armDndExpireTimer()
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: osdSuppressTimer
|
id: osdSuppressTimer
|
||||||
interval: 2000
|
interval: 2000
|
||||||
@@ -101,7 +49,6 @@ Singleton {
|
|||||||
function onSessionResumed() {
|
function onSessionResumed() {
|
||||||
root.suppressOSD = true;
|
root.suppressOSD = true;
|
||||||
osdSuppressTimer.restart();
|
osdSuppressTimer.restart();
|
||||||
root._applyDndExpirySanity();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +190,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Store.parse(root, obj);
|
Store.parse(root, obj);
|
||||||
_applyDndExpirySanity();
|
|
||||||
|
|
||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
@@ -258,7 +204,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,7 +271,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Store.parse(root, obj);
|
Store.parse(root, obj);
|
||||||
_applyDndExpirySanity();
|
|
||||||
|
|
||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
@@ -338,21 +283,11 @@ 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _applyDndExpirySanity() {
|
|
||||||
if (doNotDisturb && doNotDisturbUntil > 0 && Date.now() >= doNotDisturbUntil) {
|
|
||||||
doNotDisturb = false;
|
|
||||||
doNotDisturbUntil = 0;
|
|
||||||
} else if (!doNotDisturb && doNotDisturbUntil !== 0) {
|
|
||||||
doNotDisturbUntil = 0;
|
|
||||||
}
|
|
||||||
_armDndExpireTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
if (isGreeterMode || _parseError || !_hasLoaded)
|
if (isGreeterMode || _parseError || !_hasLoaded)
|
||||||
return;
|
return;
|
||||||
@@ -422,21 +357,8 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDoNotDisturb(enabled, durationMinutes) {
|
function setDoNotDisturb(enabled) {
|
||||||
const minutes = Number(durationMinutes) || 0;
|
|
||||||
doNotDisturb = enabled;
|
doNotDisturb = enabled;
|
||||||
doNotDisturbUntil = (enabled && minutes > 0) ? Date.now() + minutes * 60 * 1000 : 0;
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDoNotDisturbUntilTimestamp(timestampMs) {
|
|
||||||
const target = Number(timestampMs) || 0;
|
|
||||||
if (target <= Date.now()) {
|
|
||||||
setDoNotDisturb(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
doNotDisturb = true;
|
|
||||||
doNotDisturbUntil = target;
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +475,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
log.warn("Screen not found");
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +572,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
log.warn("Screen not found");
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +603,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
log.warn("Screen not found");
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,7 +634,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
log.warn("Screen not found");
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +665,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screen) {
|
if (!screen) {
|
||||||
log.warn("Screen not found");
|
console.warn("SessionData: Screen not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,32 +235,29 @@ Singleton {
|
|||||||
onFrameShowOnOverviewChanged: saveSettings()
|
onFrameShowOnOverviewChanged: saveSettings()
|
||||||
property bool frameBlurEnabled: true
|
property bool frameBlurEnabled: true
|
||||||
onFrameBlurEnabledChanged: saveSettings()
|
onFrameBlurEnabledChanged: saveSettings()
|
||||||
property bool frameCloseGaps: true
|
property int previousDirectionalMode: 1
|
||||||
onFrameCloseGapsChanged: saveSettings()
|
onPreviousDirectionalModeChanged: saveSettings()
|
||||||
property string frameLauncherEmergeSide: "bottom"
|
|
||||||
onFrameLauncherEmergeSideChanged: saveSettings()
|
|
||||||
property bool frameLauncherArcExtender: false
|
|
||||||
onFrameLauncherArcExtenderChanged: saveSettings()
|
|
||||||
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
|
||||||
property string frameMode: "separate"
|
|
||||||
onFrameModeChanged: 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;
|
||||||
_reconcileConnectedFrameBarStyles();
|
if (connectedFrameModeActive) {
|
||||||
|
_captureConnectedFrameBarStyleBackups(barConfigs, true);
|
||||||
|
_enforceConnectedModeBarStyleReset();
|
||||||
|
} else {
|
||||||
|
_restoreConnectedFrameBarStyleBackups();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color effectiveFrameColor: {
|
readonly property color effectiveFrameColor: {
|
||||||
const fc = frameColor;
|
const fc = frameColor;
|
||||||
if (!fc || fc === "default")
|
if (!fc || fc === "default") return Theme.surfaceContainer;
|
||||||
return Theme.surfaceContainer;
|
if (fc === "primary") return Theme.primary;
|
||||||
if (fc === "primary")
|
if (fc === "surface") return Theme.surface;
|
||||||
return Theme.primary;
|
|
||||||
if (fc === "surface")
|
|
||||||
return Theme.surface;
|
|
||||||
return fc;
|
return fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +274,6 @@ 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 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
|
||||||
@@ -516,13 +507,11 @@ Singleton {
|
|||||||
property int acSuspendTimeout: 0
|
property int acSuspendTimeout: 0
|
||||||
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||||
property string acProfileName: ""
|
property string acProfileName: ""
|
||||||
property int acPostLockMonitorTimeout: 0
|
|
||||||
property int batteryMonitorTimeout: 0
|
property int batteryMonitorTimeout: 0
|
||||||
property int batteryLockTimeout: 0
|
property int batteryLockTimeout: 0
|
||||||
property int batterySuspendTimeout: 0
|
property int batterySuspendTimeout: 0
|
||||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||||
property string batteryProfileName: ""
|
property string batteryProfileName: ""
|
||||||
property int batteryPostLockMonitorTimeout: 0
|
|
||||||
property int batteryChargeLimit: 100
|
property int batteryChargeLimit: 100
|
||||||
property bool lockBeforeSuspend: false
|
property bool lockBeforeSuspend: false
|
||||||
property bool loginctlLockIntegration: true
|
property bool loginctlLockIntegration: true
|
||||||
@@ -558,7 +547,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 +599,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 +691,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 +1322,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 +1342,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 +1364,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 +1423,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 +1440,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 {
|
||||||
@@ -1561,8 +1540,43 @@ Singleton {
|
|||||||
updateBarConfigs();
|
updateBarConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeroes out connected-mode-hostile fields (shadow, square/goth corners, border).
|
function _reconcileConnectedFrameBarStyles() {
|
||||||
// Returns { configs, changed } — `configs` is the same ref when no change.
|
if (connectedFrameModeActive) {
|
||||||
|
if (!_hasConnectedFrameBarStyleBackups())
|
||||||
|
_captureConnectedFrameBarStyleBackups(barConfigs, true);
|
||||||
|
_enforceConnectedModeBarStyleReset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_restoreConnectedFrameBarStyleBackups();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _sanitizeBarConfigForConnectedFrame(config) {
|
||||||
|
if (!connectedFrameModeActive || !config)
|
||||||
|
return config;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
const sanitized = Object.assign({}, config);
|
||||||
|
|
||||||
|
if ((sanitized.shadowIntensity ?? 0) !== 0) {
|
||||||
|
sanitized.shadowIntensity = 0;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (sanitized.squareCorners ?? false) {
|
||||||
|
sanitized.squareCorners = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (sanitized.gothCornersEnabled ?? false) {
|
||||||
|
sanitized.gothCornersEnabled = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (sanitized.borderEnabled ?? false) {
|
||||||
|
sanitized.borderEnabled = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? sanitized : config;
|
||||||
|
}
|
||||||
|
|
||||||
function _sanitizeBarConfigsForConnectedFrame(configs) {
|
function _sanitizeBarConfigsForConnectedFrame(configs) {
|
||||||
if (!connectedFrameModeActive || !Array.isArray(configs))
|
if (!connectedFrameModeActive || !Array.isArray(configs))
|
||||||
return {
|
return {
|
||||||
@@ -1570,53 +1584,26 @@ Singleton {
|
|||||||
"changed": false
|
"changed": false
|
||||||
};
|
};
|
||||||
|
|
||||||
let anyChanged = false;
|
let changed = false;
|
||||||
const out = configs.map(cfg => {
|
const sanitizedConfigs = configs.map(config => {
|
||||||
if (!cfg)
|
const sanitized = _sanitizeBarConfigForConnectedFrame(config);
|
||||||
return cfg;
|
if (sanitized !== config)
|
||||||
let dirty = false;
|
changed = true;
|
||||||
const s = Object.assign({}, cfg);
|
return sanitized;
|
||||||
if ((s.shadowIntensity ?? 0) !== 0) {
|
|
||||||
s.shadowIntensity = 0;
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
if (s.squareCorners ?? false) {
|
|
||||||
s.squareCorners = false;
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
if (s.gothCornersEnabled ?? false) {
|
|
||||||
s.gothCornersEnabled = false;
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
if (s.borderEnabled ?? false) {
|
|
||||||
s.borderEnabled = false;
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
if (dirty)
|
|
||||||
anyChanged = true;
|
|
||||||
return dirty ? s : cfg;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"configs": anyChanged ? out : configs,
|
"configs": changed ? sanitizedConfigs : configs,
|
||||||
"changed": anyChanged
|
"changed": changed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single entry point for connected-mode bar-style state.
|
function _enforceConnectedModeBarStyleReset() {
|
||||||
// active → capture backups (if not yet) and sanitize bar configs
|
|
||||||
// !active → restore backups
|
|
||||||
function _reconcileConnectedFrameBarStyles() {
|
|
||||||
if (!connectedFrameModeActive) {
|
|
||||||
_restoreConnectedFrameBarStyleBackups();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!_hasConnectedFrameBarStyleBackups())
|
|
||||||
_captureConnectedFrameBarStyleBackups(barConfigs, true);
|
|
||||||
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
|
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
|
||||||
if (result.changed) {
|
if (!result.changed)
|
||||||
barConfigs = result.configs;
|
return;
|
||||||
updateBarConfigs();
|
barConfigs = result.configs;
|
||||||
}
|
updateBarConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectAvailableIconThemes() {
|
function detectAvailableIconThemes() {
|
||||||
@@ -2181,77 +2168,48 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveBarEdgeForScreen(screen) {
|
function getActiveBarEdgeForScreen(screen) {
|
||||||
if (!screen)
|
if (!screen) return "";
|
||||||
return "";
|
|
||||||
for (var i = 0; i < barConfigs.length; i++) {
|
for (var i = 0; i < barConfigs.length; i++) {
|
||||||
var bc = barConfigs[i];
|
var bc = barConfigs[i];
|
||||||
if (!bc.enabled)
|
if (!bc.enabled) continue;
|
||||||
continue;
|
|
||||||
var prefs = bc.screenPreferences || ["all"];
|
var prefs = bc.screenPreferences || ["all"];
|
||||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||||
continue;
|
|
||||||
switch (bc.position ?? 0) {
|
switch (bc.position ?? 0) {
|
||||||
case SettingsData.Position.Top:
|
case SettingsData.Position.Top: return "top";
|
||||||
return "top";
|
case SettingsData.Position.Bottom: return "bottom";
|
||||||
case SettingsData.Position.Bottom:
|
case SettingsData.Position.Left: return "left";
|
||||||
return "bottom";
|
case SettingsData.Position.Right: return "right";
|
||||||
case SettingsData.Position.Left:
|
|
||||||
return "left";
|
|
||||||
case SettingsData.Position.Right:
|
|
||||||
return "right";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveBarEdgesForScreen(screen) {
|
function getActiveBarEdgesForScreen(screen) {
|
||||||
if (!screen)
|
if (!screen) return [];
|
||||||
return [];
|
|
||||||
var edges = [];
|
var edges = [];
|
||||||
for (var i = 0; i < barConfigs.length; i++) {
|
for (var i = 0; i < barConfigs.length; i++) {
|
||||||
var bc = barConfigs[i];
|
var bc = barConfigs[i];
|
||||||
if (!bc.enabled)
|
if (!bc.enabled) continue;
|
||||||
continue;
|
|
||||||
var prefs = bc.screenPreferences || ["all"];
|
var prefs = bc.screenPreferences || ["all"];
|
||||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||||
continue;
|
|
||||||
switch (bc.position ?? 0) {
|
switch (bc.position ?? 0) {
|
||||||
case SettingsData.Position.Top:
|
case SettingsData.Position.Top: edges.push("top"); break;
|
||||||
edges.push("top");
|
case SettingsData.Position.Bottom: edges.push("bottom"); break;
|
||||||
break;
|
case SettingsData.Position.Left: edges.push("left"); break;
|
||||||
case SettingsData.Position.Bottom:
|
case SettingsData.Position.Right: edges.push("right"); break;
|
||||||
edges.push("bottom");
|
|
||||||
break;
|
|
||||||
case SettingsData.Position.Left:
|
|
||||||
edges.push("left");
|
|
||||||
break;
|
|
||||||
case SettingsData.Position.Right:
|
|
||||||
edges.push("right");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
function frameEdgeInsetForSide(screen, side) {
|
|
||||||
if (!frameEnabled || !screen)
|
|
||||||
return 0;
|
|
||||||
const edges = getActiveBarEdgesForScreen(screen);
|
|
||||||
return edges.includes(side) ? frameBarSize : frameThickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveBarThicknessForScreen(screen) {
|
function getActiveBarThicknessForScreen(screen) {
|
||||||
if (frameEnabled)
|
if (frameEnabled) return frameBarSize;
|
||||||
return frameBarSize;
|
if (!screen) return frameThickness;
|
||||||
if (!screen)
|
|
||||||
return frameThickness;
|
|
||||||
for (var i = 0; i < barConfigs.length; i++) {
|
for (var i = 0; i < barConfigs.length; i++) {
|
||||||
var bc = barConfigs[i];
|
var bc = barConfigs[i];
|
||||||
if (!bc.enabled)
|
if (!bc.enabled) continue;
|
||||||
continue;
|
|
||||||
var prefs = bc.screenPreferences || ["all"];
|
var prefs = bc.screenPreferences || ["all"];
|
||||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
|
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||||
continue;
|
|
||||||
const innerPadding = bc.innerPadding ?? 4;
|
const innerPadding = bc.innerPadding ?? 4;
|
||||||
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
||||||
const spacing = bc.spacing ?? 4;
|
const spacing = bc.spacing ?? 4;
|
||||||
@@ -3099,7 +3057,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 +3092,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-93
@@ -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";
|
||||||
@@ -342,6 +341,19 @@ Singleton {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: DMSService
|
target: DMSService
|
||||||
|
enabled: typeof DMSService !== "undefined" && typeof SessionData !== "undefined"
|
||||||
|
|
||||||
|
function onLoginctlEvent(event) {
|
||||||
|
if (!SessionData.themeModeAutoEnabled)
|
||||||
|
return;
|
||||||
|
if (event.event === "unlock" || event.event === "resume") {
|
||||||
|
if (!themeAutoBackendAvailable()) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DMSService.sendRequest("theme.auto.trigger", {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onThemeAutoStateUpdate(data) {
|
function onThemeAutoStateUpdate(data) {
|
||||||
if (!SessionData.themeModeAutoEnabled) {
|
if (!SessionData.themeModeAutoEnabled) {
|
||||||
@@ -377,7 +389,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,39 +402,18 @@ 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionService
|
|
||||||
enabled: SessionData.themeModeAutoEnabled
|
|
||||||
|
|
||||||
function onSessionUnlocked() {
|
|
||||||
root.triggerThemeAutomationRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSessionResumed() {
|
|
||||||
root.triggerThemeAutomationRefresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerThemeAutomationRefresh() {
|
|
||||||
if (!themeAutoBackendAvailable()) {
|
|
||||||
root.evaluateThemeMode();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DMSService.sendRequest("theme.auto.trigger", {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyGreeterTheme(themeName) {
|
function applyGreeterTheme(themeName) {
|
||||||
switchTheme(themeName, false, false);
|
switchTheme(themeName, false, false);
|
||||||
if (themeName === dynamic && dynamicColorsFileView.path) {
|
if (themeName === dynamic && dynamicColorsFileView.path) {
|
||||||
@@ -550,8 +541,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 +551,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 +579,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,10 +960,7 @@ Singleton {
|
|||||||
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme is the canonical access point for animation variant state. The
|
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
|
||||||
// aliases below forward to AnimVariants.qml so consumers don't need two
|
|
||||||
// imports. ~200 call sites read through Theme.variantEnterCurve /
|
|
||||||
// Theme.isConnectedEffect / etc. — do NOT migrate to AnimVariants directly.
|
|
||||||
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
|
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
|
||||||
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
|
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
|
||||||
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
|
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
|
||||||
@@ -1003,28 +974,25 @@ Singleton {
|
|||||||
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
||||||
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
|
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
|
||||||
readonly property real connectedCornerRadius: {
|
readonly property real connectedCornerRadius: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined") return 12;
|
||||||
return 12;
|
|
||||||
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
|
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
|
||||||
}
|
}
|
||||||
readonly property color connectedSurfaceColor: {
|
readonly property color connectedSurfaceColor: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
return withAlpha(surfaceContainer, popupTransparency);
|
return withAlpha(surfaceContainer, popupTransparency);
|
||||||
return isConnectedEffect ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : withAlpha(surfaceContainer, popupTransparency);
|
return isConnectedEffect
|
||||||
|
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||||
|
: withAlpha(surfaceContainer, popupTransparency);
|
||||||
}
|
}
|
||||||
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
||||||
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined") ? true : (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined")
|
||||||
|
? true
|
||||||
|
: (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
||||||
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
||||||
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
||||||
function variantDuration(baseDuration, entering) {
|
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
|
||||||
return AnimVariants.variantDuration(baseDuration, entering);
|
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
|
||||||
}
|
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
|
||||||
function variantExitCleanupPadding() {
|
|
||||||
return AnimVariants.variantExitCleanupPadding();
|
|
||||||
}
|
|
||||||
function variantCloseInterval(baseDuration) {
|
|
||||||
return AnimVariants.variantCloseInterval(baseDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var animationPresetDurations: {
|
readonly property var animationPresetDurations: {
|
||||||
"none": 0,
|
"none": 0,
|
||||||
@@ -1101,9 +1069,6 @@ Singleton {
|
|||||||
return base === 0 ? 0 : Math.round(base * 0.85);
|
return base === 0 ? 0 : Math.round(base * 0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property int notificationInlineExpandDuration: notificationAnimationBaseDuration === 0 ? 0 : 185
|
|
||||||
readonly property int notificationInlineCollapseDuration: notificationAnimationBaseDuration === 0 ? 0 : 150
|
|
||||||
|
|
||||||
readonly property real notificationIconSizeNormal: 56
|
readonly property real notificationIconSizeNormal: 56
|
||||||
readonly property real notificationIconSizeCompact: 48
|
readonly property real notificationIconSizeCompact: 48
|
||||||
readonly property real notificationExpandedIconSizeNormal: 48
|
readonly property real notificationExpandedIconSizeNormal: 48
|
||||||
@@ -1396,7 +1361,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadCustomThemeFromFile(filePath) {
|
function loadCustomThemeFromFile(filePath) {
|
||||||
customThemeFileView.path = Paths.expandTilde(filePath);
|
customThemeFileView.path = filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadCustomThemeVariant() {
|
function reloadCustomThemeVariant() {
|
||||||
@@ -1575,12 +1540,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 +1557,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 +1572,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 +1596,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 +1618,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 +1730,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 +1974,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 +1986,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,14 +2006,13 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: customThemeFileView
|
id: customThemeFileView
|
||||||
blockLoading: false
|
|
||||||
watchChanges: currentTheme === "custom"
|
watchChanges: currentTheme === "custom"
|
||||||
|
|
||||||
function parseAndLoadTheme() {
|
function parseAndLoadTheme() {
|
||||||
@@ -2097,7 +2059,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 +2079,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 +2207,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 +2300,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 +2384,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 +2398,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
function stripHtmlTags(html) {
|
|
||||||
if (!html)
|
|
||||||
return "";
|
|
||||||
return String(html)
|
|
||||||
.replace(/<[^>]+>/g, "")
|
|
||||||
.replace(/ /g, " ")
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, "\"")
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function elideRichText(html, visibleBudget) {
|
|
||||||
if (!html)
|
|
||||||
return "";
|
|
||||||
if (visibleBudget <= 0)
|
|
||||||
return "";
|
|
||||||
|
|
||||||
var out = "";
|
|
||||||
var visible = 0;
|
|
||||||
var i = 0;
|
|
||||||
var openTags = [];
|
|
||||||
var len = html.length;
|
|
||||||
|
|
||||||
while (i < len && visible < visibleBudget) {
|
|
||||||
var ch = html.charAt(i);
|
|
||||||
if (ch === "<") {
|
|
||||||
var end = html.indexOf(">", i);
|
|
||||||
if (end < 0)
|
|
||||||
break;
|
|
||||||
var tag = html.substring(i, end + 1);
|
|
||||||
out += tag;
|
|
||||||
var isClose = tag.charAt(1) === "/";
|
|
||||||
var match = tag.match(/^<\/?([a-zA-Z]+)/);
|
|
||||||
var name = match ? match[1] : "";
|
|
||||||
if (isClose) {
|
|
||||||
if (openTags.length > 0 && openTags[openTags.length - 1] === name)
|
|
||||||
openTags.pop();
|
|
||||||
} else if (!tag.endsWith("/>") && name) {
|
|
||||||
openTags.push(name);
|
|
||||||
}
|
|
||||||
i = end + 1;
|
|
||||||
} else if (ch === "&") {
|
|
||||||
var eend = html.indexOf(";", i);
|
|
||||||
if (eend < 0 || eend - i > 6) {
|
|
||||||
out += "&";
|
|
||||||
visible++;
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
out += html.substring(i, eend + 1);
|
|
||||||
visible++;
|
|
||||||
i = eend + 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out += ch;
|
|
||||||
visible++;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (i < len && html.charAt(i) === "<") {
|
|
||||||
var tend = html.indexOf(">", i);
|
|
||||||
if (tend < 0)
|
|
||||||
break;
|
|
||||||
var ttag = html.substring(i, tend + 1);
|
|
||||||
out += ttag;
|
|
||||||
var tisClose = ttag.charAt(1) === "/";
|
|
||||||
var tmatch = ttag.match(/^<\/?([a-zA-Z]+)/);
|
|
||||||
var tname = tmatch ? tmatch[1] : "";
|
|
||||||
if (tisClose) {
|
|
||||||
if (openTags.length > 0 && openTags[openTags.length - 1] === tname)
|
|
||||||
openTags.pop();
|
|
||||||
} else if (!ttag.endsWith("/>") && tname) {
|
|
||||||
openTags.push(tname);
|
|
||||||
}
|
|
||||||
i = tend + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < len) {
|
|
||||||
out = out.replace(/\s+$/, "");
|
|
||||||
while (openTags.length > 0)
|
|
||||||
out += "</" + openTags.pop() + ">";
|
|
||||||
out += "…";
|
|
||||||
} else {
|
|
||||||
while (openTags.length > 0)
|
|
||||||
out += "</" + openTags.pop() + ">";
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
var SPEC = {
|
var SPEC = {
|
||||||
isLightMode: { def: false },
|
isLightMode: { def: false },
|
||||||
doNotDisturb: { def: false },
|
doNotDisturb: { def: false },
|
||||||
doNotDisturbUntil: { def: 0 },
|
|
||||||
terminalOverride: { def: "" },
|
|
||||||
|
|
||||||
wallpaperPath: { def: "" },
|
wallpaperPath: { def: "" },
|
||||||
perMonitorWallpaper: { def: false },
|
perMonitorWallpaper: { def: false },
|
||||||
|
|||||||
@@ -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,6 @@ var SPEC = {
|
|||||||
selectedGpuIndex: { def: 0 },
|
selectedGpuIndex: { def: 0 },
|
||||||
enabledGpuPciIds: { def: [] },
|
enabledGpuPciIds: { def: [] },
|
||||||
showSystemTray: { def: true },
|
showSystemTray: { def: true },
|
||||||
systemTrayIconTintMode: { def: "none" },
|
|
||||||
systemTrayIconTintSaturation: { def: 50 },
|
|
||||||
systemTrayIconTintStrength: { def: 135 },
|
|
||||||
showClock: { def: true },
|
showClock: { def: true },
|
||||||
showNotificationButton: { def: true },
|
showNotificationButton: { def: true },
|
||||||
showBattery: { def: true },
|
showBattery: { def: true },
|
||||||
@@ -262,13 +259,11 @@ var SPEC = {
|
|||||||
acSuspendTimeout: { def: 0 },
|
acSuspendTimeout: { def: 0 },
|
||||||
acSuspendBehavior: { def: 0 },
|
acSuspendBehavior: { def: 0 },
|
||||||
acProfileName: { def: "" },
|
acProfileName: { def: "" },
|
||||||
acPostLockMonitorTimeout: { def: 0 },
|
|
||||||
batteryMonitorTimeout: { def: 0 },
|
batteryMonitorTimeout: { def: 0 },
|
||||||
batteryLockTimeout: { def: 0 },
|
batteryLockTimeout: { def: 0 },
|
||||||
batterySuspendTimeout: { def: 0 },
|
batterySuspendTimeout: { def: 0 },
|
||||||
batterySuspendBehavior: { def: 0 },
|
batterySuspendBehavior: { def: 0 },
|
||||||
batteryProfileName: { def: "" },
|
batteryProfileName: { def: "" },
|
||||||
batteryPostLockMonitorTimeout: { def: 0 },
|
|
||||||
batteryChargeLimit: { def: 100 },
|
batteryChargeLimit: { def: 100 },
|
||||||
lockBeforeSuspend: { def: false },
|
lockBeforeSuspend: { def: false },
|
||||||
loginctlLockIntegration: { def: true },
|
loginctlLockIntegration: { def: true },
|
||||||
@@ -304,7 +299,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 +347,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 },
|
||||||
@@ -375,7 +366,7 @@ var SPEC = {
|
|||||||
lockScreenShowMediaPlayer: { def: true },
|
lockScreenShowMediaPlayer: { def: true },
|
||||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
lockAtStartup: { def: false },
|
lockAtStartup: { def: false },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
@@ -431,9 +422,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: {} },
|
||||||
@@ -562,11 +550,7 @@ var SPEC = {
|
|||||||
frameScreenPreferences: { def: ["all"] },
|
frameScreenPreferences: { def: ["all"] },
|
||||||
frameBarSize: { def: 40 },
|
frameBarSize: { def: 40 },
|
||||||
frameShowOnOverview: { def: false },
|
frameShowOnOverview: { def: false },
|
||||||
frameBlurEnabled: { def: true },
|
frameBlurEnabled: { def: true }
|
||||||
frameCloseGaps: { def: true },
|
|
||||||
frameLauncherEmergeSide: { def: "bottom" },
|
|
||||||
frameLauncherArcExtender: { def: false },
|
|
||||||
frameMode: { def: "separate" }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
+76
-181
@@ -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,16 +27,6 @@ import qs.Services
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("DMSShell")
|
|
||||||
|
|
||||||
property bool osdSurfacesLoaded: true
|
|
||||||
property int pendingOsdResumeReloads: 0
|
|
||||||
|
|
||||||
function recreateOsdSurfaces() {
|
|
||||||
OSDManager.currentOSDsByScreen = ({});
|
|
||||||
osdSurfacesLoaded = false;
|
|
||||||
osdSurfaceReloadTimer.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
Instantiator {
|
Instantiator {
|
||||||
id: daemonPluginInstantiator
|
id: daemonPluginInstantiator
|
||||||
@@ -56,7 +45,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 +84,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 +124,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,32 +235,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: osdResumeRecreateTimer
|
|
||||||
interval: 400
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
root.recreateOsdSurfaces();
|
|
||||||
root.pendingOsdResumeReloads--;
|
|
||||||
|
|
||||||
if (root.pendingOsdResumeReloads <= 0) {
|
|
||||||
root.pendingOsdResumeReloads = 0;
|
|
||||||
interval = 400;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
interval = 1400;
|
|
||||||
restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: osdSurfaceReloadTimer
|
|
||||||
interval: 120
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.osdSurfacesLoaded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
// Force PolkitService singleton to initialize
|
// Force PolkitService singleton to initialize
|
||||||
@@ -289,15 +252,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 +293,7 @@ Item {
|
|||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
DankDashPopout {
|
DankDashPopout {
|
||||||
id: dankDashPopout
|
id: dankDashPopout
|
||||||
|
onPopoutClosed: PopoutService.unloadDankDash()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,43 +308,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 +699,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 +731,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,16 +752,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SessionService
|
|
||||||
|
|
||||||
function onSessionResumed() {
|
|
||||||
root.pendingOsdResumeReloads = 2;
|
|
||||||
osdResumeRecreateTimer.interval = 400;
|
|
||||||
osdResumeRecreateTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankColorPickerModal {
|
DankColorPickerModal {
|
||||||
id: colorPickerModal
|
id: colorPickerModal
|
||||||
|
|
||||||
@@ -880,19 +793,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 +811,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;
|
||||||
@@ -1027,85 +926,81 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: VolumeOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: MediaVolumeOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: MediaPlaybackOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: MicMuteOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: BrightnessOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: IdleInhibitorOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: osdSurfacesLoader
|
id: powerProfileWatcherLoader
|
||||||
active: root.osdSurfacesLoaded
|
active: SettingsData.osdPowerProfileEnabled
|
||||||
asynchronous: false
|
source: "Services/PowerProfileWatcher.qml"
|
||||||
|
}
|
||||||
|
|
||||||
sourceComponent: Component {
|
Variants {
|
||||||
Item {
|
model: SettingsData.osdPowerProfileEnabled ? SettingsData.getFilteredScreens("osd") : []
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: VolumeOSD {
|
delegate: PowerProfileOSD {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
delegate: MediaVolumeOSD {
|
delegate: CapsLockOSD {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
delegate: MediaPlaybackOSD {
|
delegate: AudioOutputOSD {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: MicMuteOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: BrightnessOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: IdleInhibitorOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.osdPowerProfileEnabled ? SettingsData.getFilteredScreens("osd") : []
|
|
||||||
|
|
||||||
delegate: PowerProfileOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: CapsLockOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: AudioOutputOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-57
@@ -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";
|
||||||
@@ -295,37 +310,6 @@ Item {
|
|||||||
return "NOTEPAD_TOGGLE_FAILED";
|
return "NOTEPAD_TOGGLE_FAILED";
|
||||||
}
|
}
|
||||||
|
|
||||||
function expand(): string {
|
|
||||||
var instance = getActiveNotepadInstance();
|
|
||||||
if (instance) {
|
|
||||||
instance.expandedWidth = true;
|
|
||||||
if (!instance.isVisible)
|
|
||||||
instance.show();
|
|
||||||
return "NOTEPAD_EXPAND_SUCCESS";
|
|
||||||
}
|
|
||||||
return "NOTEPAD_EXPAND_FAILED";
|
|
||||||
}
|
|
||||||
|
|
||||||
function collapse(): string {
|
|
||||||
var instance = getActiveNotepadInstance();
|
|
||||||
if (instance) {
|
|
||||||
instance.expandedWidth = false;
|
|
||||||
if (!instance.isVisible)
|
|
||||||
instance.show();
|
|
||||||
return "NOTEPAD_COLLAPSE_SUCCESS";
|
|
||||||
}
|
|
||||||
return "NOTEPAD_COLLAPSE_FAILED";
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExpand(): string {
|
|
||||||
var instance = getActiveNotepadInstance();
|
|
||||||
if (instance) {
|
|
||||||
instance.expandedWidth = !instance.expandedWidth;
|
|
||||||
return "NOTEPAD_TOGGLE_EXPAND_SUCCESS";
|
|
||||||
}
|
|
||||||
return "NOTEPAD_TOGGLE_EXPAND_FAILED";
|
|
||||||
}
|
|
||||||
|
|
||||||
target: "notepad"
|
target: "notepad"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,7 +830,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 +863,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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user