Add AGS package manager

This commit is contained in:
Pascal
2026-04-28 04:24:08 +02:00
parent 6eb922c417
commit b4e4081f25
4 changed files with 480 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
notify() {
notify-send "Pakete" "$1" >/dev/null 2>&1 || true
}
if ! command -v ags >/dev/null 2>&1; then
notify "ags ist nicht installiert."
exit 1
fi
cd "$HYPR_DIR"
ags quit --instance package-manager >/dev/null 2>&1 || true
exec ags run "$HYPR_DIR/ags/package-manager.tsx"

View File

@@ -40,6 +40,7 @@ choice="$(
printf '%s\n' \ printf '%s\n' \
"󰑓 Hyprland neu laden" \ "󰑓 Hyprland neu laden" \
"󰌢 Waybar neu starten" \ "󰌢 Waybar neu starten" \
"󰏖 Paket Installation / Updates" \
"󰗽 Bildschirm heller" \ "󰗽 Bildschirm heller" \
"󰗾 Bildschirm dunkler" \ "󰗾 Bildschirm dunkler" \
"󰍃 Session beenden" | "󰍃 Session beenden" |
@@ -53,6 +54,9 @@ case "$choice" in
*"Waybar neu starten"*) *"Waybar neu starten"*)
restart_waybar restart_waybar
;; ;;
*"Paket Installation / Updates"*)
"$SCRIPT_DIR/package-manager.sh"
;;
*"Bildschirm heller"*) *"Bildschirm heller"*)
if command -v brightnessctl >/dev/null 2>&1; then if command -v brightnessctl >/dev/null 2>&1; then
brightnessctl -e4 -n2 set 5%+ brightnessctl -e4 -n2 set 5%+

View File

@@ -0,0 +1,144 @@
* {
all: unset;
font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif;
font-size: 13px;
}
.package-window {
background: transparent;
}
.package-panel {
min-width: 940px;
min-height: 620px;
border: 1px solid rgba(205, 214, 244, 0.16);
border-radius: 16px;
background: rgba(20, 20, 30, 0.97);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
color: #cdd6f4;
padding: 18px;
}
.header {
min-height: 42px;
}
.title {
color: #00ff9c;
font-size: 21px;
font-weight: 800;
}
.subtitle,
.muted,
.package-meta {
color: #a6adc8;
font-size: 12px;
}
entry {
padding: 10px 12px;
border: 1px solid rgba(205, 214, 244, 0.14);
border-radius: 10px;
background: rgba(40, 40, 55, 0.82);
color: #cdd6f4;
}
entry:focus {
border-color: rgba(0, 255, 156, 0.78);
}
.button,
.tool-button,
.icon-button {
padding: 8px 10px;
border-radius: 8px;
background: rgba(40, 40, 55, 0.82);
color: #cdd6f4;
}
.button:hover,
.button:focus,
.tool-button:hover,
.tool-button:focus,
.icon-button:hover,
.icon-button:focus {
background: rgba(0, 255, 156, 0.22);
}
.primary,
.tool-button.active {
background: rgba(0, 255, 156, 0.24);
color: #00ff9c;
}
.close {
color: #f38ba8;
}
.icon-button {
min-width: 34px;
min-height: 34px;
padding: 0;
}
.toolbar {
min-height: 38px;
}
.status-strip {
min-height: 30px;
padding: 7px 10px;
border: 1px solid rgba(205, 214, 244, 0.10);
border-radius: 8px;
background: rgba(40, 40, 55, 0.48);
}
.results-scroll {
min-height: 420px;
}
.package-row {
min-height: 76px;
padding: 10px;
border: 1px solid rgba(205, 214, 244, 0.10);
border-radius: 8px;
background: rgba(40, 40, 55, 0.62);
}
.package-row:hover {
border-color: rgba(0, 255, 156, 0.35);
background: rgba(40, 40, 55, 0.82);
}
.package-name {
color: #cdd6f4;
font-weight: 800;
}
.package-desc {
color: #cdd6f4;
}
.repo-pill,
.installed-pill {
padding: 4px 8px;
border-radius: 999px;
font-size: 11px;
background: rgba(116, 199, 236, 0.16);
color: #89dceb;
}
.installed-pill {
background: rgba(0, 255, 156, 0.16);
color: #00ff9c;
}
.empty {
min-height: 180px;
color: #a6adc8;
}
.error {
color: #f38ba8;
}

View File

