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:
@@ -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 ""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user