diff --git a/CHANGELOG.MD b/CHANGELOG.MD index db07d02d..4494ec20 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -7,8 +7,6 @@ This file is more of a quick reference so I know what to account for before next - Terminal mux - Locale overrides - new neovim theming -- system upder overhaul -- New Log system, remember to update all plugins after release as it breaks compat # 1.4.0 diff --git a/core/cmd/dms/commands_system.go b/core/cmd/dms/commands_system.go index 772a4747..a7e48a34 100644 --- a/core/cmd/dms/commands_system.go +++ b/core/cmd/dms/commands_system.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "strings" "time" @@ -184,20 +183,10 @@ func runSystemUpdateApply() { DryRun: sysUpdateDry, } + onLine := func(line string) { fmt.Println(line) } for _, b := range backends { - cmd, err := b.UpgradeCommand(opts) - if err != nil { - log.Fatalf("%s: %v", b.ID(), err) - } - if cmd == "" { - continue - } - fmt.Printf("\n== %s ==\n$ %s\n\n", b.DisplayName(), cmd) - shell := exec.CommandContext(ctx, "sh", "-c", cmd) - shell.Stdin = os.Stdin - shell.Stdout = os.Stdout - shell.Stderr = os.Stderr - if err := shell.Run(); err != nil { + 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) } } diff --git a/core/internal/server/sysupdate/backend.go b/core/internal/server/sysupdate/backend.go index 155a8389..9bc71ce3 100644 --- a/core/internal/server/sysupdate/backend.go +++ b/core/internal/server/sysupdate/backend.go @@ -12,8 +12,9 @@ type Backend interface { Repo() RepoKind IsAvailable(ctx context.Context) bool NeedsAuth() bool + RunsInTerminal() bool CheckUpdates(ctx context.Context) ([]Package, error) - UpgradeCommand(opts UpgradeOptions) (string, error) + Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error } type Selection struct { @@ -36,10 +37,11 @@ func (s Selection) Info() []BackendInfo { 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(), + ID: b.ID(), + DisplayName: b.DisplayName(), + Repo: b.Repo(), + NeedsAuth: b.NeedsAuth(), + RunsInTerminal: b.RunsInTerminal(), }) } return out diff --git a/core/internal/server/sysupdate/backend_apt.go b/core/internal/server/sysupdate/backend_apt.go index eaa4ccee..53071eb9 100644 --- a/core/internal/server/sysupdate/backend_apt.go +++ b/core/internal/server/sysupdate/backend_apt.go @@ -15,10 +15,11 @@ var aptUpgradableLine = regexp.MustCompile(`^([^/]+)/\S+\s+(\S+)\s+\S+\s+\[upgra 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) 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") } @@ -33,16 +34,19 @@ func (aptBackend) CheckUpdates(ctx context.Context) ([]Package, error) { return parseAptUpgradable(string(out)), nil } -func (aptBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { bin := "apt-get" if !commandExists(bin) { bin = "apt" } - env := "DEBIAN_FRONTEND=noninteractive LC_ALL=C " if opts.DryRun { - return env + bin + " upgrade --dry-run", nil + return Run(ctx, []string{bin, "upgrade", "--dry-run"}, RunOptions{ + Env: []string{"DEBIAN_FRONTEND=noninteractive", "LC_ALL=C"}, + OnLine: onLine, + }) } - return "sudo " + env + bin + " upgrade -y", nil + argv := []string{"pkexec", "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "upgrade", "-y"} + return Run(ctx, argv, RunOptions{OnLine: onLine}) } func parseAptUpgradable(text string) []Package { diff --git a/core/internal/server/sysupdate/backend_dnf.go b/core/internal/server/sysupdate/backend_dnf.go index edce700a..9ae73bc6 100644 --- a/core/internal/server/sysupdate/backend_dnf.go +++ b/core/internal/server/sysupdate/backend_dnf.go @@ -3,7 +3,6 @@ package sysupdate import ( "context" "errors" - "fmt" "os/exec" "strings" ) @@ -17,10 +16,11 @@ 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) 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) { @@ -41,11 +41,11 @@ func (b dnfBackend) CheckUpdates(ctx context.Context) ([]Package, error) { return parseDnfList(out, b.bin, installed), nil } -func (b dnfBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { if opts.DryRun { - return fmt.Sprintf("%s upgrade --assumeno", b.bin), nil + return Run(ctx, []string{b.bin, "upgrade", "--assumeno"}, RunOptions{OnLine: onLine}) } - return fmt.Sprintf("sudo %s upgrade -y", b.bin), nil + return Run(ctx, []string{"pkexec", b.bin, "upgrade", "-y"}, RunOptions{OnLine: onLine}) } func dnfListUpgrades(ctx context.Context, bin string) (string, error) { diff --git a/core/internal/server/sysupdate/backend_flatpak.go b/core/internal/server/sysupdate/backend_flatpak.go index d33be407..fed5ba79 100644 --- a/core/internal/server/sysupdate/backend_flatpak.go +++ b/core/internal/server/sysupdate/backend_flatpak.go @@ -16,6 +16,7 @@ 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) { @@ -68,11 +69,12 @@ type flatpakInstalledEntry struct { commit string } -func (flatpakBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { + argv := []string{"flatpak", "update", "-y", "--noninteractive"} if opts.DryRun { - return "flatpak update --no-deploy -y", nil + argv = []string{"flatpak", "update", "--no-deploy", "-y"} } - return "flatpak update -y --noninteractive", nil + return Run(ctx, argv, RunOptions{OnLine: onLine}) } func parseFlatpakUpdates(text string, installed map[string]flatpakInstalledEntry) []Package { diff --git a/core/internal/server/sysupdate/backend_pacman.go b/core/internal/server/sysupdate/backend_pacman.go index 6e0a70dc..53d0987a 100644 --- a/core/internal/server/sysupdate/backend_pacman.go +++ b/core/internal/server/sysupdate/backend_pacman.go @@ -28,6 +28,7 @@ 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) { @@ -38,11 +39,11 @@ func (b pacmanBackend) CheckUpdates(ctx context.Context) ([]Package, error) { return parseArchUpdates(out, b.ID(), RepoSystem), nil } -func (pacmanBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { if opts.DryRun { - return "pacman -Sup", nil + return Run(ctx, []string{"pacman", "-Sup"}, RunOptions{OnLine: onLine}) } - return "sudo pacman -Syu --noconfirm", nil + return Run(ctx, []string{"pkexec", "pacman", "-Syu", "--noconfirm"}, RunOptions{OnLine: onLine}) } type archHelperBackend struct { @@ -52,6 +53,7 @@ type archHelperBackend struct { 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 true } func (b archHelperBackend) IsAvailable(_ context.Context) bool { return commandExists(b.id) } func (b archHelperBackend) DisplayName() string { @@ -80,15 +82,20 @@ func (b archHelperBackend) CheckUpdates(ctx context.Context) ([]Package, error) return pkgs, nil } -func (b archHelperBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (b archHelperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { if opts.DryRun { - return fmt.Sprintf("%s -Sup", b.id), nil + return Run(ctx, []string{b.id, "-Sup"}, RunOptions{OnLine: onLine}) } - cmd := fmt.Sprintf("%s -Syu --noconfirm", b.id) + 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 -Syu", b.id) if !opts.IncludeAUR { cmd += " --repo" } - return cmd, nil + title := fmt.Sprintf("DMS — System Update (%s)", b.id) + return Run(ctx, wrapInTerminal(term, title, cmd), RunOptions{OnLine: onLine}) } func pacmanRepoUpdates(ctx context.Context) (string, error) { diff --git a/core/internal/server/sysupdate/backend_rpmostree.go b/core/internal/server/sysupdate/backend_rpmostree.go index 5cb43189..077b22c4 100644 --- a/core/internal/server/sysupdate/backend_rpmostree.go +++ b/core/internal/server/sysupdate/backend_rpmostree.go @@ -15,10 +15,11 @@ func init() { 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) 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") { @@ -115,9 +116,10 @@ func bootedDeployment(deps []ostreeDeployment) *ostreeDeployment { return nil } -func (rpmOstreeBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (rpmOstreeBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { + argv := []string{"rpm-ostree", "upgrade"} if opts.DryRun { - return "rpm-ostree upgrade --check", nil + argv = append(argv, "--check") } - return "rpm-ostree upgrade", nil + return Run(ctx, argv, RunOptions{OnLine: onLine}) } diff --git a/core/internal/server/sysupdate/backend_zypper.go b/core/internal/server/sysupdate/backend_zypper.go index b0627ee1..f0aa0d5b 100644 --- a/core/internal/server/sysupdate/backend_zypper.go +++ b/core/internal/server/sysupdate/backend_zypper.go @@ -17,6 +17,7 @@ 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 { @@ -69,9 +70,9 @@ func parseZypperXML(out []byte) ([]Package, error) { return pkgs, nil } -func (zypperBackend) UpgradeCommand(opts UpgradeOptions) (string, error) { +func (zypperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error { if opts.DryRun { - return "zypper --non-interactive --dry-run update", nil + return Run(ctx, []string{"zypper", "--non-interactive", "--dry-run", "update"}, RunOptions{OnLine: onLine}) } - return "sudo zypper --non-interactive update", nil + return Run(ctx, []string{"pkexec", "zypper", "--non-interactive", "update"}, RunOptions{OnLine: onLine}) } diff --git a/core/internal/server/sysupdate/executor.go b/core/internal/server/sysupdate/executor.go index 1217d20d..9a086759 100644 --- a/core/internal/server/sysupdate/executor.go +++ b/core/internal/server/sysupdate/executor.go @@ -1,18 +1,63 @@ package sysupdate import ( + "bufio" "context" "fmt" + "io" "os" "os/exec" + "sync" ) -func Run(ctx context.Context, argv []string) error { +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:]...) - return cmd.Run() + if len(opts.Env) > 0 { + cmd.Env = append(cmd.Environ(), opts.Env...) + } + + 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) { diff --git a/core/internal/server/sysupdate/manager.go b/core/internal/server/sysupdate/manager.go index fff4d3fc..0f5403ce 100644 --- a/core/internal/server/sysupdate/manager.go +++ b/core/internal/server/sysupdate/manager.go @@ -18,6 +18,7 @@ import ( const ( defaultIntervalSeconds = 30 * 60 minIntervalSeconds = 5 * 60 + recentLogCapacity = 200 checkTimeout = 5 * time.Minute upgradeTimeout = 30 * time.Minute ) @@ -239,6 +240,7 @@ func (m *Manager) runRefresh(parent context.Context) { } m.state.Phase = PhaseRefreshing m.state.Error = nil + m.state.RecentLog = nil m.mu.Unlock() m.markDirty() @@ -296,13 +298,57 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) { m.opMu.Unlock() }() - combined, err := buildBundledCommand(m.selection, opts) - if err != nil { - m.setError(ErrCodeNoBackend, err.Error()) + if opts.CustomCommand != "" { + m.runCustomUpgrade(ctx, opts.CustomCommand, opts.Terminal) return } - term := findTerminal(opts.Terminal) + backends := upgradeBackends(m.selection, opts) + if len(backends) == 0 { + m.setError(ErrCodeNoBackend, "no backend selected for upgrade") + 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) } + 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 @@ -313,12 +359,14 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) { 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() - argv := wrapInTerminal(term, "DMS — System Update", combined) - if err := Run(ctx, argv); err != nil { + 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): @@ -343,31 +391,6 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) { go m.runRefresh(context.Background()) } -func buildBundledCommand(sel Selection, opts UpgradeOptions) (string, error) { - if opts.CustomCommand != "" { - return opts.CustomCommand, nil - } - backends := upgradeBackends(sel, opts) - if len(backends) == 0 { - return "", errors.New("no backend selected for upgrade") - } - parts := make([]string, 0, len(backends)) - for _, b := range backends { - cmd, err := b.UpgradeCommand(opts) - if err != nil { - return "", fmt.Errorf("%s: %w", b.ID(), err) - } - if cmd == "" { - continue - } - parts = append(parts, cmd) - } - if len(parts) == 0 { - return "", errors.New("no backend produced an upgrade command") - } - return strings.Join(parts, " && "), nil -} - func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend { var out []Backend if sel.System != nil { @@ -383,6 +406,20 @@ func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend { 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 @@ -421,6 +458,7 @@ 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 diff --git a/core/internal/server/sysupdate/types.go b/core/internal/server/sysupdate/types.go index 3cdd5ba7..06dbe174 100644 --- a/core/internal/server/sysupdate/types.go +++ b/core/internal/server/sysupdate/types.go @@ -41,10 +41,11 @@ type Package struct { } type BackendInfo struct { - ID string `json:"id"` - DisplayName string `json:"displayName"` - Repo RepoKind `json:"repo"` - NeedsAuth bool `json:"needsAuth"` + ID string `json:"id"` + DisplayName string `json:"displayName"` + Repo RepoKind `json:"repo"` + NeedsAuth bool `json:"needsAuth"` + RunsInTerminal bool `json:"runsInTerminal"` } type ErrorInfo struct { @@ -66,6 +67,7 @@ type State struct { 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"` } diff --git a/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml b/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml index 041d9c17..7acd3877 100644 --- a/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml +++ b/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml @@ -57,6 +57,8 @@ DankPopout { color: "transparent" focus: true + readonly property bool hasTerminalBackend: (SystemUpdateService.backends || []).some(b => b.runsInTerminal === true) + Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { systemUpdatePopout.close(); @@ -206,9 +208,13 @@ DankPopout { includeAUR: SettingsData.updaterAllowAUR, terminal: SessionData.terminalOverride }; - systemUpdatePopout._reopenAfterUpgrade = true; + if (updaterPanel.hasTerminalBackend) { + systemUpdatePopout._reopenAfterUpgrade = true; + SystemUpdateService.runUpdates(opts); + systemUpdatePopout.close(); + return; + } SystemUpdateService.runUpdates(opts); - systemUpdatePopout.close(); } } @@ -377,7 +383,7 @@ DankPopout { anchors.fill: parent anchors.margins: Theme.spacingM spacing: Theme.spacingS - visible: SystemUpdateService.isUpgrading + visible: SystemUpdateService.isUpgrading && updaterPanel.hasTerminalBackend DankIcon { anchors.horizontalCenter: parent.horizontalCenter @@ -397,13 +403,38 @@ DankPopout { StyledText { width: parent.width - text: I18n.tr("See the terminal window for prompts. This popout will return when the upgrade exits.") + text: I18n.tr("AUR helpers are interactive — see the terminal window for prompts. This popout will return to idle when the upgrade exits.") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceVariantText wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter } } + + DankFlickable { + anchors.fill: parent + anchors.margins: Theme.spacingM + visible: SystemUpdateService.isUpgrading && !updaterPanel.hasTerminalBackend + contentWidth: width + contentHeight: logText.implicitHeight + clip: true + + onContentHeightChanged: { + if (contentHeight > height) { + contentY = contentHeight - height; + } + } + + StyledText { + id: logText + width: parent.width + text: (SystemUpdateService.recentLog || []).join("\n") + font.family: Theme.monoFontFamily || "monospace" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + wrapMode: Text.NoWrap + } + } } } } diff --git a/quickshell/Modules/Dock/DockTrashMenuItem.qml b/quickshell/Modules/Dock/DockTrashMenuItem.qml index 288e38b1..6574a972 100644 --- a/quickshell/Modules/Dock/DockTrashMenuItem.qml +++ b/quickshell/Modules/Dock/DockTrashMenuItem.qml @@ -9,6 +9,7 @@ Rectangle { property string iconName: "" property string text: "" property bool isDestructive: false + property bool enabled: true signal triggered diff --git a/quickshell/Services/SystemUpdateService.qml b/quickshell/Services/SystemUpdateService.qml index 52120cc9..85710ce8 100644 --- a/quickshell/Services/SystemUpdateService.qml +++ b/quickshell/Services/SystemUpdateService.qml @@ -24,6 +24,7 @@ Singleton { property string distributionPretty: "" property string pkgManager: "" property bool distributionSupported: false + property var recentLog: [] property int intervalSeconds: 1800 property int lastCheckUnix: 0 property int nextCheckUnix: 0 @@ -88,6 +89,7 @@ Singleton { distribution = data.distro || ""; distributionPretty = data.distroPretty || ""; distributionSupported = (backends.length > 0); + recentLog = data.recentLog || []; intervalSeconds = data.intervalSeconds || 1800; lastCheckUnix = data.lastCheckUnix || 0; nextCheckUnix = data.nextCheckUnix || 0;