@@ -0,0 +1,314 @@
import app from "ags/gtk4/app";
import { Astal, Gtk } from "ags/gtk4";
import { execAsync } from "ags/process";
import css from "./package-manager.css";
const WINDOW_MARGIN_TOP = 48;
const ESC_KEYVAL = 65307;
type Helper = "pacman" | "paru";
type PackageResult = {
repo: string;
name: string;
version: string;
installed: boolean;
description: string;
};
let helper: Helper = "paru";
let query = "";
let results: PackageResult[] = [];
let busy = false;
let statusMessage = "Suchbegriff eingeben und Enter druecken.";
let errorMessage = "";
function shQuote(value: string) {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function notify(message: string) {
execAsync(["notify-send", "Pakete", message]).catch(console.error);
}
function runShell(command: string) {
return execAsync(["bash", "-lc", command]);
}
function setBusy(value: boolean) {
busy = value;
rebuild();
}
function helperAvailable(nextHelper: Helper) {
return runShell(`command -v ${nextHelper} >/dev/null 2>&1`);
}
function parseSearch(output: string) {
const parsed: PackageResult[] = [];
const lines = output.split("\n");
for (let index = 0; index < lines.length; index += 1) {
const header = lines[index];
const match = header.match(/^([^/\s]+)\/([^\s]+)\s+([^\s]+)(?:\s+\[(installed[^\]]*)\])?/);
if (!match) {
continue;
}
const descriptionLines: string[] = [];
let next = index + 1;
while (next < lines.length && /^\s+/.test(lines[next])) {
descriptionLines.push(lines[next].trim());
next += 1;
}
parsed.push({
repo: match[1],
name: match[2],
version: match[3],
installed: Boolean(match[4]),
description: descriptionLines.join(" ") || "Keine Beschreibung.",
});
index = next - 1;
}
return parsed.slice(0, 80);
}
function searchPackages() {
const term = query.trim();
if (!term) {
results = [];
statusMessage = "Suchbegriff fehlt.";
rebuild();
return;
}
setBusy(true);
errorMessage = "";
statusMessage = `Suche mit ${helper} nach "${term}"...`;
helperAvailable(helper)
.then(() => runShell(`${helper} -Ss ${shQuote(term)} 2>/dev/null || true`))
.then(output => {
results = parseSearch(output);
statusMessage = results.length
? `${results.length} Treffer fuer "${term}" mit ${helper}.`
: `Keine Treffer fuer "${term}".`;
})
.catch(error => {
console.error(error);
results = [];
errorMessage = `${helper} ist nicht verfuegbar oder die Suche ist fehlgeschlagen.`;
statusMessage = "Suche fehlgeschlagen.";
})
.finally(() => {
setBusy(false);
});
}
function terminalCommand(command: string) {
const hold = `${command}; printf '\\n'; read -r -p 'Enter zum Schliessen... ' _`;
return `kitty --title ${shQuote("Paketmanager")} sh -lc ${shQuote(hold)} || alacritty -e sh -lc ${shQuote(hold)} || foot sh -lc ${shQuote(hold)} || xterm -e sh -lc ${shQuote(hold)}`;
}
function installPackage(pkg: PackageResult) {
const command = helper === "pacman"
? `sudo pacman -S ${shQuote(pkg.name)}`
: `paru -S ${shQuote(pkg.name)}`;
notify(`Installation gestartet: ${pkg.name}`);
runShell(terminalCommand(command)).catch(error => {
console.error(error);
notify("Kein Terminal fuer Paketinstallation gefunden.");
});
}
function updateSystem() {
const command = helper === "pacman"
? "sudo pacman -Syu"
: "paru -Syu";
notify(`Update gestartet mit ${helper}.`);
runShell(terminalCommand(command)).catch(error => {
console.error(error);
notify("Kein Terminal fuer Updates gefunden.");
});
}
function HelperButton({ id, label }: { id: Helper; label: string }) {
return (
<button
class={helper === id ? "tool-button active" : "tool-button"}
onClicked={() => {
helper = id;
statusMessage = `Aktiver Helper: ${helper}`;
rebuild();
}}
label={label}
/>
);
}
function PackageRow({ pkg }: { pkg: PackageResult }) {
return (
<box class="package-row" spacing={10}>
<box orientation={Gtk.Orientation.VERTICAL} spacing={4} hexpand>
<box spacing={8}>
<label class="package-name" xalign={0} label={pkg.name} />
<label class="repo-pill" label={pkg.repo} />
{pkg.installed ? <label class="installed-pill" label="installed" /> : <box />}
</box>
<label class="package-meta" xalign={0} label={pkg.version} />
<label class="package-desc" xalign={0} wrap label={pkg.description} />
</box>
<button class="button primary" sensitive={!busy} onClicked={() => installPackage(pkg)} label="Installieren" />
</box>
);
}
function PackageManagerWindow() {
return (
<window
name="package-manager"
namespace="package-manager"
class="package-window"
visible
keymode={Astal.Keymode.EXCLUSIVE}
anchor={Astal.WindowAnchor.TOP}
application={app}
>
<Gtk.EventControllerKey onKeyPressed={(_, keyval) => {
if (keyval === ESC_KEYVAL) {
app.quit();
return true;
}
return false;
}} />
<box class="package-panel" orientation={Gtk.Orientation.VERTICAL} spacing={14} marginTop={WINDOW_MARGIN_TOP}>
<box class="header">
<box orientation={Gtk.Orientation.VERTICAL} hexpand>
<label class="title" xalign={0} label="Paket Installation / Updates" />
<label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation im Terminal." />
</box>
<button class="icon-button close" tooltipText="Schliessen" onClicked={() => app.quit()}>
<label label="" />
</button>
</box>
<box class="toolbar" spacing={8}>
<entry
hexpand
placeholderText="Paket suchen, z.B. firefox, docker, obsidian..."
onChanged={entry => {
query = entry.get_text();
}}
onActivate={entry => {
query = entry.get_text();
searchPackages();
}}
/>
<button class="button primary" sensitive={!busy} onClicked={searchPackages} label={busy ? "Suche..." : "Suchen"} />
</box>
<box class="toolbar" spacing={8}>
<HelperButton id="paru" label="paru" />
<HelperButton id="pacman" label="pacman" />
<button class="button" sensitive={!busy} onClicked={updateSystem} label="System updaten" />
</box>
<box class="status-strip">
<label class={errorMessage ? "error" : "muted"} xalign={0} hexpand label={errorMessage || statusMessage} />
</box>
<scrolledwindow
class="results-scroll"
hexpand
vexpand
hscrollbarPolicy={Gtk.PolicyType.NEVER}
vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
>
{results.length
? (
<box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
{results.map(pkg => <PackageRow pkg={pkg} />)}
</box>
)
: (
<box class="empty">
<label hexpand xalign={0.5} label="Noch keine Suchergebnisse." />
</box>
)}
</scrolledwindow>
</box>
</window>
);
}
function rebuild() {
const win = app.get_window("package-manager");
if (!win) {
return;
}
win.set_child(
<box class="package-panel" orientation={Gtk.Orientation.VERTICAL} spacing={14} marginTop={WINDOW_MARGIN_TOP}>
<box class="header">
<box orientation={Gtk.Orientation.VERTICAL} hexpand>
<label class="title" xalign={0} label="Paket Installation / Updates" />
<label class="subtitle" xalign={0} label="Pacman und AUR Suche, Installation im Terminal." />
</box>
<button class="icon-button close" tooltipText="Schliessen" onClicked={() => app.quit()}>
<label label="" />
</button>
</box>
<box class="toolbar" spacing={8}>
<entry
hexpand
text={query}
placeholderText="Paket suchen, z.B. firefox, docker, obsidian..."
onChanged={entry => {
query = entry.get_text();
}}
onActivate={entry => {
query = entry.get_text();
searchPackages();
}}
/>
<button class="button primary" sensitive={!busy} onClicked={searchPackages} label={busy ? "Suche..." : "Suchen"} />
</box>
<box class="toolbar" spacing={8}>
<HelperButton id="paru" label="paru" />
<HelperButton id="pacman" label="pacman" />
<button class="button" sensitive={!busy} onClicked={updateSystem} label="System updaten" />
</box>
<box class="status-strip">
<label class={errorMessage ? "error" : "muted"} xalign={0} hexpand label={errorMessage || statusMessage} />
</box>
<scrolledwindow
class="results-scroll"
hexpand
vexpand
hscrollbarPolicy={Gtk.PolicyType.NEVER}
vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
>
{results.length
? (
<box orientation={Gtk.Orientation.VERTICAL} spacing={8}>
{results.map(pkg => <PackageRow pkg={pkg} />)}
</box>
)
: (
<box class="empty">
<label hexpand xalign={0.5} label="Noch keine Suchergebnisse." />
</box>
)}
</scrolledwindow>
</box> as Gtk.Widget
);
}
app.start({
css,
instanceName: "package-manager",
main() {
PackageManagerWindow();
},
});