1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

refactor(sysupdate): improve cmd handling, formatted colors & progress indication

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
purian23
2026-05-04 13:13:44 -04:00
parent cfe6e6867e
commit 408beb202c
9 changed files with 113 additions and 23 deletions
+65 -6
View File
@@ -12,6 +12,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -98,6 +99,8 @@ func runSystemUpdateCheck() {
log.Fatal("No supported package manager found") log.Fatal("No supported package manager found")
} }
stopSpin := startSpinner("Checking for updates… ")
type backendResult struct { type backendResult struct {
ID string `json:"id"` ID string `json:"id"`
Display string `json:"displayName"` Display string `json:"displayName"`
@@ -115,6 +118,7 @@ func runSystemUpdateCheck() {
results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: pkgs}) results = append(results, backendResult{ID: b.ID(), Display: b.DisplayName(), Packages: pkgs})
allPkgs = append(allPkgs, pkgs...) allPkgs = append(allPkgs, pkgs...)
} }
stopSpin()
if sysUpdateJSON { if sysUpdateJSON {
out, _ := json.MarshalIndent(map[string]any{ out, _ := json.MarshalIndent(map[string]any{
@@ -137,7 +141,7 @@ func runSystemUpdateCheck() {
} }
fmt.Println() fmt.Println()
for _, p := range allPkgs { for _, p := range allPkgs {
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?")) printPackage(p)
} }
} }
@@ -150,7 +154,9 @@ func runSystemUpdateApply() {
log.Fatal("No supported package manager found") log.Fatal("No supported package manager found")
} }
stopSpin := startSpinner("Checking for updates…")
pkgs, firstErr := collectUpdates(checkCtx, backends) pkgs, firstErr := collectUpdates(checkCtx, backends)
stopSpin()
if firstErr != nil { if firstErr != nil {
fmt.Printf("Warning: %v\n\n", firstErr) fmt.Printf("Warning: %v\n\n", firstErr)
} }
@@ -163,12 +169,12 @@ func runSystemUpdateApply() {
} }
fmt.Println() fmt.Println()
for _, p := range pkgs { for _, p := range pkgs {
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?")) printPackage(p)
} }
fmt.Println() fmt.Println()
if !sysUpdateNoConfirm && !sysUpdateDry { if !sysUpdateNoConfirm && !sysUpdateDry {
if !promptYesNo("Proceed with upgrade? [y/N]: ") { if !promptYesNo("Proceed with upgrade? [Y/n]: ") {
fmt.Println("Aborted.") fmt.Println("Aborted.")
return return
} }
@@ -178,9 +184,11 @@ func runSystemUpdateApply() {
defer cancel() defer cancel()
opts := sysupdate.UpgradeOptions{ opts := sysupdate.UpgradeOptions{
Targets: pkgs,
IncludeFlatpak: !sysUpdateNoFlatpak, IncludeFlatpak: !sysUpdateNoFlatpak,
IncludeAUR: !sysUpdateNoAUR, IncludeAUR: !sysUpdateNoAUR,
DryRun: sysUpdateDry, DryRun: sysUpdateDry,
UseSudo: true,
} }
onLine := func(line string) { fmt.Println(line) } onLine := func(line string) { fmt.Println(line) }
@@ -236,10 +244,10 @@ func promptYesNo(prompt string) bool {
return false return false
} }
switch strings.ToLower(strings.TrimSpace(line)) { switch strings.ToLower(strings.TrimSpace(line)) {
case "y", "yes": case "n", "no":
return true
default:
return false return false
default:
return true
} }
} }
@@ -262,6 +270,57 @@ func stdinIsTTY() bool {
return (fi.Mode() & os.ModeCharDevice) != 0 return (fi.Mode() & os.ModeCharDevice) != 0
} }
func stdoutIsTTY() bool {
fi, err := os.Stdout.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) != 0
}
// startSpinner prints an animated spinner to stdout for progress indication
func startSpinner(msg string) func() {
if !stdoutIsTTY() {
return func() {}
}
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
done := make(chan struct{})
go func() {
for i := 0; ; i++ {
select {
case <-done:
fmt.Print("\r\033[K")
return
case <-time.After(80 * time.Millisecond):
fmt.Printf("\r%s %s", frames[i%len(frames)], msg)
}
}
}()
return func() { close(done) }
}
var (
styleRepo = lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Bold(false)
styleName = lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
styleFrom = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
styleArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
styleTo = lipgloss.NewStyle().Foreground(lipgloss.Color("76")).Bold(true)
)
func printPackage(p sysupdate.Package) {
if !stdoutIsTTY() {
fmt.Printf(" [%s] %s %s -> %s\n", p.Repo, p.Name, defaultIfEmpty(p.FromVersion, "?"), defaultIfEmpty(p.ToVersion, "?"))
return
}
fmt.Printf(" %s %s %s %s %s\n",
styleRepo.Render("["+string(p.Repo)+"]"),
styleName.Render(p.Name),
styleFrom.Render(defaultIfEmpty(p.FromVersion, "?")),
styleArrow.Render("->"),
styleTo.Render(defaultIfEmpty(p.ToVersion, "?")),
)
}
func errOrEmpty(err error) string { func errOrEmpty(err error) string {
if err == nil { if err == nil {
return "" return ""
+1 -1
View File
@@ -15,7 +15,7 @@ func isReadOnlyCommand(args []string) bool {
continue continue
} }
switch arg { switch arg {
case "completion", "help", "__complete": case "completion", "help", "__complete", "system":
return true return true
} }
return false return false
@@ -49,7 +49,8 @@ func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(
if len(names) == 0 { if len(names) == 0 {
return nil return nil
} }
argv := append([]string{"pkexec", "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "install", "-y", "--only-upgrade"}, names...) privesc := privescBin(opts.UseSudo)
argv := append([]string{privesc, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "install", "-y", "--only-upgrade"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine}) return Run(ctx, argv, RunOptions{OnLine: onLine})
} }
@@ -43,18 +43,19 @@ func (b dnfBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
if opts.DryRun { if opts.DryRun {
return Run(ctx, []string{b.bin, "upgrade", "--refresh", "--assumeno"}, RunOptions{OnLine: onLine}) return Run(ctx, []string{b.bin, "upgrade", "--assumeno"}, RunOptions{OnLine: onLine})
} }
names := pickTargetNames(opts.Targets, b.bin, true) names := pickTargetNames(opts.Targets, b.bin, true)
if len(names) == 0 { if len(names) == 0 {
return nil return nil
} }
argv := append([]string{"pkexec", b.bin, "upgrade", "--refresh", "-y"}, names...) privesc := privescBin(opts.UseSudo)
argv := append([]string{privesc, b.bin, "upgrade", "-y"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine}) return Run(ctx, argv, RunOptions{OnLine: onLine})
} }
func dnfListUpgrades(ctx context.Context, bin string) (string, error) { func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
cmd := exec.CommandContext(ctx, bin, "list", "--upgrades", "--quiet") cmd := exec.CommandContext(ctx, bin, "list", "--upgrades", "--refresh", "--quiet")
out, err := cmd.Output() out, err := cmd.Output()
if err == nil { if err == nil {
return string(out), nil return string(out), nil
@@ -47,7 +47,8 @@ func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine
if len(names) == 0 { if len(names) == 0 {
return nil return nil
} }
argv := append([]string{"pkexec", "pacman", "-Sy", "--noconfirm", "--needed"}, names...) privesc := privescBin(opts.UseSudo)
argv := append([]string{privesc, "pacman", "-Sy", "--noconfirm", "--needed"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine}) return Run(ctx, argv, RunOptions{OnLine: onLine})
} }
@@ -78,6 +78,7 @@ func (zypperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fu
if len(names) == 0 { if len(names) == 0 {
return nil return nil
} }
argv := append([]string{"pkexec", "zypper", "--non-interactive", "update"}, names...) privesc := privescBin(opts.UseSudo)
argv := append([]string{privesc, "zypper", "--non-interactive", "update"}, names...)
return Run(ctx, argv, RunOptions{OnLine: onLine}) return Run(ctx, argv, RunOptions{OnLine: onLine})
} }
@@ -9,6 +9,8 @@ import (
"os/exec" "os/exec"
"sync" "sync"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
type RunOptions struct { type RunOptions struct {
@@ -77,6 +79,18 @@ func Capture(ctx context.Context, argv []string) (string, error) {
return string(out), err return string(out), err
} }
// privescBin returns the binary to use for privilege escalation.
// When useSudo is true it auto-detects the best available tool (sudo/doas/run0).
// When false it falls back to pkexec for GUI callers.
func privescBin(useSudo bool) string {
if useSudo {
if t, err := privesc.Detect(); err == nil {
return t.Name()
}
}
return "pkexec"
}
func findTerminal(override string) string { func findTerminal(override string) string {
if override != "" && commandExists(override) { if override != "" && commandExists(override) {
return override return override
+1
View File
@@ -76,6 +76,7 @@ type UpgradeOptions struct {
IncludeFlatpak bool IncludeFlatpak bool
IncludeAUR bool IncludeAUR bool
DryRun bool DryRun bool
UseSudo bool
CustomCommand string CustomCommand string
Terminal string Terminal string
Targets []Package Targets []Package
@@ -365,19 +365,31 @@ DankPopout {
elide: Text.ElideRight elide: Text.ElideRight
} }
StyledText { Row {
width: parent.width width: parent.width
text: { spacing: 4
const from = modelData.fromVersion || "";
const to = modelData.toVersion || ""; StyledText {
if (from && to) { text: {
return `${from} ${to}`; const from = modelData.fromVersion || "";
const to = modelData.toVersion || "";
if (from && to)
return `${from} `;
return "";
} }
return to || from || ""; font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text !== ""
}
StyledText {
text: modelData.toVersion || modelData.fromVersion || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
elide: Text.ElideRight
width: parent.width - (parent.children[0].visible ? parent.children[0].implicitWidth + 4 : 0)
} }
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
} }
} }
} }