mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
feat(dms updater): add support for ignoring specific packages during system updates
- Added UI component & popout settings to manage ignored packages - Added CLI support available in danklinux docs Fixes: #2827 Closes: #2344, #1741 Port 1.5
This commit is contained in:
@@ -47,6 +47,7 @@ var (
|
||||
sysUpdateJSON bool
|
||||
sysUpdateNoFlatpak bool
|
||||
sysUpdateNoAUR bool
|
||||
sysUpdateIgnore []string
|
||||
sysUpdateIntervalS int
|
||||
sysUpdateListPmTime = 5 * time.Minute
|
||||
)
|
||||
@@ -58,6 +59,7 @@ func init() {
|
||||
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().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
|
||||
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
||||
|
||||
systemCmd.AddCommand(systemUpdateCmd)
|
||||
@@ -192,6 +194,7 @@ func runSystemUpdateApply() {
|
||||
Targets: pkgs,
|
||||
IncludeFlatpak: !sysUpdateNoFlatpak,
|
||||
IncludeAUR: !sysUpdateNoAUR,
|
||||
Ignored: sysUpdateIgnore,
|
||||
DryRun: sysUpdateDry,
|
||||
UseSudo: true,
|
||||
}
|
||||
@@ -234,12 +237,19 @@ func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupd
|
||||
}
|
||||
|
||||
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
||||
if !sysUpdateNoAUR {
|
||||
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
|
||||
return pkgs
|
||||
}
|
||||
ignored := make(map[string]bool, len(sysUpdateIgnore))
|
||||
for _, name := range sysUpdateIgnore {
|
||||
ignored[name] = true
|
||||
}
|
||||
out := pkgs[:0]
|
||||
for _, p := range pkgs {
|
||||
if p.Repo == sysupdate.RepoAUR {
|
||||
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
|
||||
continue
|
||||
}
|
||||
if ignored[p.Name] {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
|
||||
@@ -2,6 +2,7 @@ package sysupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -31,7 +32,36 @@ func (aptBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseAptUpgradable(string(out)), nil
|
||||
return filterAptHeld(parseAptUpgradable(string(out)), aptHeldPackages(ctx)), nil
|
||||
}
|
||||
|
||||
// aptHeldPackages returns held packages, which apt-get upgrade never applies.
|
||||
func aptHeldPackages(ctx context.Context) map[string]bool {
|
||||
out, err := exec.CommandContext(ctx, "apt-mark", "showhold").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
held := make(map[string]bool)
|
||||
for line := range strings.SplitSeq(string(out), "\n") {
|
||||
if name := strings.TrimSpace(line); name != "" {
|
||||
held[name] = true
|
||||
}
|
||||
}
|
||||
return held
|
||||
}
|
||||
|
||||
func filterAptHeld(pkgs []Package, held map[string]bool) []Package {
|
||||
if len(held) == 0 {
|
||||
return pkgs
|
||||
}
|
||||
out := pkgs[:0]
|
||||
for _, p := range pkgs {
|
||||
if held[p.Name] {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||
@@ -52,7 +82,22 @@ func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(
|
||||
}
|
||||
|
||||
func aptUpgradeArgv(bin string, opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "upgrade", "-y")
|
||||
ignored := shellSafeNames(opts.Ignored)
|
||||
if len(ignored) == 0 {
|
||||
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "upgrade", "-y")
|
||||
}
|
||||
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "sh", "-c", aptHoldScript(bin, ignored))
|
||||
}
|
||||
|
||||
// aptHoldScript holds ignored packages only for the upgrade, leaving pre-existing user holds untouched.
|
||||
func aptHoldScript(bin string, ignored []string) string {
|
||||
names := strings.Join(ignored, " ")
|
||||
return fmt.Sprintf(
|
||||
`new=""; for p in %s; do apt-mark showhold | grep -qx "$p" || new="$new $p"; done; `+
|
||||
`[ -n "$new" ] && apt-mark hold $new; `+
|
||||
`%s upgrade -y; rc=$?; `+
|
||||
`[ -n "$new" ] && apt-mark unhold $new; exit $rc`,
|
||||
names, bin)
|
||||
}
|
||||
|
||||
func parseAptUpgradable(text string) []Package {
|
||||
|
||||
@@ -70,3 +70,22 @@ libsdl2-2.0-0/stable 2.30.0+dfsg-1 amd64 [upgradable from: 2.28.5+dfsg-1]`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAptHeld(t *testing.T) {
|
||||
pkgs := []Package{
|
||||
{Name: "bash", Repo: RepoSystem, Backend: "apt"},
|
||||
{Name: "linux-image-generic", Repo: RepoSystem, Backend: "apt"},
|
||||
{Name: "zsh", Repo: RepoSystem, Backend: "apt"},
|
||||
}
|
||||
|
||||
got := filterAptHeld(append([]Package(nil), pkgs...), map[string]bool{"linux-image-generic": true})
|
||||
want := []Package{pkgs[0], pkgs[2]}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("filterAptHeld() = %#v\nwant %#v", got, want)
|
||||
}
|
||||
|
||||
unfiltered := filterAptHeld(append([]Package(nil), pkgs...), nil)
|
||||
if !reflect.DeepEqual(unfiltered, pkgs) {
|
||||
t.Errorf("filterAptHeld(nil held) = %#v\nwant %#v", unfiltered, pkgs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sysupdate
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
@@ -52,7 +53,11 @@ func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fun
|
||||
}
|
||||
|
||||
func dnfUpgradeArgv(bin string, opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, bin, "upgrade", "--refresh", "-y")
|
||||
argv := []string{bin, "upgrade", "--refresh", "-y"}
|
||||
if len(opts.Ignored) > 0 {
|
||||
argv = append(argv, "--exclude="+strings.Join(opts.Ignored, ","))
|
||||
}
|
||||
return privilegedArgv(opts, argv...)
|
||||
}
|
||||
|
||||
func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
|
||||
@@ -65,9 +70,22 @@ func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
|
||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 100 {
|
||||
return string(out), nil
|
||||
}
|
||||
if detail := lastNonEmptyLine(string(out)); detail != "" {
|
||||
return "", fmt.Errorf("%w: %s", err, detail)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func lastNonEmptyLine(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
if line := strings.TrimSpace(lines[i]); line != "" {
|
||||
return line
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func dnfCheckUpdatesArgv(bin string) []string {
|
||||
subcommand := "check-update"
|
||||
if bin == "dnf5" {
|
||||
|
||||
@@ -95,11 +95,21 @@ func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine f
|
||||
if !BackendHasTargets(flatpakBackend{}, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||
return nil
|
||||
}
|
||||
return Run(ctx, flatpakUpgradeArgv(), RunOptions{OnLine: onLine})
|
||||
return Run(ctx, flatpakUpgradeArgv(opts), RunOptions{OnLine: onLine})
|
||||
}
|
||||
|
||||
func flatpakUpgradeArgv() []string {
|
||||
return []string{"flatpak", "update", "-y", "--noninteractive"}
|
||||
func flatpakUpgradeArgv(opts UpgradeOptions) []string {
|
||||
argv := []string{"flatpak", "update", "-y", "--noninteractive"}
|
||||
if len(opts.Ignored) == 0 {
|
||||
return argv
|
||||
}
|
||||
// No exclude flag; update the already-filtered refs explicitly.
|
||||
for _, p := range opts.Targets {
|
||||
if p.Repo == RepoFlatpak && p.Ref != "" {
|
||||
argv = append(argv, p.Ref)
|
||||
}
|
||||
}
|
||||
return argv
|
||||
}
|
||||
|
||||
func parseFlatpakUpdateOutput(text string, installed map[string]flatpakInstalledEntry) []Package {
|
||||
|
||||
@@ -50,7 +50,11 @@ func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine
|
||||
}
|
||||
|
||||
func pacmanUpgradeArgv(opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, "pacman", "-Syu", "--noconfirm", "--needed")
|
||||
argv := []string{"pacman", "-Syu", "--noconfirm", "--needed"}
|
||||
if len(opts.Ignored) > 0 {
|
||||
argv = append(argv, "--ignore", strings.Join(opts.Ignored, ","))
|
||||
}
|
||||
return privilegedArgv(opts, argv...)
|
||||
}
|
||||
|
||||
type archHelperBackend struct {
|
||||
@@ -99,23 +103,27 @@ func (b archHelperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onL
|
||||
return nil
|
||||
}
|
||||
if os.Getenv("DMS_FORCE_PKEXEC") == "1" {
|
||||
argv := append([]string{"pkexec"}, archHelperUpgradeArgv(b.id, opts.IncludeAUR)...)
|
||||
argv := append([]string{"pkexec"}, archHelperUpgradeArgv(b.id, opts.IncludeAUR, opts.Ignored)...)
|
||||
return Run(ctx, argv, RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||
}
|
||||
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 := strings.Join(archHelperUpgradeArgv(b.id, opts.IncludeAUR), " ")
|
||||
cmd := strings.Join(archHelperUpgradeArgv(b.id, opts.IncludeAUR, opts.Ignored), " ")
|
||||
title := fmt.Sprintf("DMS — System Update (%s)", b.id)
|
||||
return Run(ctx, wrapInTerminal(term, title, cmd), RunOptions{OnLine: onLine})
|
||||
}
|
||||
|
||||
func archHelperUpgradeArgv(id string, includeAUR bool) []string {
|
||||
func archHelperUpgradeArgv(id string, includeAUR bool, ignored []string) []string {
|
||||
argv := []string{id, "-Syu", "--noconfirm", "--needed"}
|
||||
if !includeAUR {
|
||||
argv = append(argv, "--repo")
|
||||
}
|
||||
ignored = shellSafeNames(ignored)
|
||||
if len(ignored) > 0 {
|
||||
argv = append(argv, "--ignore", strings.Join(ignored, ","))
|
||||
}
|
||||
return argv
|
||||
}
|
||||
|
||||
@@ -248,6 +256,10 @@ func parseArchUpdates(text, backendID string, repo RepoKind) []Package {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// pacman -Qu / paru -Qua flag IgnorePkg entries with a trailing marker
|
||||
if strings.HasSuffix(line, "[ignored]") {
|
||||
continue
|
||||
}
|
||||
m := archUpdateLine.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
|
||||
@@ -92,6 +92,17 @@ foo`,
|
||||
{Name: "bat", Repo: RepoSystem, Backend: "pacman", FromVersion: "0.26.0-1", ToVersion: "0.26.1-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skips IgnorePkg entries",
|
||||
input: `bat 0.26.0-1 -> 0.26.1-2
|
||||
linux 6.18.0-1 -> 6.18.1-1 [ignored]
|
||||
discord 0.0.108-1 -> 0.0.109-1 [ignored]`,
|
||||
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 ",
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -81,5 +83,20 @@ func (zypperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fu
|
||||
}
|
||||
|
||||
func zypperUpgradeArgv(opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, "zypper", "--non-interactive", "update")
|
||||
ignored := shellSafeNames(opts.Ignored)
|
||||
if len(ignored) == 0 {
|
||||
return privilegedArgv(opts, "zypper", "--non-interactive", "update")
|
||||
}
|
||||
return privilegedArgv(opts, "sh", "-c", zypperLockScript(ignored))
|
||||
}
|
||||
|
||||
// zypperLockScript locks ignored packages only for the update, leaving pre-existing user locks untouched.
|
||||
func zypperLockScript(ignored []string) string {
|
||||
names := strings.Join(ignored, " ")
|
||||
return fmt.Sprintf(
|
||||
`new=""; for p in %s; do grep -qsE "^solvable_name:[[:space:]]*$p$" /etc/zypp/locks || new="$new $p"; done; `+
|
||||
`[ -n "$new" ] && zypper --non-interactive al $new; `+
|
||||
`zypper --non-interactive update; rc=$?; `+
|
||||
`[ -n "$new" ] && zypper --non-interactive rl $new; exit $rc`,
|
||||
names)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func handleUpgrade(conn net.Conn, req models.Request, m *Manager) {
|
||||
DryRun: params.BoolOpt(req.Params, "dry", false),
|
||||
CustomCommand: params.StringOpt(req.Params, "customCommand", ""),
|
||||
Terminal: params.StringOpt(req.Params, "terminal", ""),
|
||||
Ignored: stringSliceOpt(req.Params, "ignored"),
|
||||
}
|
||||
if err := m.Upgrade(opts); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -53,3 +54,21 @@ func handleUpgrade(conn net.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func stringSliceOpt(p map[string]any, key string) []string {
|
||||
val, ok := params.Any(p, key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
arr, ok := val.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
minIntervalSeconds = 5 * 60
|
||||
recentLogCapacity = 200
|
||||
checkTimeout = 5 * time.Minute
|
||||
retryIntervalSeconds = 5 * 60
|
||||
upgradeTimeout = 30 * time.Minute
|
||||
postUpgradeCompleteDelay = 3 * time.Second
|
||||
)
|
||||
@@ -150,7 +151,7 @@ func (m *Manager) Refresh(opts RefreshOptions) {
|
||||
m.refreshSerial.Unlock()
|
||||
return
|
||||
}
|
||||
m.runRefresh(context.Background())
|
||||
m.runRefresh(context.Background(), true)
|
||||
}
|
||||
|
||||
func (m *Manager) Upgrade(opts UpgradeOptions) error {
|
||||
@@ -237,12 +238,12 @@ func (m *Manager) scheduler() {
|
||||
case <-m.wakeSched:
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
m.runRefresh(context.Background())
|
||||
m.runRefresh(context.Background(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) runRefresh(parent context.Context) {
|
||||
func (m *Manager) runRefresh(parent context.Context, manual bool) {
|
||||
m.refreshSerial.Lock()
|
||||
defer m.refreshSerial.Unlock()
|
||||
|
||||
@@ -284,27 +285,43 @@ func (m *Manager) runRefresh(parent context.Context) {
|
||||
now := time.Now().Unix()
|
||||
m.mu.Lock()
|
||||
m.state.LastCheckUnix = now
|
||||
m.state.Packages = m.state.Packages[:0]
|
||||
prev := m.state.Packages
|
||||
next := make([]Package, 0, len(prev))
|
||||
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)
|
||||
}
|
||||
// Retain a failed backend's last known packages so a transient failure doesn't wipe the list.
|
||||
for _, p := range prev {
|
||||
if p.Backend == backends[i].ID() {
|
||||
next = append(next, p)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
m.state.Packages = append(m.state.Packages, r.pkgs...)
|
||||
next = append(next, r.pkgs...)
|
||||
}
|
||||
m.state.Count = len(m.state.Packages)
|
||||
m.state.Packages = next
|
||||
m.state.Count = len(next)
|
||||
m.state.NextCheckUnix = now + int64(m.state.IntervalSeconds)
|
||||
if firstErr != nil {
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: ErrCodeBackendFailed, Message: firstErr.Error()}
|
||||
} else {
|
||||
switch {
|
||||
case firstErr == nil:
|
||||
m.state.Phase = PhaseIdle
|
||||
m.state.LastSuccessUnix = now
|
||||
case manual:
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: ErrCodeBackendFailed, Message: firstErr.Error()}
|
||||
default:
|
||||
// Background checks fail silently and retry sooner; only manual refreshes surface errors.
|
||||
m.state.Phase = PhaseIdle
|
||||
retry := min(int64(m.state.IntervalSeconds), retryIntervalSeconds)
|
||||
m.state.NextCheckUnix = now + retry
|
||||
log.Warnf("[sysupdate] background check failed, retrying in %ds: %v", retry, firstErr)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.wake()
|
||||
m.markDirty()
|
||||
}
|
||||
|
||||
@@ -328,10 +345,15 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) {
|
||||
opts.Targets = append([]Package(nil), m.state.Packages...)
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
opts.Targets = dropIgnoredTargets(opts.Targets, opts.Ignored)
|
||||
|
||||
backends := upgradeBackends(m.selection, opts)
|
||||
if len(backends) == 0 {
|
||||
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
|
||||
if len(opts.Targets) > 0 {
|
||||
m.setError(ErrCodeNoBackend, "all pending updates are excluded by current settings (AUR/Flatpak disabled)")
|
||||
} else {
|
||||
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -429,6 +451,24 @@ func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
||||
m.markDirty()
|
||||
}
|
||||
|
||||
func dropIgnoredTargets(targets []Package, ignored []string) []Package {
|
||||
if len(ignored) == 0 {
|
||||
return targets
|
||||
}
|
||||
skip := make(map[string]bool, len(ignored))
|
||||
for _, name := range ignored {
|
||||
skip[name] = true
|
||||
}
|
||||
out := targets[:0]
|
||||
for _, p := range targets {
|
||||
if skip[p.Name] {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend {
|
||||
var out []Backend
|
||||
if sel.System != nil {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
package sysupdate
|
||||
|
||||
import "regexp"
|
||||
|
||||
var safePkgName = regexp.MustCompile(`^[A-Za-z0-9@._+:-]+$`)
|
||||
|
||||
// shellSafeNames drops names unsafe to interpolate into the apt/zypper sh -c scripts.
|
||||
func shellSafeNames(names []string) []string {
|
||||
out := make([]string, 0, len(names))
|
||||
for _, n := range names {
|
||||
if safePkgName.MatchString(n) {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func BackendHasTargets(b Backend, targets []Package, includeAUR, includeFlatpak bool) bool {
|
||||
if b == nil || len(targets) == 0 {
|
||||
return false
|
||||
|
||||
@@ -81,6 +81,7 @@ type UpgradeOptions struct {
|
||||
CustomCommand string
|
||||
Terminal string
|
||||
Targets []Package
|
||||
Ignored []string
|
||||
}
|
||||
|
||||
type RefreshOptions struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package sysupdate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -35,19 +36,55 @@ func TestUpgradeCommandBuilders(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "aur helper full update with aur",
|
||||
got: archHelperUpgradeArgv("paru", true),
|
||||
got: archHelperUpgradeArgv("paru", true, nil),
|
||||
want: []string{"paru", "-Syu", "--noconfirm", "--needed"},
|
||||
},
|
||||
{
|
||||
name: "aur helper repo-only full update",
|
||||
got: archHelperUpgradeArgv("yay", false),
|
||||
got: archHelperUpgradeArgv("yay", false, nil),
|
||||
want: []string{"yay", "-Syu", "--noconfirm", "--needed", "--repo"},
|
||||
},
|
||||
{
|
||||
name: "aur helper with ignored packages",
|
||||
got: archHelperUpgradeArgv("paru", true, []string{"linux", "bad;name", "discord"}),
|
||||
want: []string{"paru", "-Syu", "--noconfirm", "--needed", "--ignore", "linux,discord"},
|
||||
},
|
||||
{
|
||||
name: "pacman with ignored packages",
|
||||
got: pacmanUpgradeArgv(UpgradeOptions{Ignored: []string{"linux"}}),
|
||||
want: []string{"pkexec", "pacman", "-Syu", "--noconfirm", "--needed", "--ignore", "linux"},
|
||||
},
|
||||
{
|
||||
name: "dnf with ignored packages",
|
||||
got: dnfUpgradeArgv("dnf5", UpgradeOptions{Ignored: []string{"kernel", "mesa"}}),
|
||||
want: []string{"pkexec", "dnf5", "upgrade", "--refresh", "-y", "--exclude=kernel,mesa"},
|
||||
},
|
||||
{
|
||||
name: "apt without ignored uses plain upgrade",
|
||||
got: aptUpgradeArgv("apt-get", UpgradeOptions{}),
|
||||
want: []string{"pkexec", "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "apt-get", "upgrade", "-y"},
|
||||
},
|
||||
{
|
||||
name: "zypper without ignored uses plain update",
|
||||
got: zypperUpgradeArgv(UpgradeOptions{}),
|
||||
want: []string{"pkexec", "zypper", "--non-interactive", "update"},
|
||||
},
|
||||
{
|
||||
name: "flatpak full update",
|
||||
got: flatpakUpgradeArgv(),
|
||||
got: flatpakUpgradeArgv(UpgradeOptions{}),
|
||||
want: []string{"flatpak", "update", "-y", "--noninteractive"},
|
||||
},
|
||||
{
|
||||
name: "flatpak update with ignored targets refs",
|
||||
got: flatpakUpgradeArgv(UpgradeOptions{
|
||||
Ignored: []string{"org.mozilla.firefox"},
|
||||
Targets: []Package{
|
||||
{Name: "Discord", Repo: RepoFlatpak, Ref: "com.discordapp.Discord//stable"},
|
||||
{Name: "bash", Repo: RepoSystem, Backend: "apt"},
|
||||
},
|
||||
}),
|
||||
want: []string{"flatpak", "update", "-y", "--noninteractive", "com.discordapp.Discord//stable"},
|
||||
},
|
||||
{
|
||||
name: "rpm-ostree upgrade",
|
||||
got: rpmOstreeUpgradeArgv(UpgradeOptions{}),
|
||||
@@ -69,6 +106,45 @@ func TestUpgradeCommandBuilders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAptUpgradeArgvHoldsIgnored(t *testing.T) {
|
||||
argv := aptUpgradeArgv("apt-get", UpgradeOptions{Ignored: []string{"linux-image-generic", "bad;name"}})
|
||||
if len(argv) < 2 || argv[len(argv)-2] != "-c" {
|
||||
t.Fatalf("expected sh -c script, got %#v", argv)
|
||||
}
|
||||
script := argv[len(argv)-1]
|
||||
if !strings.Contains(script, "apt-mark hold") || !strings.Contains(script, "apt-mark unhold") {
|
||||
t.Fatalf("hold script missing hold/unhold: %q", script)
|
||||
}
|
||||
if !strings.Contains(script, "linux-image-generic") {
|
||||
t.Fatalf("hold script missing ignored package: %q", script)
|
||||
}
|
||||
if strings.Contains(script, "bad;name") {
|
||||
t.Fatalf("hold script must drop unsafe name: %q", script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZypperUpgradeArgvLocksIgnored(t *testing.T) {
|
||||
argv := zypperUpgradeArgv(UpgradeOptions{Ignored: []string{"kernel-default"}})
|
||||
if len(argv) < 2 || argv[len(argv)-2] != "-c" {
|
||||
t.Fatalf("expected sh -c script, got %#v", argv)
|
||||
}
|
||||
script := argv[len(argv)-1]
|
||||
if !strings.Contains(script, "zypper --non-interactive al") || !strings.Contains(script, "zypper --non-interactive rl") {
|
||||
t.Fatalf("lock script missing add/remove lock: %q", script)
|
||||
}
|
||||
if !strings.Contains(script, "kernel-default") {
|
||||
t.Fatalf("lock script missing ignored package: %q", script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellSafeNames(t *testing.T) {
|
||||
got := shellSafeNames([]string{"linux", "gtk+", "bad name", "rm -rf /", "org.mozilla.firefox", "a;b"})
|
||||
want := []string{"linux", "gtk+", "org.mozilla.firefox"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("shellSafeNames() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendHasTargetsRespectsBackendAndOptions(t *testing.T) {
|
||||
targets := []Package{
|
||||
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf5"},
|
||||
|
||||
@@ -959,6 +959,7 @@ Singleton {
|
||||
property int updaterIntervalSeconds: 1800
|
||||
property bool updaterIncludeFlatpak: true
|
||||
property bool updaterAllowAUR: true
|
||||
property var updaterIgnoredPackages: []
|
||||
|
||||
property string displayNameMode: "system"
|
||||
property var screenPreferences: ({})
|
||||
|
||||
@@ -514,6 +514,7 @@ var SPEC = {
|
||||
updaterIntervalSeconds: { def: 1800 },
|
||||
updaterIncludeFlatpak: { def: true },
|
||||
updaterAllowAUR: { def: true },
|
||||
updaterIgnoredPackages: { def: [] },
|
||||
|
||||
displayNameMode: { def: "system" },
|
||||
screenPreferences: { def: {} },
|
||||
|
||||
@@ -76,6 +76,47 @@ DankPopout {
|
||||
|
||||
readonly property bool hasTerminalBackend: (SystemUpdateService.backends || []).some(b => b.runsInTerminal === true)
|
||||
|
||||
property int nowUnix: Math.floor(Date.now() / 1000)
|
||||
|
||||
Connections {
|
||||
target: systemUpdatePopout
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (systemUpdatePopout.shouldBeVisible) {
|
||||
updaterPanel.nowUnix = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function distroLabel() {
|
||||
const pretty = (SystemUpdateService.distributionPretty || "").trim();
|
||||
if (pretty) {
|
||||
return pretty.split(/\s+/)[0];
|
||||
}
|
||||
const id = (SystemUpdateService.distribution || "").trim();
|
||||
if (id) {
|
||||
return id.charAt(0).toUpperCase() + id.slice(1);
|
||||
}
|
||||
return I18n.tr("System");
|
||||
}
|
||||
|
||||
function lastCheckedText() {
|
||||
const last = SystemUpdateService.lastCheckUnix;
|
||||
if (!last) {
|
||||
return "";
|
||||
}
|
||||
const delta = Math.max(0, nowUnix - last);
|
||||
if (delta < 90) {
|
||||
return I18n.tr("checked just now");
|
||||
}
|
||||
if (delta < 3600) {
|
||||
return I18n.tr("checked %1m ago").arg(Math.round(delta / 60));
|
||||
}
|
||||
if (delta < 86400) {
|
||||
return I18n.tr("checked %1h ago").arg(Math.round(delta / 3600));
|
||||
}
|
||||
return I18n.tr("checked %1d ago").arg(Math.round(delta / 86400));
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
systemUpdatePopout.close();
|
||||
@@ -171,8 +212,17 @@ DankPopout {
|
||||
anchors.topMargin: Theme.spacingS
|
||||
visible: SystemUpdateService.backends.length > 0 && !SystemUpdateService.isUpgrading
|
||||
text: {
|
||||
const names = (SystemUpdateService.backends || []).map(b => b.displayName).join(", ");
|
||||
return I18n.tr("Backends: %1").arg(names);
|
||||
const kinds = [];
|
||||
for (const b of SystemUpdateService.backends || []) {
|
||||
const label = b.repo === "flatpak" ? I18n.tr("Flatpak") : I18n.tr("System");
|
||||
if (!kinds.includes(label)) {
|
||||
kinds.push(label);
|
||||
}
|
||||
}
|
||||
const distro = updaterPanel.distroLabel();
|
||||
const checked = updaterPanel.lastCheckedText();
|
||||
const base = `${distro}: ${kinds.join(", ")}`;
|
||||
return checked ? `${base} · ${checked}` : base;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -180,6 +230,21 @@ DankPopout {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: hiddenRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: backendsRow.visible ? backendsRow.bottom : header.bottom
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
visible: SystemUpdateService.hiddenUpdateCount > 0 && !SystemUpdateService.isUpgrading
|
||||
text: I18n.tr("%1 hidden (AUR disabled or ignored)").arg(SystemUpdateService.hiddenUpdateCount)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonsRow
|
||||
anchors.left: parent.left
|
||||
@@ -275,7 +340,7 @@ DankPopout {
|
||||
id: bodyArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: backendsRow.visible ? backendsRow.bottom : header.bottom
|
||||
anchors.top: hiddenRow.visible ? hiddenRow.bottom : (backendsRow.visible ? backendsRow.bottom : header.bottom)
|
||||
anchors.bottom: buttonsRow.top
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
@@ -294,7 +359,8 @@ DankPopout {
|
||||
text: {
|
||||
switch (true) {
|
||||
case SystemUpdateService.hasError:
|
||||
return I18n.tr("Failed: %1").arg(SystemUpdateService.errorMessage);
|
||||
const msg = I18n.tr("Failed: %1").arg(SystemUpdateService.errorMessage);
|
||||
return SystemUpdateService.errorHint ? `${msg}\n\n${SystemUpdateService.errorHint}` : msg;
|
||||
case !SystemUpdateService.helperAvailable:
|
||||
return I18n.tr("No supported package manager found.");
|
||||
case SystemUpdateService.isChecking:
|
||||
@@ -318,13 +384,18 @@ DankPopout {
|
||||
model: SystemUpdateService.availableUpdates
|
||||
|
||||
delegate: Rectangle {
|
||||
id: packageRow
|
||||
width: ListView.view.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: packageMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
|
||||
color: rowHoverHandler.hovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
|
||||
|
||||
required property var modelData
|
||||
|
||||
HoverHandler {
|
||||
id: rowHoverHandler
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -350,7 +421,7 @@ DankPopout {
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 64 - Theme.spacingS
|
||||
width: parent.width - 64 - Theme.spacingS * 2 - 28
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
@@ -395,13 +466,26 @@ DankPopout {
|
||||
id: packageMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.changelogUrl ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
cursorShape: packageRow.modelData.changelogUrl ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
if (modelData.changelogUrl) {
|
||||
Qt.openUrlExternally(modelData.changelogUrl);
|
||||
if (packageRow.modelData.changelogUrl) {
|
||||
Qt.openUrlExternally(packageRow.modelData.changelogUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.right: packageRow.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: packageRow.verticalCenter
|
||||
buttonSize: 24
|
||||
iconName: "visibility_off"
|
||||
iconSize: 16
|
||||
iconColor: Theme.surfaceVariantText
|
||||
visible: rowHoverHandler.hovered
|
||||
tooltipText: I18n.tr("Ignore this package")
|
||||
onClicked: SystemUpdateService.ignorePackage(packageRow.modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,20 @@ Item {
|
||||
}
|
||||
]
|
||||
|
||||
readonly property string customIntervalLabel: I18n.tr("Custom")
|
||||
property bool customIntervalSelected: false
|
||||
|
||||
Component.onCompleted: {
|
||||
customIntervalSelected = !intervalOptions.some(o => o.seconds === SettingsData.updaterIntervalSeconds);
|
||||
}
|
||||
|
||||
function intervalLabelFor(seconds) {
|
||||
for (const opt of intervalOptions) {
|
||||
if (opt.seconds === seconds) {
|
||||
return opt.label;
|
||||
}
|
||||
}
|
||||
return intervalOptions[1].label;
|
||||
return customIntervalLabel;
|
||||
}
|
||||
|
||||
function intervalSecondsFor(label) {
|
||||
@@ -84,15 +91,71 @@ Item {
|
||||
SettingsDropdownRow {
|
||||
text: I18n.tr("Check interval")
|
||||
description: I18n.tr("How often the server polls for new updates.")
|
||||
options: root.intervalOptions.map(o => o.label)
|
||||
currentValue: root.intervalLabelFor(SettingsData.updaterIntervalSeconds)
|
||||
options: root.intervalOptions.map(o => o.label).concat([root.customIntervalLabel])
|
||||
currentValue: root.customIntervalSelected ? root.customIntervalLabel : root.intervalLabelFor(SettingsData.updaterIntervalSeconds)
|
||||
onValueChanged: label => {
|
||||
if (label === root.customIntervalLabel) {
|
||||
root.customIntervalSelected = true;
|
||||
return;
|
||||
}
|
||||
root.customIntervalSelected = false;
|
||||
const secs = root.intervalSecondsFor(label);
|
||||
SettingsData.set("updaterIntervalSeconds", secs);
|
||||
SystemUpdateService.setInterval(secs);
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: customIntervalColumn.implicitHeight
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
visible: root.customIntervalSelected
|
||||
|
||||
Column {
|
||||
id: customIntervalColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Custom interval in minutes (minimum 5)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customIntervalField
|
||||
width: parent.width
|
||||
placeholderText: "30"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(SettingsData.updaterIntervalSeconds / 60).toString();
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
const minutes = parseInt(text, 10);
|
||||
if (isNaN(minutes) || minutes < 5) {
|
||||
return;
|
||||
}
|
||||
const secs = minutes * 60;
|
||||
SettingsData.set("updaterIntervalSeconds", secs);
|
||||
SystemUpdateService.setInterval(secs);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => {
|
||||
customIntervalField.forceActiveFocus();
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Check on startup")
|
||||
description: I18n.tr("When enabled, checks updates on startup. When disabled, only the interval above or a manual refresh runs a check.")
|
||||
@@ -119,6 +182,145 @@ Item {
|
||||
TerminalPickerRow {}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
id: ignoredPackagesCard
|
||||
width: parent.width
|
||||
iconName: "inventory_2"
|
||||
title: I18n.tr("Ignored Packages")
|
||||
settingKey: "systemUpdaterIgnoredPackages"
|
||||
tags: ["system", "update", "package", "ignore"]
|
||||
|
||||
function addIgnoredPackage() {
|
||||
const name = newIgnoredPackageField.text.trim();
|
||||
if (name === "") {
|
||||
return;
|
||||
}
|
||||
if (!/^[A-Za-z0-9@._+:-]+$/.test(name)) {
|
||||
ignoredPackageError.visible = true;
|
||||
return;
|
||||
}
|
||||
ignoredPackageError.visible = false;
|
||||
SystemUpdateService.ignorePackage(name);
|
||||
newIgnoredPackageField.text = "";
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (SettingsData.updaterUseCustomCommand) {
|
||||
return I18n.tr("Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.");
|
||||
}
|
||||
return (SettingsData.updaterIgnoredPackages || []).length > 0 ? I18n.tr("Ignored packages are hidden from the updater and skipped by 'Update All'.") : I18n.tr("No packages ignored. Add one here or hover an update in the popout and click the hide button.");
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
bottomPadding: Theme.spacingS
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankTextField {
|
||||
id: newIgnoredPackageField
|
||||
width: parent.width - addIgnoredBtn.width - Theme.spacingS
|
||||
height: 36
|
||||
placeholderText: I18n.tr("Package name (e.g., docker)")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
onAccepted: ignoredPackagesCard.addIgnoredPackage()
|
||||
onTextEdited: ignoredPackageError.visible = false
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: addIgnoredBtn
|
||||
buttonSize: 36
|
||||
iconName: "add"
|
||||
iconSize: 20
|
||||
backgroundColor: Theme.primary
|
||||
iconColor: Theme.onPrimary
|
||||
tooltipText: I18n.tr("Ignore package")
|
||||
onClicked: ignoredPackagesCard.addIgnoredPackage()
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: ignoredPackageError
|
||||
visible: false
|
||||
text: I18n.tr("Invalid package name — letters, digits and @._+:- only.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "visibility_off"
|
||||
title: I18n.tr("Ignored (%1)").arg((SettingsData.updaterIgnoredPackages || []).length)
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: (SettingsData.updaterIgnoredPackages || []).length > 0
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.5)
|
||||
|
||||
Repeater {
|
||||
model: SettingsData.updaterIgnoredPackages
|
||||
|
||||
delegate: Rectangle {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.5)
|
||||
|
||||
DankIcon {
|
||||
id: ignoredIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "visibility_off"
|
||||
size: 18
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: ignoredIcon.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: removeIgnoredBtn.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: parent.modelData
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: removeIgnoredBtn
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: 32
|
||||
iconName: "delete"
|
||||
iconSize: 18
|
||||
iconColor: Theme.error
|
||||
backgroundColor: "transparent"
|
||||
tooltipText: I18n.tr("Stop ignoring %1").arg(parent.modelData)
|
||||
onClicked: {
|
||||
const list = (SettingsData.updaterIgnoredPackages || []).slice();
|
||||
list.splice(parent.index, 1);
|
||||
SettingsData.set("updaterIgnoredPackages", list);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "tune"
|
||||
|
||||
@@ -15,10 +15,12 @@ Singleton {
|
||||
property bool sysupdateAvailable: false
|
||||
|
||||
property var availableUpdates: []
|
||||
property var _rawUpdates: []
|
||||
property bool isChecking: false
|
||||
property bool isUpgrading: false
|
||||
property bool hasError: false
|
||||
property string errorMessage: ""
|
||||
property string errorHint: ""
|
||||
property string errorCode: ""
|
||||
property var backends: []
|
||||
property string distribution: ""
|
||||
@@ -31,6 +33,7 @@ Singleton {
|
||||
property int nextCheckUnix: 0
|
||||
|
||||
readonly property int updateCount: availableUpdates.length
|
||||
readonly property int hiddenUpdateCount: _rawUpdates.length - availableUpdates.length
|
||||
readonly property bool helperAvailable: sysupdateAvailable && backends.length > 0
|
||||
|
||||
Connections {
|
||||
@@ -57,6 +60,12 @@ Singleton {
|
||||
function onUpdaterCheckOnStartChanged() {
|
||||
Qt.callLater(() => root._maybeStartupCheck());
|
||||
}
|
||||
function onUpdaterAllowAURChanged() {
|
||||
root._refilter();
|
||||
}
|
||||
function onUpdaterIgnoredPackagesChanged() {
|
||||
root._refilter();
|
||||
}
|
||||
function on_HasLoadedChanged() {
|
||||
Qt.callLater(() => root._maybeStartupCheck());
|
||||
}
|
||||
@@ -100,7 +109,8 @@ Singleton {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
availableUpdates = data.packages || [];
|
||||
_rawUpdates = data.packages || [];
|
||||
availableUpdates = _filterUpdates(_rawUpdates);
|
||||
backends = data.backends || [];
|
||||
distribution = data.distro || "";
|
||||
distributionPretty = data.distroPretty || "";
|
||||
@@ -129,10 +139,12 @@ Singleton {
|
||||
hasError = true;
|
||||
errorMessage = data.error.message || "";
|
||||
errorCode = data.error.code || "";
|
||||
errorHint = data.error.hint || "";
|
||||
} else {
|
||||
hasError = false;
|
||||
errorMessage = "";
|
||||
errorCode = "";
|
||||
errorHint = "";
|
||||
}
|
||||
|
||||
if (backends.length > 0) {
|
||||
@@ -143,6 +155,29 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function _filterUpdates(pkgs) {
|
||||
const ignored = SettingsData.updaterIgnoredPackages || [];
|
||||
return (pkgs || []).filter(p => {
|
||||
if (!SettingsData.updaterAllowAUR && p.repo === "aur")
|
||||
return false;
|
||||
return ignored.indexOf(p.name) === -1;
|
||||
});
|
||||
}
|
||||
|
||||
function _refilter() {
|
||||
availableUpdates = _filterUpdates(_rawUpdates);
|
||||
}
|
||||
|
||||
function ignorePackage(name) {
|
||||
if (!name)
|
||||
return;
|
||||
const list = (SettingsData.updaterIgnoredPackages || []).slice();
|
||||
if (list.indexOf(name) !== -1)
|
||||
return;
|
||||
list.push(name);
|
||||
SettingsData.set("updaterIgnoredPackages", list);
|
||||
}
|
||||
|
||||
function checkForUpdates() {
|
||||
DMSService.sysupdateRefresh(false, null);
|
||||
}
|
||||
@@ -153,6 +188,7 @@ Singleton {
|
||||
_runCustomTerminalCommand();
|
||||
return;
|
||||
}
|
||||
params.ignored = SettingsData.updaterIgnoredPackages || [];
|
||||
DMSService.sysupdateUpgrade(params, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -6804,6 +6804,23 @@
|
||||
"icon": "tune",
|
||||
"description": "Open a terminal and run a custom command instead of the in-shell upgrade flow."
|
||||
},
|
||||
{
|
||||
"section": "systemUpdaterIgnoredPackages",
|
||||
"label": "Ignored Packages",
|
||||
"tabIndex": 20,
|
||||
"category": "System Updater",
|
||||
"keywords": [
|
||||
"ignore",
|
||||
"ignored",
|
||||
"package",
|
||||
"packages",
|
||||
"system",
|
||||
"update",
|
||||
"updater",
|
||||
"upgrade"
|
||||
],
|
||||
"icon": "inventory_2"
|
||||
},
|
||||
{
|
||||
"section": "systemUpdater",
|
||||
"label": "System Updater",
|
||||
|
||||
Reference in New Issue
Block a user