feat: add selected update installs and polished UI
This commit is contained in:
@@ -51,6 +51,7 @@ The GUI opens in your browser and lets you:
|
|||||||
- refresh the update list
|
- refresh the update list
|
||||||
- search and filter updates by source
|
- search and filter updates by source
|
||||||
- start update installation in a terminal
|
- start update installation in a terminal
|
||||||
|
- select individual packages and install only those when you explicitly want that workflow
|
||||||
- hide noisy packages from the active update count
|
- hide noisy packages from the active update count
|
||||||
- see system readiness, Pacman lock state, disk space, AUR helper, terminal, and kernel version
|
- see system readiness, Pacman lock state, disk space, AUR helper, terminal, and kernel version
|
||||||
- enable or disable AUR checks
|
- enable or disable AUR checks
|
||||||
@@ -58,6 +59,8 @@ The GUI opens in your browser and lets you:
|
|||||||
- configure automatic refresh
|
- configure automatic refresh
|
||||||
- choose the terminal used for installing updates
|
- choose the terminal used for installing updates
|
||||||
|
|
||||||
|
Selective package installs use `pacman -S --needed` or the configured AUR helper with `-S --needed`. On Arch, full system updates are still the safer default because partial upgrades can cause dependency mismatches.
|
||||||
|
|
||||||
## Desktop App
|
## Desktop App
|
||||||
|
|
||||||
Install the Electron dependencies once:
|
Install the Electron dependencies once:
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const indexHTML = `<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
|
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
|
||||||
<button id="installBtn" type="button">Installieren</button>
|
<button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button>
|
||||||
|
<button id="installBtn" type="button">Alle installieren</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -57,11 +58,19 @@ const indexHTML = `<!doctype html>
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="selection-bar">
|
||||||
|
<label class="select-all">
|
||||||
|
<input id="selectAll" type="checkbox">
|
||||||
|
<span id="selectionText">0 Updates ausgewaehlt</span>
|
||||||
|
</label>
|
||||||
|
<span>Selektive Updates koennen unter Arch riskant sein. Vollupdate bleibt empfohlen.</span>
|
||||||
|
</div>
|
||||||
<div id="warnings" class="warnings" hidden></div>
|
<div id="warnings" class="warnings" hidden></div>
|
||||||
<div id="emptyState" class="empty">Keine passenden 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>
|
||||||
|
<th></th>
|
||||||
<th>Quelle</th>
|
<th>Quelle</th>
|
||||||
<th>Paket</th>
|
<th>Paket</th>
|
||||||
<th>Aktuell</th>
|
<th>Aktuell</th>
|
||||||
@@ -161,14 +170,16 @@ const indexHTML = `<!doctype html>
|
|||||||
const appCSS = `
|
const appCSS = `
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #111317;
|
--bg: #0f1215;
|
||||||
--panel: #191d23;
|
--panel: #191e25;
|
||||||
--panel-soft: #222832;
|
--panel-soft: #232a34;
|
||||||
|
--panel-lift: #202732;
|
||||||
--text: #f4f7fb;
|
--text: #f4f7fb;
|
||||||
--muted: #9ba7b6;
|
--muted: #9ba7b6;
|
||||||
--line: #303844;
|
--line: #303844;
|
||||||
--accent: #44d19d;
|
--accent: #44d19d;
|
||||||
--accent-strong: #2eb984;
|
--accent-strong: #2eb984;
|
||||||
|
--blue: #6ea8fe;
|
||||||
--warn: #f4c95d;
|
--warn: #f4c95d;
|
||||||
--danger: #ff6f61;
|
--danger: #ff6f61;
|
||||||
}
|
}
|
||||||
@@ -182,7 +193,9 @@ body {
|
|||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--bg);
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(68, 209, 157, 0.14), transparent 34rem),
|
||||||
|
linear-gradient(180deg, #13171d 0%, var(--bg) 22rem);
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,10 +262,12 @@ button {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--panel-soft);
|
background: var(--panel-soft);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@@ -273,6 +288,17 @@ form button:hover {
|
|||||||
background: var(--accent-strong);
|
background: var(--accent-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#installSelectedBtn {
|
||||||
|
border-color: rgba(110, 168, 254, 0.42);
|
||||||
|
color: #dcebff;
|
||||||
|
background: rgba(110, 168, 254, 0.12);
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
#installSelectedBtn:hover {
|
||||||
|
border-color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
@@ -285,6 +311,7 @@ form button:hover {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
@@ -292,6 +319,7 @@ form button:hover {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-height: 82px;
|
min-height: 82px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
|
background: linear-gradient(180deg, var(--panel-lift), var(--panel));
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat span,
|
.stat span,
|
||||||
@@ -345,6 +373,27 @@ select {
|
|||||||
width: min(32vw, 320px);
|
width: min(32vw, 320px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
color: var(--warn);
|
||||||
|
background: rgba(244, 201, 93, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all {
|
||||||
|
display: flex;
|
||||||
|
grid-template-columns: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.warnings {
|
.warnings {
|
||||||
margin: 14px 16px 0;
|
margin: 14px 16px 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -384,7 +433,12 @@ td {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
td:first-child {
|
.updates-panel td:first-child {
|
||||||
|
width: 46px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.updates-panel td:nth-child(2) {
|
||||||
width: 92px;
|
width: 92px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 720;
|
font-weight: 720;
|
||||||
@@ -399,6 +453,10 @@ tr:last-child td {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr.selected td {
|
||||||
|
background: rgba(110, 168, 254, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
@@ -477,12 +535,16 @@ input[type="checkbox"] {
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-bar {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
#searchInput {
|
#searchInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:nth-child(3),
|
th:nth-child(4),
|
||||||
td:nth-child(3) {
|
td:nth-child(4) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,6 +558,9 @@ const diskFree = document.querySelector("#diskFree");
|
|||||||
const kernel = document.querySelector("#kernel");
|
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 installSelectedBtn = document.querySelector("#installSelectedBtn");
|
||||||
|
const selectAll = document.querySelector("#selectAll");
|
||||||
|
const selectionText = document.querySelector("#selectionText");
|
||||||
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 ignoredPanel = document.querySelector("#ignoredPanel");
|
||||||
@@ -520,6 +585,7 @@ const saveState = document.querySelector("#saveState");
|
|||||||
|
|
||||||
let currentData = null;
|
let currentData = null;
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
|
let selectedPackages = new Set();
|
||||||
|
|
||||||
async function request(path, options = {}) {
|
async function request(path, options = {}) {
|
||||||
const response = await fetch(path, {
|
const response = await fetch(path, {
|
||||||
@@ -568,6 +634,22 @@ function renderTable() {
|
|||||||
updatesBody.replaceChildren();
|
updatesBody.replaceChildren();
|
||||||
for (const pkg of packages) {
|
for (const pkg of packages) {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
|
row.classList.toggle("selected", selectedPackages.has(pkg.Name));
|
||||||
|
|
||||||
|
const selectCell = document.createElement("td");
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.checked = selectedPackages.has(pkg.Name);
|
||||||
|
checkbox.addEventListener("change", () => {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
selectedPackages.add(pkg.Name);
|
||||||
|
} else {
|
||||||
|
selectedPackages.delete(pkg.Name);
|
||||||
|
}
|
||||||
|
renderTable();
|
||||||
|
});
|
||||||
|
selectCell.append(checkbox);
|
||||||
|
row.append(selectCell);
|
||||||
row.append(cell(pkg.Source));
|
row.append(cell(pkg.Source));
|
||||||
row.append(cell(pkg.Name));
|
row.append(cell(pkg.Name));
|
||||||
row.append(cell(pkg.Current || "-"));
|
row.append(cell(pkg.Current || "-"));
|
||||||
@@ -586,6 +668,7 @@ function renderTable() {
|
|||||||
|
|
||||||
updatesTable.hidden = packages.length === 0;
|
updatesTable.hidden = packages.length === 0;
|
||||||
emptyState.hidden = packages.length !== 0;
|
emptyState.hidden = packages.length !== 0;
|
||||||
|
updateSelectionUi(packages);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderIgnored() {
|
function renderIgnored() {
|
||||||
@@ -618,6 +701,8 @@ function cell(value) {
|
|||||||
|
|
||||||
function render(data) {
|
function render(data) {
|
||||||
currentData = data;
|
currentData = data;
|
||||||
|
const availableNames = new Set(data.packages.filter((pkg) => !pkg.Ignored).map((pkg) => pkg.Name));
|
||||||
|
selectedPackages = new Set([...selectedPackages].filter((name) => availableNames.has(name)));
|
||||||
summary.textContent = data.summary;
|
summary.textContent = data.summary;
|
||||||
activeCount.textContent = data.total;
|
activeCount.textContent = data.total;
|
||||||
ignoredCount.textContent = data.ignored_total;
|
ignoredCount.textContent = data.ignored_total;
|
||||||
@@ -645,6 +730,14 @@ function render(data) {
|
|||||||
scheduleAutoRefresh();
|
scheduleAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSelectionUi(packages = visiblePackages()) {
|
||||||
|
const selectedCount = selectedPackages.size;
|
||||||
|
selectionText.textContent = selectedCount === 1 ? "1 Update ausgewaehlt" : selectedCount + " Updates ausgewaehlt";
|
||||||
|
installSelectedBtn.disabled = selectedCount === 0 || currentData?.system.pacman_locked;
|
||||||
|
selectAll.checked = packages.length > 0 && packages.every((pkg) => selectedPackages.has(pkg.Name));
|
||||||
|
selectAll.indeterminate = packages.some((pkg) => selectedPackages.has(pkg.Name)) && !selectAll.checked;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStatus() {
|
async function loadStatus() {
|
||||||
refreshBtn.disabled = true;
|
refreshBtn.disabled = true;
|
||||||
summary.textContent = "Pruefe Updates...";
|
summary.textContent = "Pruefe Updates...";
|
||||||
@@ -677,11 +770,22 @@ async function setIgnored(name, ignored) {
|
|||||||
refreshBtn.addEventListener("click", loadStatus);
|
refreshBtn.addEventListener("click", loadStatus);
|
||||||
searchInput.addEventListener("input", renderTable);
|
searchInput.addEventListener("input", renderTable);
|
||||||
sourceFilter.addEventListener("change", renderTable);
|
sourceFilter.addEventListener("change", renderTable);
|
||||||
|
selectAll.addEventListener("change", () => {
|
||||||
|
const packages = visiblePackages();
|
||||||
|
for (const pkg of packages) {
|
||||||
|
if (selectAll.checked) {
|
||||||
|
selectedPackages.add(pkg.Name);
|
||||||
|
} else {
|
||||||
|
selectedPackages.delete(pkg.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderTable();
|
||||||
|
});
|
||||||
|
|
||||||
installBtn.addEventListener("click", async () => {
|
installBtn.addEventListener("click", async () => {
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await request("/api/install", { method: "POST" });
|
await request("/api/install", { method: "POST", body: "{}" });
|
||||||
summary.textContent = "Installation im Terminal gestartet.";
|
summary.textContent = "Installation im Terminal gestartet.";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
summary.textContent = error.message;
|
summary.textContent = error.message;
|
||||||
@@ -690,6 +794,30 @@ installBtn.addEventListener("click", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
installSelectedBtn.addEventListener("click", async () => {
|
||||||
|
const packages = [...selectedPackages];
|
||||||
|
if (packages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = window.confirm("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.");
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
installSelectedBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
await request("/api/install", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ packages }),
|
||||||
|
});
|
||||||
|
summary.textContent = "Ausgewaehlte Installation im Terminal gestartet.";
|
||||||
|
} catch (error) {
|
||||||
|
summary.textContent = error.message;
|
||||||
|
} finally {
|
||||||
|
installSelectedBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
form.addEventListener("submit", async (event) => {
|
form.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
saveState.textContent = "Speichere...";
|
saveState.textContent = "Speichere...";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -50,6 +51,10 @@ type ignoreRequest struct {
|
|||||||
Ignored bool `json:"ignored"`
|
Ignored bool `json:"ignored"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type installRequest struct {
|
||||||
|
Packages []string `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
func Run(configPath, statePath string, openBrowser bool) error {
|
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 {
|
||||||
@@ -209,7 +214,15 @@ func (s *Server) install(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name, args, err := terminalCommand(cfg)
|
var req installRequest
|
||||||
|
if r.Body != nil {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
writeError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name, args, err := terminalCommand(cfg, req.Packages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err, http.StatusBadRequest)
|
writeError(w, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -242,7 +255,7 @@ func setIgnored(names []string, name string, ignored bool) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func systemStatus(cfg config.Config) SystemStatus {
|
func systemStatus(cfg config.Config) SystemStatus {
|
||||||
terminal, _, _ := terminalCommand(cfg)
|
terminal, _, _ := terminalCommand(cfg, nil)
|
||||||
return SystemStatus{
|
return SystemStatus{
|
||||||
AURHelper: updater.AURHelper(),
|
AURHelper: updater.AURHelper(),
|
||||||
Terminal: terminal,
|
Terminal: terminal,
|
||||||
@@ -298,10 +311,22 @@ func kernelVersion() string {
|
|||||||
return strings.TrimSpace(string(out))
|
return strings.TrimSpace(string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func terminalCommand(cfg config.Config) (string, []string, error) {
|
func terminalCommand(cfg config.Config, packages []string) (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 len(packages) > 0 {
|
||||||
|
selected := shellPackageList(packages)
|
||||||
|
if selected == "" {
|
||||||
|
return "", nil, errors.New("no valid packages selected")
|
||||||
|
}
|
||||||
|
updateCommand = "sudo pacman -S --needed " + selected + "; printf '\\nDone. Press enter to close... '; read _"
|
||||||
|
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
|
||||||
|
updateCommand = helper + " -S --needed " + selected + "; printf '\\nDone. Press enter to close... '; read _"
|
||||||
|
}
|
||||||
|
}
|
||||||
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
|
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
|
||||||
updateCommand = helper + " -Syu; printf '\\nDone. Press enter to close... '; read _"
|
if len(packages) == 0 {
|
||||||
|
updateCommand = helper + " -Syu; printf '\\nDone. Press enter to close... '; read _"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := []string{}
|
candidates := []string{}
|
||||||
@@ -334,6 +359,37 @@ func terminalCommand(cfg config.Config) (string, []string, error) {
|
|||||||
return "", nil, errors.New("no supported terminal found; set one in settings")
|
return "", nil, errors.New("no supported terminal found; set one in settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shellPackageList(packages []string) string {
|
||||||
|
quoted := []string{}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, pkg := range packages {
|
||||||
|
pkg = strings.TrimSpace(pkg)
|
||||||
|
if pkg == "" || seen[pkg] || !validPackageName(pkg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pkg] = true
|
||||||
|
quoted = append(quoted, shellQuote(pkg))
|
||||||
|
}
|
||||||
|
return strings.Join(quoted, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validPackageName(value string) bool {
|
||||||
|
for _, r := range value {
|
||||||
|
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ContainsRune("@._+-", r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func shellQuote(value string) string {
|
||||||
|
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
func openURL(url string) error {
|
func openURL(url string) error {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "linux":
|
case "linux":
|
||||||
|
|||||||
11
internal/gui/server_test.go
Normal file
11
internal/gui/server_test.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestShellPackageList(t *testing.T) {
|
||||||
|
got := shellPackageList([]string{"vapoursynth", "libastal-4-git", "vapoursynth", "bad package"})
|
||||||
|
want := "'vapoursynth' 'libastal-4-git'"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("shellPackageList() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user