diff --git a/README.md b/README.md index 2840cf8..93968b6 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ LazyUpdateManager is a small update helper for Arch Linux and Hyprland. It check - Sends notifications with `notify-send`, with `hyprctl notify` as fallback - Provides a graphical browser UI for checking and installing updates - Provides an Electron desktop app wrapper for the GUI +- Supports update search, source filters, ignored packages, system status, and auto-refresh - Includes a systemd user timer that checks every two hours and reminds weekly - Provides an interactive `update` command @@ -48,9 +49,13 @@ make build The GUI opens in your browser and lets you: - refresh the update list +- search and filter updates by source - start update installation in a terminal +- hide noisy packages from the active update count +- see system readiness, Pacman lock state, disk space, AUR helper, terminal, and kernel version - enable or disable AUR checks - change the reminder interval +- configure automatic refresh - choose the terminal used for installing updates ## Desktop App diff --git a/cmd/lazy-update-manager/main.go b/cmd/lazy-update-manager/main.go index 543229f..c47311a 100644 --- a/cmd/lazy-update-manager/main.go +++ b/cmd/lazy-update-manager/main.go @@ -58,7 +58,10 @@ func status() int { return 1 } - result, err := updater.CheckWithOptions(ctx, updater.Options{CheckAUR: cfg.CheckAUR}) + result, err := updater.CheckWithOptions(ctx, updater.Options{ + CheckAUR: cfg.CheckAUR, + IgnoredPackages: cfg.IgnoredPackages, + }) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -85,7 +88,10 @@ func check(args []string) int { return 1 } - result, err := updater.CheckWithOptions(ctx, updater.Options{CheckAUR: cfg.CheckAUR}) + result, err := updater.CheckWithOptions(ctx, updater.Options{ + CheckAUR: cfg.CheckAUR, + IgnoredPackages: cfg.IgnoredPackages, + }) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -117,7 +123,10 @@ func notifyUpdates(args []string) int { return 1 } - result, err := updater.CheckWithOptions(ctx, updater.Options{CheckAUR: cfg.CheckAUR}) + result, err := updater.CheckWithOptions(ctx, updater.Options{ + CheckAUR: cfg.CheckAUR, + IgnoredPackages: cfg.IgnoredPackages, + }) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -146,6 +155,9 @@ func notifyUpdates(args []string) int { store.LastReminder = now store.LastUpdateCount = result.Total() + store.LastCheck = now + store.LastSuccess = now + store.LastSummary = result.Summary() if err := state.Save(defaultStatePath(), store); err != nil { fmt.Fprintln(os.Stderr, err) return 1 @@ -162,7 +174,7 @@ func runGUI(args []string) int { return 2 } - if err := gui.Run(defaultConfigPath(), !*noOpen); err != nil { + if err := gui.Run(defaultConfigPath(), defaultStatePath(), !*noOpen); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } diff --git a/internal/config/config.go b/internal/config/config.go index acaef69..977ae18 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,9 +8,12 @@ import ( ) type Config struct { - CheckAUR bool `json:"check_aur"` - ReminderIntervalHours int `json:"reminder_interval_hours"` - Terminal string `json:"terminal"` + CheckAUR bool `json:"check_aur"` + ReminderIntervalHours int `json:"reminder_interval_hours"` + Terminal string `json:"terminal"` + AutoRefreshMinutes int `json:"auto_refresh_minutes"` + ShowIgnored bool `json:"show_ignored"` + IgnoredPackages []string `json:"ignored_packages"` } func Default() Config { @@ -18,6 +21,9 @@ func Default() Config { CheckAUR: true, ReminderIntervalHours: 168, Terminal: "auto", + AutoRefreshMinutes: 30, + ShowIgnored: false, + IgnoredPackages: []string{}, } } @@ -61,5 +67,11 @@ func normalize(cfg Config) Config { if cfg.Terminal == "" { cfg.Terminal = Default().Terminal } + if cfg.AutoRefreshMinutes < 0 { + cfg.AutoRefreshMinutes = Default().AutoRefreshMinutes + } + if cfg.IgnoredPackages == nil { + cfg.IgnoredPackages = []string{} + } return cfg } diff --git a/internal/gui/assets.go b/internal/gui/assets.go index cd7a48a..618a60e 100644 --- a/internal/gui/assets.go +++ b/internal/gui/assets.go @@ -16,65 +16,141 @@ const indexHTML = `

