feat: add advanced update management features
This commit is contained in:
@@ -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
|
- Sends notifications with `notify-send`, with `hyprctl notify` as fallback
|
||||||
- Provides a graphical browser UI for checking and installing updates
|
- Provides a graphical browser UI for checking and installing updates
|
||||||
- Provides an Electron desktop app wrapper for the GUI
|
- 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
|
- Includes a systemd user timer that checks every two hours and reminds weekly
|
||||||
- Provides an interactive `update` command
|
- Provides an interactive `update` command
|
||||||
|
|
||||||
@@ -48,9 +49,13 @@ make build
|
|||||||
The GUI opens in your browser and lets you:
|
The GUI opens in your browser and lets you:
|
||||||
|
|
||||||
- refresh the update list
|
- refresh the update list
|
||||||
|
- search and filter updates by source
|
||||||
- start update installation in a terminal
|
- 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
|
- enable or disable AUR checks
|
||||||
- change the reminder interval
|
- change the reminder interval
|
||||||
|
- configure automatic refresh
|
||||||
- choose the terminal used for installing updates
|
- choose the terminal used for installing updates
|
||||||
|
|
||||||
## Desktop App
|
## Desktop App
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ func status() int {
|
|||||||
return 1
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -85,7 +88,10 @@ func check(args []string) int {
|
|||||||
return 1
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -117,7 +123,10 @@ func notifyUpdates(args []string) int {
|
|||||||
return 1
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -146,6 +155,9 @@ func notifyUpdates(args []string) int {
|
|||||||
|
|
||||||
store.LastReminder = now
|
store.LastReminder = now
|
||||||
store.LastUpdateCount = result.Total()
|
store.LastUpdateCount = result.Total()
|
||||||
|
store.LastCheck = now
|
||||||
|
store.LastSuccess = now
|
||||||
|
store.LastSummary = result.Summary()
|
||||||
if err := state.Save(defaultStatePath(), store); err != nil {
|
if err := state.Save(defaultStatePath(), store); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -162,7 +174,7 @@ func runGUI(args []string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gui.Run(defaultConfigPath(), !*noOpen); err != nil {
|
if err := gui.Run(defaultConfigPath(), defaultStatePath(), !*noOpen); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ type Config struct {
|
|||||||
CheckAUR bool `json:"check_aur"`
|
CheckAUR bool `json:"check_aur"`
|
||||||
ReminderIntervalHours int `json:"reminder_interval_hours"`
|
ReminderIntervalHours int `json:"reminder_interval_hours"`
|
||||||
Terminal string `json:"terminal"`
|
Terminal string `json:"terminal"`
|
||||||
|
AutoRefreshMinutes int `json:"auto_refresh_minutes"`
|
||||||
|
ShowIgnored bool `json:"show_ignored"`
|
||||||
|
IgnoredPackages []string `json:"ignored_packages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Default() Config {
|
func Default() Config {
|
||||||
@@ -18,6 +21,9 @@ func Default() Config {
|
|||||||
CheckAUR: true,
|
CheckAUR: true,
|
||||||
ReminderIntervalHours: 168,
|
ReminderIntervalHours: 168,
|
||||||
Terminal: "auto",
|
Terminal: "auto",
|
||||||
|
AutoRefreshMinutes: 30,
|
||||||
|
ShowIgnored: false,
|
||||||
|
IgnoredPackages: []string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,5 +67,11 @@ func normalize(cfg Config) Config {
|
|||||||
if cfg.Terminal == "" {
|
if cfg.Terminal == "" {
|
||||||
cfg.Terminal = Default().Terminal
|
cfg.Terminal = Default().Terminal
|
||||||
}
|
}
|
||||||
|
if cfg.AutoRefreshMinutes < 0 {
|
||||||
|
cfg.AutoRefreshMinutes = Default().AutoRefreshMinutes
|
||||||
|
}
|
||||||
|
if cfg.IgnoredPackages == nil {
|
||||||
|
cfg.IgnoredPackages = []string{}
|
||||||
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,49 @@ const indexHTML = `<!doctype html>
|
|||||||
<p id="summary">Pruefe Updates...</p>
|
<p id="summary">Pruefe Updates...</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="refreshBtn" type="button" title="Updates pruefen">⟳</button>
|
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
|
||||||
<button id="installBtn" type="button">Installieren</button>
|
<button id="installBtn" type="button">Installieren</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span>Aktive Updates</span>
|
||||||
|
<strong id="activeCount">0</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Ausgeblendet</span>
|
||||||
|
<strong id="ignoredCount">0</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Freier Speicher</span>
|
||||||
|
<strong id="diskFree">-</strong>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span>Kernel</span>
|
||||||
|
<strong id="kernel">-</strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="content">
|
<section class="content">
|
||||||
|
<div class="main-column">
|
||||||
<div class="panel updates-panel">
|
<div class="panel updates-panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head toolbar">
|
||||||
|
<div>
|
||||||
<h2>Updates</h2>
|
<h2>Updates</h2>
|
||||||
<span id="countBadge">0</span>
|
<span id="lastCheck">Noch nicht geprueft</span>
|
||||||
|
</div>
|
||||||
|
<div class="tools">
|
||||||
|
<input id="searchInput" type="search" placeholder="Pakete suchen">
|
||||||
|
<select id="sourceFilter">
|
||||||
|
<option value="all">Alle Quellen</option>
|
||||||
|
<option value="pacman">Pacman</option>
|
||||||
|
<option value="aur">AUR</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="warnings" class="warnings" hidden></div>
|
<div id="warnings" class="warnings" hidden></div>
|
||||||
<div id="emptyState" class="empty">Keine Updates gefunden.</div>
|
<div id="emptyState" class="empty">Keine passenden Updates gefunden.</div>
|
||||||
<table id="updatesTable" hidden>
|
<table id="updatesTable" hidden>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -36,13 +66,47 @@ const indexHTML = `<!doctype html>
|
|||||||
<th>Paket</th>
|
<th>Paket</th>
|
||||||
<th>Aktuell</th>
|
<th>Aktuell</th>
|
||||||
<th>Verfuegbar</th>
|
<th>Verfuegbar</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="updatesBody"></tbody>
|
<tbody id="updatesBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="panel settings-panel">
|
<div id="ignoredPanel" class="panel ignored-panel" hidden>
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Ausgeblendete Updates</h2>
|
||||||
|
<span id="ignoredHint"></span>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tbody id="ignoredBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="side-column">
|
||||||
|
<section class="panel system-panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>System</h2>
|
||||||
|
<span id="lockState"></span>
|
||||||
|
</div>
|
||||||
|
<dl class="system-list">
|
||||||
|
<div>
|
||||||
|
<dt>AUR Helper</dt>
|
||||||
|
<dd id="aurHelper">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Terminal</dt>
|
||||||
|
<dd id="terminalStatus">-</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Letzte erfolgreiche Pruefung</dt>
|
||||||
|
<dd id="lastSuccess">-</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel settings-panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>Einstellungen</h2>
|
<h2>Einstellungen</h2>
|
||||||
<span id="saveState"></span>
|
<span id="saveState"></span>
|
||||||
@@ -53,12 +117,23 @@ const indexHTML = `<!doctype html>
|
|||||||
<span>AUR Updates pruefen</span>
|
<span>AUR Updates pruefen</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="toggle">
|
||||||
|
<input id="showIgnored" type="checkbox">
|
||||||
|
<span>Ausgeblendete anzeigen</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Erinnerung alle</span>
|
<span>Erinnerung alle</span>
|
||||||
<input id="reminderHours" type="number" min="1" step="1">
|
<input id="reminderHours" type="number" min="1" step="1">
|
||||||
<small>Stunden</small>
|
<small>Stunden</small>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span>Auto-Refresh</span>
|
||||||
|
<input id="autoRefreshMinutes" type="number" min="0" step="1">
|
||||||
|
<small>Minuten, 0 deaktiviert</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Terminal</span>
|
<span>Terminal</span>
|
||||||
<select id="terminal">
|
<select id="terminal">
|
||||||
@@ -75,6 +150,7 @@ const indexHTML = `<!doctype html>
|
|||||||
|
|
||||||
<button type="submit">Speichern</button>
|
<button type="submit">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -85,16 +161,16 @@ const indexHTML = `<!doctype html>
|
|||||||
const appCSS = `
|
const appCSS = `
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #101114;
|
--bg: #111317;
|
||||||
--panel: #191b20;
|
--panel: #191d23;
|
||||||
--panel-soft: #20242b;
|
--panel-soft: #222832;
|
||||||
--text: #f1f5f9;
|
--text: #f4f7fb;
|
||||||
--muted: #9aa4b2;
|
--muted: #9ba7b6;
|
||||||
--line: #303640;
|
--line: #303844;
|
||||||
--accent: #42d392;
|
--accent: #44d19d;
|
||||||
--accent-strong: #2ab67c;
|
--accent-strong: #2eb984;
|
||||||
--warn: #f5c451;
|
--warn: #f4c95d;
|
||||||
--danger: #ff6b6b;
|
--danger: #ff6f61;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -117,9 +193,9 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
width: min(1180px, calc(100vw - 32px));
|
width: min(1260px, calc(100vw - 32px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 28px 0;
|
padding: 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
@@ -127,31 +203,40 @@ select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
p {
|
p,
|
||||||
|
dl,
|
||||||
|
dd {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 720;
|
font-weight: 740;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 680;
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary,
|
||||||
|
#lastCheck,
|
||||||
|
#ignoredHint,
|
||||||
|
#saveState {
|
||||||
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary {
|
#summary {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions,
|
||||||
|
.tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
@@ -172,15 +257,15 @@ button:hover {
|
|||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
cursor: progress;
|
cursor: progress;
|
||||||
opacity: 0.65;
|
opacity: 0.62;
|
||||||
}
|
}
|
||||||
|
|
||||||
#installBtn,
|
#installBtn,
|
||||||
form button {
|
form button {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
color: #06130e;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #07110d;
|
font-weight: 760;
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#installBtn:hover,
|
#installBtn:hover,
|
||||||
@@ -188,47 +273,86 @@ form button:hover {
|
|||||||
background: var(--accent-strong);
|
background: var(--accent-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 18px;
|
gap: 12px;
|
||||||
align-items: start;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat,
|
||||||
.panel {
|
.panel {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--panel);
|
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 {
|
.panel-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 54px;
|
gap: 14px;
|
||||||
|
min-height: 58px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
#countBadge {
|
.toolbar {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
}
|
||||||
min-width: 32px;
|
|
||||||
height: 26px;
|
input,
|
||||||
border-radius: 999px;
|
select {
|
||||||
color: #07110d;
|
min-height: 40px;
|
||||||
background: var(--accent);
|
border: 1px solid var(--line);
|
||||||
font-weight: 760;
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--panel-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchInput {
|
||||||
|
width: min(32vw, 320px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.warnings {
|
.warnings {
|
||||||
margin: 14px 16px 0;
|
margin: 14px 16px 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid rgba(245, 196, 81, 0.45);
|
border: 1px solid rgba(244, 201, 93, 0.45);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--warn);
|
color: var(--warn);
|
||||||
background: rgba(245, 196, 81, 0.08);
|
background: rgba(244, 201, 93, 0.08);
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
@@ -263,13 +387,53 @@ td {
|
|||||||
td:first-child {
|
td:first-child {
|
||||||
width: 92px;
|
width: 92px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 700;
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
width: 132px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:last-child td {
|
tr:last-child td {
|
||||||
border-bottom: 0;
|
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 {
|
form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -288,43 +452,33 @@ label {
|
|||||||
color: var(--text);
|
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"] {
|
input[type="checkbox"] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
accent-color: var(--accent);
|
accent-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
small,
|
@media (max-width: 920px) {
|
||||||
#saveState {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
|
||||||
.shell {
|
.shell {
|
||||||
width: min(100vw - 20px, 1180px);
|
width: min(100vw - 20px, 1260px);
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar,
|
.topbar,
|
||||||
.content {
|
.content,
|
||||||
display: grid;
|
.stats {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions,
|
||||||
grid-template-columns: 48px 1fr;
|
.tools {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchInput {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:nth-child(3),
|
th:nth-child(3),
|
||||||
@@ -336,19 +490,37 @@ small,
|
|||||||
|
|
||||||
const appJS = `
|
const appJS = `
|
||||||
const summary = document.querySelector("#summary");
|
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 refreshBtn = document.querySelector("#refreshBtn");
|
||||||
const installBtn = document.querySelector("#installBtn");
|
const installBtn = document.querySelector("#installBtn");
|
||||||
const updatesTable = document.querySelector("#updatesTable");
|
const updatesTable = document.querySelector("#updatesTable");
|
||||||
const updatesBody = document.querySelector("#updatesBody");
|
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 emptyState = document.querySelector("#emptyState");
|
||||||
const warnings = document.querySelector("#warnings");
|
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 form = document.querySelector("#settingsForm");
|
||||||
const checkAur = document.querySelector("#checkAur");
|
const checkAur = document.querySelector("#checkAur");
|
||||||
|
const showIgnored = document.querySelector("#showIgnored");
|
||||||
const reminderHours = document.querySelector("#reminderHours");
|
const reminderHours = document.querySelector("#reminderHours");
|
||||||
|
const autoRefreshMinutes = document.querySelector("#autoRefreshMinutes");
|
||||||
const terminal = document.querySelector("#terminal");
|
const terminal = document.querySelector("#terminal");
|
||||||
const saveState = document.querySelector("#saveState");
|
const saveState = document.querySelector("#saveState");
|
||||||
|
|
||||||
|
let currentData = null;
|
||||||
|
let refreshTimer = null;
|
||||||
|
|
||||||
async function request(path, options = {}) {
|
async function request(path, options = {}) {
|
||||||
const response = await fetch(path, {
|
const response = await fetch(path, {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -361,31 +533,116 @@ async function request(path, options = {}) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
if (!value || value.startsWith("0001-")) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat("de-DE", {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(new Date(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 = 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) {
|
function render(data) {
|
||||||
|
currentData = data;
|
||||||
summary.textContent = data.summary;
|
summary.textContent = data.summary;
|
||||||
countBadge.textContent = data.total;
|
activeCount.textContent = data.total;
|
||||||
installBtn.disabled = data.total === 0;
|
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;
|
checkAur.checked = data.settings.check_aur;
|
||||||
|
showIgnored.checked = data.settings.show_ignored;
|
||||||
reminderHours.value = data.settings.reminder_interval_hours;
|
reminderHours.value = data.settings.reminder_interval_hours;
|
||||||
|
autoRefreshMinutes.value = data.settings.auto_refresh_minutes;
|
||||||
terminal.value = data.settings.terminal || "auto";
|
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);
|
|
||||||
}
|
|
||||||
updatesBody.appendChild(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatesTable.hidden = data.packages.length === 0;
|
|
||||||
emptyState.hidden = data.packages.length !== 0;
|
|
||||||
|
|
||||||
warnings.hidden = data.warnings.length === 0;
|
warnings.hidden = data.warnings.length === 0;
|
||||||
warnings.textContent = data.warnings.join("\\n");
|
warnings.textContent = data.warnings.join("\\n");
|
||||||
|
|
||||||
|
renderTable();
|
||||||
|
renderIgnored();
|
||||||
|
scheduleAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStatus() {
|
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);
|
refreshBtn.addEventListener("click", loadStatus);
|
||||||
|
searchInput.addEventListener("input", renderTable);
|
||||||
|
sourceFilter.addEventListener("change", renderTable);
|
||||||
|
|
||||||
installBtn.addEventListener("click", async () => {
|
installBtn.addEventListener("click", async () => {
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
@@ -422,8 +698,11 @@ form.addEventListener("submit", async (event) => {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
check_aur: checkAur.checked,
|
check_aur: checkAur.checked,
|
||||||
|
show_ignored: showIgnored.checked,
|
||||||
reminder_interval_hours: Number(reminderHours.value),
|
reminder_interval_hours: Number(reminderHours.value),
|
||||||
|
auto_refresh_minutes: Number(autoRefreshMinutes.value),
|
||||||
terminal: terminal.value,
|
terminal: terminal.value,
|
||||||
|
ignored_packages: currentData?.settings.ignored_packages || [],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
saveState.textContent = "Gespeichert";
|
saveState.textContent = "Gespeichert";
|
||||||
|
|||||||
@@ -8,40 +8,62 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lazy-update-manager/internal/config"
|
"lazy-update-manager/internal/config"
|
||||||
|
"lazy-update-manager/internal/state"
|
||||||
"lazy-update-manager/internal/updater"
|
"lazy-update-manager/internal/updater"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
statePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
|
IgnoredTotal int `json:"ignored_total"`
|
||||||
Packages []updater.Package `json:"packages"`
|
Packages []updater.Package `json:"packages"`
|
||||||
Warnings []string `json:"warnings"`
|
Warnings []string `json:"warnings"`
|
||||||
Settings config.Config `json:"settings"`
|
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")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
server := &Server{configPath: configPath}
|
server := &Server{configPath: configPath, statePath: statePath}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", server.index)
|
mux.HandleFunc("/", server.index)
|
||||||
mux.HandleFunc("/app.css", server.css)
|
mux.HandleFunc("/app.css", server.css)
|
||||||
mux.HandleFunc("/app.js", server.js)
|
mux.HandleFunc("/app.js", server.js)
|
||||||
mux.HandleFunc("/api/status", server.status)
|
mux.HandleFunc("/api/status", server.status)
|
||||||
mux.HandleFunc("/api/settings", server.settings)
|
mux.HandleFunc("/api/settings", server.settings)
|
||||||
|
mux.HandleFunc("/api/ignore", server.ignore)
|
||||||
mux.HandleFunc("/api/install", server.install)
|
mux.HandleFunc("/api/install", server.install)
|
||||||
|
|
||||||
url := "http://" + listener.Addr().String()
|
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)
|
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
writeError(w, err, http.StatusInternalServerError)
|
writeError(w, err, http.StatusInternalServerError)
|
||||||
return
|
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
|
packages := result.Packages
|
||||||
if packages == nil {
|
if packages == nil {
|
||||||
@@ -92,9 +131,12 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, StatusResponse{
|
writeJSON(w, StatusResponse{
|
||||||
Summary: result.Summary(),
|
Summary: result.Summary(),
|
||||||
Total: result.Total(),
|
Total: result.Total(),
|
||||||
|
IgnoredTotal: result.IgnoredTotal(),
|
||||||
Packages: packages,
|
Packages: packages,
|
||||||
Warnings: warnings,
|
Warnings: warnings,
|
||||||
Settings: cfg,
|
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) {
|
func (s *Server) install(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
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"})
|
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) {
|
func terminalCommand(cfg config.Config) (string, []string, error) {
|
||||||
updateCommand := "lazy-update-manager update; printf '\\nDone. Press enter to close... '; read _"
|
updateCommand := "lazy-update-manager update; printf '\\nDone. Press enter to close... '; read _"
|
||||||
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
|
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
type Store struct {
|
type Store struct {
|
||||||
LastReminder time.Time `json:"last_reminder"`
|
LastReminder time.Time `json:"last_reminder"`
|
||||||
LastUpdateCount int `json:"last_update_count"`
|
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) {
|
func Load(path string) (Store, error) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Package struct {
|
|||||||
Name string
|
Name string
|
||||||
Current string
|
Current string
|
||||||
Available string
|
Available string
|
||||||
|
Ignored bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -21,10 +22,27 @@ type Result struct {
|
|||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
CheckAUR bool
|
CheckAUR bool
|
||||||
|
IgnoredPackages []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Result) Total() int {
|
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 {
|
func (r Result) Summary() string {
|
||||||
@@ -60,9 +78,26 @@ func CheckWithOptions(ctx context.Context, opts Options) (Result, error) {
|
|||||||
result.Packages = append(result.Packages, aur...)
|
result.Packages = append(result.Packages, aur...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markIgnored(&result, opts.IgnoredPackages)
|
||||||
return result, nil
|
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) {
|
func checkPacman(ctx context.Context) ([]Package, error) {
|
||||||
if _, err := exec.LookPath("checkupdates"); err == nil {
|
if _, err := exec.LookPath("checkupdates"); err == nil {
|
||||||
out, err := exec.CommandContext(ctx, "checkupdates").Output()
|
out, err := exec.CommandContext(ctx, "checkupdates").Output()
|
||||||
|
|||||||
Reference in New Issue
Block a user