Pruefe Updates...

- +
+
+
+ Aktive Updates + 0 +
+
+ Ausgeblendet + 0 +
+
+ Freier Speicher + - +
+
+ Kernel + - +
+
+
-
-
-

Updates

- 0 +
+
+
+
+

Updates

+ Noch nicht geprueft +
+
+ + +
+
+ +
Keine passenden Updates gefunden.
+ + + + + + + + + + + + +
+ + - -
Keine Updates gefunden.
- - - - - - - - - - -
-
@@ -85,16 +161,16 @@ const indexHTML = ` const appCSS = ` :root { color-scheme: dark; - --bg: #101114; - --panel: #191b20; - --panel-soft: #20242b; - --text: #f1f5f9; - --muted: #9aa4b2; - --line: #303640; - --accent: #42d392; - --accent-strong: #2ab67c; - --warn: #f5c451; - --danger: #ff6b6b; + --bg: #111317; + --panel: #191d23; + --panel-soft: #222832; + --text: #f4f7fb; + --muted: #9ba7b6; + --line: #303844; + --accent: #44d19d; + --accent-strong: #2eb984; + --warn: #f4c95d; + --danger: #ff6f61; } * { @@ -117,9 +193,9 @@ select { } .shell { - width: min(1180px, calc(100vw - 32px)); + width: min(1260px, calc(100vw - 32px)); margin: 0 auto; - padding: 28px 0; + padding: 24px 0; } .topbar { @@ -127,31 +203,40 @@ select { align-items: center; justify-content: space-between; gap: 18px; - margin-bottom: 18px; + margin-bottom: 16px; } h1, h2, -p { +p, +dl, +dd { margin: 0; } h1 { font-size: 28px; - font-weight: 720; + font-weight: 740; } h2 { - font-size: 16px; - font-weight: 680; + font-size: 15px; + font-weight: 700; +} + +#summary, +#lastCheck, +#ignoredHint, +#saveState { + color: var(--muted); } #summary { margin-top: 5px; - color: var(--muted); } -.actions { +.actions, +.tools { display: flex; gap: 10px; } @@ -172,15 +257,15 @@ button:hover { button:disabled { cursor: progress; - opacity: 0.65; + opacity: 0.62; } #installBtn, form button { border-color: transparent; + color: #06130e; background: var(--accent); - color: #07110d; - font-weight: 700; + font-weight: 760; } #installBtn:hover, @@ -188,47 +273,86 @@ form button:hover { background: var(--accent-strong); } -.content { +.stats { display: grid; - grid-template-columns: minmax(0, 1fr) 320px; - gap: 18px; - align-items: start; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 16px; } +.stat, .panel { border: 1px solid var(--line); border-radius: 8px; background: var(--panel); } +.stat { + display: grid; + gap: 6px; + min-height: 82px; + padding: 14px; +} + +.stat span, +dt, +small { + color: var(--muted); +} + +.stat strong { + font-size: 24px; +} + +.content { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 16px; + align-items: start; +} + +.main-column, +.side-column { + display: grid; + gap: 16px; +} + .panel-head { display: flex; align-items: center; justify-content: space-between; - min-height: 54px; + gap: 14px; + min-height: 58px; padding: 0 16px; border-bottom: 1px solid var(--line); } -#countBadge { - display: inline-flex; +.toolbar { align-items: center; - justify-content: center; - min-width: 32px; - height: 26px; - border-radius: 999px; - color: #07110d; - background: var(--accent); - font-weight: 760; +} + +input, +select { + min-height: 40px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0 10px; + color: var(--text); + background: var(--panel-soft); +} + +#searchInput { + width: min(32vw, 320px); } .warnings { margin: 14px 16px 0; padding: 12px; - border: 1px solid rgba(245, 196, 81, 0.45); + border: 1px solid rgba(244, 201, 93, 0.45); border-radius: 8px; color: var(--warn); - background: rgba(245, 196, 81, 0.08); + background: rgba(244, 201, 93, 0.08); + white-space: pre-wrap; } .empty { @@ -263,13 +387,53 @@ td { td:first-child { width: 92px; color: var(--accent); - font-weight: 700; + font-weight: 720; +} + +td:last-child { + width: 132px; + text-align: right; } tr:last-child td { border-bottom: 0; } +.ghost { + min-height: 34px; + padding: 0 10px; + color: var(--muted); + background: transparent; +} + +.danger { + color: var(--danger); +} + +.system-list { + display: grid; + gap: 14px; + padding: 16px; +} + +.system-list div { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.system-list dd { + text-align: right; +} + +#lockState { + color: var(--accent); +} + +#lockState.locked { + color: var(--danger); +} + form { display: grid; gap: 16px; @@ -288,43 +452,33 @@ label { color: var(--text); } -input, -select { - width: 100%; - min-height: 40px; - border: 1px solid var(--line); - border-radius: 8px; - padding: 0 10px; - color: var(--text); - background: var(--panel-soft); -} - input[type="checkbox"] { width: 18px; min-height: 18px; accent-color: var(--accent); } -small, -#saveState { - color: var(--muted); -} - -@media (max-width: 820px) { +@media (max-width: 920px) { .shell { - width: min(100vw - 20px, 1180px); + width: min(100vw - 20px, 1260px); padding: 16px 0; } .topbar, - .content { - display: grid; + .content, + .stats { grid-template-columns: 1fr; + display: grid; } - .actions { - grid-template-columns: 48px 1fr; + .actions, + .tools { display: grid; + grid-template-columns: 1fr 1fr; + } + + #searchInput { + width: 100%; } th:nth-child(3), @@ -336,19 +490,37 @@ small, const appJS = ` const summary = document.querySelector("#summary"); -const countBadge = document.querySelector("#countBadge"); +const activeCount = document.querySelector("#activeCount"); +const ignoredCount = document.querySelector("#ignoredCount"); +const diskFree = document.querySelector("#diskFree"); +const kernel = document.querySelector("#kernel"); const refreshBtn = document.querySelector("#refreshBtn"); const installBtn = document.querySelector("#installBtn"); const updatesTable = document.querySelector("#updatesTable"); const updatesBody = document.querySelector("#updatesBody"); +const ignoredPanel = document.querySelector("#ignoredPanel"); +const ignoredBody = document.querySelector("#ignoredBody"); +const ignoredHint = document.querySelector("#ignoredHint"); const emptyState = document.querySelector("#emptyState"); const warnings = document.querySelector("#warnings"); +const searchInput = document.querySelector("#searchInput"); +const sourceFilter = document.querySelector("#sourceFilter"); +const lastCheck = document.querySelector("#lastCheck"); +const lastSuccess = document.querySelector("#lastSuccess"); +const aurHelper = document.querySelector("#aurHelper"); +const terminalStatus = document.querySelector("#terminalStatus"); +const lockState = document.querySelector("#lockState"); const form = document.querySelector("#settingsForm"); const checkAur = document.querySelector("#checkAur"); +const showIgnored = document.querySelector("#showIgnored"); const reminderHours = document.querySelector("#reminderHours"); +const autoRefreshMinutes = document.querySelector("#autoRefreshMinutes"); const terminal = document.querySelector("#terminal"); const saveState = document.querySelector("#saveState"); +let currentData = null; +let refreshTimer = null; + async function request(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json" }, @@ -361,31 +533,116 @@ async function request(path, options = {}) { return data; } -function render(data) { - summary.textContent = data.summary; - countBadge.textContent = data.total; - installBtn.disabled = data.total === 0; +function formatDate(value) { + if (!value || value.startsWith("0001-")) { + return "-"; + } + return new Intl.DateTimeFormat("de-DE", { + dateStyle: "short", + timeStyle: "short", + }).format(new Date(value)); +} - checkAur.checked = data.settings.check_aur; - reminderHours.value = data.settings.reminder_interval_hours; - terminal.value = data.settings.terminal || "auto"; - - updatesBody.replaceChildren(); - for (const pkg of data.packages) { - const row = document.createElement("tr"); - for (const value of [pkg.Source, pkg.Name, pkg.Current || "-", pkg.Available || "-"]) { - const cell = document.createElement("td"); - cell.textContent = value; - row.appendChild(cell); +function visiblePackages() { + if (!currentData) { + return []; + } + const query = searchInput.value.trim().toLowerCase(); + const source = sourceFilter.value; + return currentData.packages.filter((pkg) => { + if (pkg.Ignored && !currentData.settings.show_ignored) { + return false; } - updatesBody.appendChild(row); + if (source !== "all" && pkg.Source !== source) { + return false; + } + if (query && !pkg.Name.toLowerCase().includes(query)) { + return false; + } + return !pkg.Ignored; + }); +} + +function renderTable() { + const packages = visiblePackages(); + updatesBody.replaceChildren(); + for (const pkg of packages) { + const row = document.createElement("tr"); + row.append(cell(pkg.Source)); + row.append(cell(pkg.Name)); + row.append(cell(pkg.Current || "-")); + row.append(cell(pkg.Available || "-")); + + const actionCell = document.createElement("td"); + const button = document.createElement("button"); + button.className = "ghost"; + button.type = "button"; + button.textContent = "Ausblenden"; + button.addEventListener("click", () => setIgnored(pkg.Name, true)); + actionCell.append(button); + row.append(actionCell); + updatesBody.append(row); } - updatesTable.hidden = data.packages.length === 0; - emptyState.hidden = data.packages.length !== 0; + updatesTable.hidden = packages.length === 0; + emptyState.hidden = packages.length !== 0; +} + +function renderIgnored() { + const ignored = currentData.packages.filter((pkg) => pkg.Ignored); + ignoredBody.replaceChildren(); + ignoredPanel.hidden = ignored.length === 0 || !currentData.settings.show_ignored; + ignoredHint.textContent = ignored.length === 1 ? "1 Paket" : ignored.length + " Pakete"; + + for (const pkg of ignored) { + const row = document.createElement("tr"); + row.append(cell(pkg.Name)); + row.append(cell(pkg.Available || "-")); + const actionCell = document.createElement("td"); + const button = document.createElement("button"); + button.className = "ghost"; + button.type = "button"; + button.textContent = "Einblenden"; + button.addEventListener("click", () => setIgnored(pkg.Name, false)); + actionCell.append(button); + row.append(actionCell); + ignoredBody.append(row); + } +} + +function cell(value) { + const element = document.createElement("td"); + element.textContent = value; + return element; +} + +function render(data) { + currentData = data; + summary.textContent = data.summary; + activeCount.textContent = data.total; + ignoredCount.textContent = data.ignored_total; + installBtn.disabled = data.total === 0 || data.system.pacman_locked; + diskFree.textContent = data.system.disk_free + " frei"; + kernel.textContent = data.system.kernel; + aurHelper.textContent = data.system.aur_helper || "nicht gefunden"; + terminalStatus.textContent = data.system.terminal || "nicht gefunden"; + lastCheck.textContent = "Letzte Pruefung: " + formatDate(data.state.last_check); + lastSuccess.textContent = formatDate(data.state.last_success); + lockState.textContent = data.system.pacman_locked ? "Pacman gesperrt" : "Bereit"; + lockState.classList.toggle("locked", data.system.pacman_locked); + + checkAur.checked = data.settings.check_aur; + showIgnored.checked = data.settings.show_ignored; + reminderHours.value = data.settings.reminder_interval_hours; + autoRefreshMinutes.value = data.settings.auto_refresh_minutes; + terminal.value = data.settings.terminal || "auto"; warnings.hidden = data.warnings.length === 0; warnings.textContent = data.warnings.join("\\n"); + + renderTable(); + renderIgnored(); + scheduleAutoRefresh(); } async function loadStatus() { @@ -400,7 +657,26 @@ async function loadStatus() { } } +function scheduleAutoRefresh() { + clearInterval(refreshTimer); + const minutes = Number(autoRefreshMinutes.value); + if (!minutes || minutes < 1) { + return; + } + refreshTimer = setInterval(loadStatus, minutes * 60 * 1000); +} + +async function setIgnored(name, ignored) { + await request("/api/ignore", { + method: "POST", + body: JSON.stringify({ name, ignored }), + }); + await loadStatus(); +} + refreshBtn.addEventListener("click", loadStatus); +searchInput.addEventListener("input", renderTable); +sourceFilter.addEventListener("change", renderTable); installBtn.addEventListener("click", async () => { installBtn.disabled = true; @@ -422,8 +698,11 @@ form.addEventListener("submit", async (event) => { method: "PUT", body: JSON.stringify({ check_aur: checkAur.checked, + show_ignored: showIgnored.checked, reminder_interval_hours: Number(reminderHours.value), + auto_refresh_minutes: Number(autoRefreshMinutes.value), terminal: terminal.value, + ignored_packages: currentData?.settings.ignored_packages || [], }), }); saveState.textContent = "Gespeichert"; diff --git a/internal/gui/server.go b/internal/gui/server.go index 4f0ec6a..bf96ef3 100644 --- a/internal/gui/server.go +++ b/internal/gui/server.go @@ -8,40 +8,62 @@ import ( "log" "net" "net/http" + "os" "os/exec" "runtime" "strings" + "syscall" "time" "lazy-update-manager/internal/config" + "lazy-update-manager/internal/state" "lazy-update-manager/internal/updater" ) type Server struct { configPath string + statePath string } type StatusResponse struct { - Summary string `json:"summary"` - Total int `json:"total"` - Packages []updater.Package `json:"packages"` - Warnings []string `json:"warnings"` - Settings config.Config `json:"settings"` + Summary string `json:"summary"` + Total int `json:"total"` + IgnoredTotal int `json:"ignored_total"` + Packages []updater.Package `json:"packages"` + Warnings []string `json:"warnings"` + Settings config.Config `json:"settings"` + State state.Store `json:"state"` + System SystemStatus `json:"system"` } -func Run(configPath string, openBrowser bool) error { +type SystemStatus struct { + AURHelper string `json:"aur_helper"` + Terminal string `json:"terminal"` + PacmanLocked bool `json:"pacman_locked"` + DiskFree string `json:"disk_free"` + DiskFreePercent int `json:"disk_free_percent"` + Kernel string `json:"kernel"` +} + +type ignoreRequest struct { + Name string `json:"name"` + Ignored bool `json:"ignored"` +} + +func Run(configPath, statePath string, openBrowser bool) error { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return err } - server := &Server{configPath: configPath} + server := &Server{configPath: configPath, statePath: statePath} mux := http.NewServeMux() mux.HandleFunc("/", server.index) mux.HandleFunc("/app.css", server.css) mux.HandleFunc("/app.js", server.js) mux.HandleFunc("/api/status", server.status) mux.HandleFunc("/api/settings", server.settings) + mux.HandleFunc("/api/ignore", server.ignore) mux.HandleFunc("/api/install", server.install) url := "http://" + listener.Addr().String() @@ -74,11 +96,28 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() - result, err := updater.CheckWithOptions(ctx, updater.Options{CheckAUR: cfg.CheckAUR}) + result, err := updater.CheckWithOptions(ctx, updater.Options{ + CheckAUR: cfg.CheckAUR, + IgnoredPackages: cfg.IgnoredPackages, + }) if err != nil { writeError(w, err, http.StatusInternalServerError) return } + now := time.Now() + store, err := state.Load(s.statePath) + if err != nil { + writeError(w, err, http.StatusInternalServerError) + return + } + store.LastCheck = now + store.LastSuccess = now + store.LastUpdateCount = result.Total() + store.LastSummary = result.Summary() + if err := state.Save(s.statePath, store); err != nil { + writeError(w, err, http.StatusInternalServerError) + return + } packages := result.Packages if packages == nil { @@ -90,11 +129,14 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) { } writeJSON(w, StatusResponse{ - Summary: result.Summary(), - Total: result.Total(), - Packages: packages, - Warnings: warnings, - Settings: cfg, + Summary: result.Summary(), + Total: result.Total(), + IgnoredTotal: result.IgnoredTotal(), + Packages: packages, + Warnings: warnings, + Settings: cfg, + State: store, + System: systemStatus(cfg), }) } @@ -123,6 +165,38 @@ func (s *Server) settings(w http.ResponseWriter, r *http.Request) { } } +func (s *Server) ignore(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + var req ignoreRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, err, http.StatusBadRequest) + return + } + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" { + writeError(w, errors.New("package name is required"), http.StatusBadRequest) + return + } + + cfg, err := config.Load(s.configPath) + if err != nil { + writeError(w, err, http.StatusInternalServerError) + return + } + + cfg.IgnoredPackages = setIgnored(cfg.IgnoredPackages, req.Name, req.Ignored) + if err := config.Save(s.configPath, cfg); err != nil { + writeError(w, err, http.StatusInternalServerError) + return + } + + writeJSON(w, cfg) +} + func (s *Server) install(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -150,6 +224,80 @@ func (s *Server) install(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]string{"status": "started"}) } +func setIgnored(names []string, name string, ignored bool) []string { + seen := map[string]bool{} + next := []string{} + for _, item := range names { + item = strings.TrimSpace(item) + if item == "" || item == name || seen[item] { + continue + } + seen[item] = true + next = append(next, item) + } + if ignored { + next = append(next, name) + } + return next +} + +func systemStatus(cfg config.Config) SystemStatus { + terminal, _, _ := terminalCommand(cfg) + return SystemStatus{ + AURHelper: updater.AURHelper(), + Terminal: terminal, + PacmanLocked: fileExists("/var/lib/pacman/db.lck"), + DiskFree: diskFree("/"), + DiskFreePercent: diskFreePercent("/"), + Kernel: kernelVersion(), + } +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func diskFree(path string) string { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil { + return "unknown" + } + bytes := stat.Bavail * uint64(stat.Bsize) + return humanBytes(bytes) +} + +func diskFreePercent(path string) int { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil || stat.Blocks == 0 { + return 0 + } + return int((stat.Bavail * 100) / stat.Blocks) +} + +func humanBytes(bytes uint64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + value := float64(bytes) + for _, suffix := range []string{"KiB", "MiB", "GiB", "TiB"} { + value = value / unit + if value < unit { + return fmt.Sprintf("%.1f %s", value, suffix) + } + } + return fmt.Sprintf("%.1f PiB", value/unit) +} + +func kernelVersion() string { + out, err := exec.Command("uname", "-r").Output() + if err != nil { + return "unknown" + } + return strings.TrimSpace(string(out)) +} + func terminalCommand(cfg config.Config) (string, []string, error) { updateCommand := "lazy-update-manager update; printf '\\nDone. Press enter to close... '; read _" if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR { diff --git a/internal/state/state.go b/internal/state/state.go index 44ca480..16b0a6b 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -11,6 +11,9 @@ import ( type Store struct { LastReminder time.Time `json:"last_reminder"` LastUpdateCount int `json:"last_update_count"` + LastCheck time.Time `json:"last_check"` + LastSuccess time.Time `json:"last_success"` + LastSummary string `json:"last_summary"` } func Load(path string) (Store, error) { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 5206478..060f55a 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -12,6 +12,7 @@ type Package struct { Name string Current string Available string + Ignored bool } type Result struct { @@ -20,11 +21,28 @@ type Result struct { } type Options struct { - CheckAUR bool + CheckAUR bool + IgnoredPackages []string } func (r Result) Total() int { - return len(r.Packages) + total := 0 + for _, pkg := range r.Packages { + if !pkg.Ignored { + total++ + } + } + return total +} + +func (r Result) IgnoredTotal() int { + total := 0 + for _, pkg := range r.Packages { + if pkg.Ignored { + total++ + } + } + return total } func (r Result) Summary() string { @@ -60,9 +78,26 @@ func CheckWithOptions(ctx context.Context, opts Options) (Result, error) { result.Packages = append(result.Packages, aur...) } + markIgnored(&result, opts.IgnoredPackages) return result, nil } +func markIgnored(result *Result, names []string) { + ignored := map[string]bool{} + for _, name := range names { + name = strings.TrimSpace(name) + if name != "" { + ignored[name] = true + } + } + if len(ignored) == 0 { + return + } + for i := range result.Packages { + result.Packages[i].Ignored = ignored[result.Packages[i].Name] + } +} + func checkPacman(ctx context.Context) ([]Package, error) { if _, err := exec.LookPath("checkupdates"); err == nil { out, err := exec.CommandContext(ctx, "checkupdates").Output()