diff --git a/BarWidget.qml b/BarWidget.qml new file mode 100644 index 0000000..0d2e474 --- /dev/null +++ b/BarWidget.qml @@ -0,0 +1,69 @@ +import QtQuick +import Quickshell +import qs.Ui + +BarWidget { + id: root + moduleName: "local.omarchy-citizen" + + readonly property bool opened: panelLoader.item + ? panelLoader.item.opened === true + : false + + readonly property bool popoutSwitchClosing: panelLoader.item + ? panelLoader.item.popoutSwitchClosing === true + : false + + function open() { + if (panelLoader.item) panelLoader.item.open() + } + + function close() { + if (panelLoader.item) panelLoader.item.close() + } + + function toggle() { + if (panelLoader.item) panelLoader.item.toggle() + } + + function closeForPopoutSwitch() { + if (panelLoader.item) panelLoader.item.closeForPopoutSwitch() + } + + function injectPanel() { + if (!panelLoader.item) return + panelLoader.item.bar = root.bar + panelLoader.item.anchorItem = button + panelLoader.item.hostWidget = root + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + onBarChanged: injectPanel() + + Loader { + id: panelLoader + active: true + source: Qt.resolvedUrl("Panel.qml") + visible: false + + onLoaded: { + root.injectPanel() + Qt.callLater(root.injectPanel) + } + } + + WidgetButton { + id: button + anchors.fill: parent + bar: root.bar + text: "✦SC" + tooltipText: "Omarchy Citizen – Plug & Play Star Citizen" + onPressed: function(buttonCode) { + if (buttonCode === Qt.LeftButton) root.toggle() + else if (buttonCode === Qt.RightButton && root.bar) + root.bar.run("xdg-open https://wiki.starcitizen-lug.org") + } + } +} diff --git a/GIT_DISTRIBUTION.md b/GIT_DISTRIBUTION.md new file mode 100644 index 0000000..f254f5c --- /dev/null +++ b/GIT_DISTRIBUTION.md @@ -0,0 +1,52 @@ +# Omarchy Citizen als Git-Plugin veröffentlichen + +Für automatische Plugin-Updates sollte Omarchy Citizen als normales öffentliches +Git-Repository veröffentlicht werden. `manifest.json` muss im Repository-Root liegen. + +## Repository-Inhalt + +Den kompletten Inhalt dieses Ordners in das Repository übernehmen, einschließlich: + +```text +manifest.json +BarWidget.qml +Panel.qml +citizenctl +support-sanitize.py +backend/ +``` + +Insbesondere sollte `backend/bin/omarchy-citizen-backend` mit eingecheckt werden, damit +Nutzer keine Go-Toolchain benötigen. + +## Installation beim Nutzer + +Sobald die Repository-URL feststeht: + +```bash +omarchy plugin add https://github.com/DEIN-ACCOUNT/omarchy-citizen.git --enable --yes +``` + +Danach erkennt das Go-Backend `origin` automatisch. Im Plugin kann der Nutzer unter +**Erweiterte Optionen → Plugin-Updates → Auto-Updates einschalten** einmalig zustimmen. + +Ab diesem Zeitpunkt prüft der systemd-User-Timer etwa alle sechs Stunden. Bei einem +Update ruft das Backend Omarchys eigenen nicht-interaktiven Plugin-Updater auf. + +## Release-Ablauf + +Vor jedem Commit/Release: + +```bash +cd backend +./build.sh +cd .. +omarchy plugin validate . +``` + +Dann Versionsnummern in `manifest.json`, `citizenctl`, Go-Backend und Dokumentation +synchron halten, committen und pushen. + +Tags/Releases sind für den Updater nicht zwingend erforderlich: Omarchy aktualisiert den +Git-Checkout per Fast-Forward auf dessen Upstream-Branch. Für nachvollziehbare Versionen +sind Git-Tags trotzdem empfehlenswert. diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..8298023 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,275 @@ +# Omarchy Citizen 0.6.0 – Plug-&-Play-Installation + +## Ziel + +Der Nutzer soll Star Citizen unter Omarchy benutzen können, ohne Linux administrieren zu müssen. + +Im normalen Alltag sind nur drei Dinge wichtig: + +1. **EINRICHTEN & STARTKLAR MACHEN** bzw. **STAR CITIZEN STARTEN** +2. **Problem beheben** +3. **Support-Paket erstellen** + +Wine-Runner, DXVK, einzelne Reparaturschritte, Logs und NGL befinden sich unter +**Erweiterte Optionen**. + +--- + +## 1. Plugin installieren + +### Mausfreundlich + +Entpacke `omarchy-citizen-0.6.0.zip`. + +Im entpackten Ordner liegt: + +`INSTALLIEREN.sh` + +Führe diese Datei über deinen Dateimanager als Programm aus. + +Falls der Dateimanager Skripte nicht direkt ausführt, bleibt der Terminal-Fallback: + +```bash +./INSTALLIEREN.sh +``` + +oder: + +```bash +./install.sh +``` + +Der Plugin-Installer benötigt kein `sudo` und schreibt nur nach: + +```text +~/.config/omarchy/plugins/local.omarchy-citizen/ +``` + +Nach der Installation erscheint **✦SC** in der Omarchy-Leiste. + +--- + +## 2. Erstes Setup + +Klicke: + +**✦SC → EINRICHTEN & STARTKLAR MACHEN** + +Omarchy Citizen übernimmt danach soweit möglich selbst: + +- benötigte Systempakete prüfen/installieren +- `zenity`, `polkit`, `cabextract`, `unzip` +- LUG Helper aus dem AUR installieren +- LUG Preflight Check starten +- Star-Citizen-/RSI-Launcher-Einrichtung starten +- vorhandenen Wine-Prefix erkennen +- Start-Script erkennen/reparieren + +Für Paketinstallationen kann einmal ein sichtbares Omarchy-Terminal erscheinen. +Das ist absichtlich so, damit Passwortabfragen sicher und nachvollziehbar bleiben. +Der Nutzer muss dort keine Paketnamen oder Linux-Befehle kennen. + +--- + +## 3. Normaler Alltag + +Wenn das Panel **FLIGHT READY** zeigt: + +**✦SC → STAR CITIZEN STARTEN** + +Das Plugin verwendet weiterhin das vom LUG Helper verwaltete `sc-launch.sh`. + +--- + +## 4. Problem beheben + +Klicke: + +**✦SC → Problem beheben** + +Der Assistent prüft zuerst automatisch, ob etwas offensichtlich fehlt. + +Wenn das System grundsätzlich eingerichtet ist, fragt er nur nach dem Symptom: + +- Automatisch prüfen +- RSI Launcher startet nicht / ist kaputt +- Star Citizen startet nicht +- Grafik / Performance / Abstürze +- NGL macht Probleme +- Problem unklar + +Dahinter werden die passenden LUG-Helper-Funktionen aufgerufen. + +Der Nutzer muss keine Begriffe wie Wine-Prefix, DXVK oder `vm.max_map_count` kennen. + +--- + +## 5. Permanentes Debug-Log + +Omarchy Citizen protokolliert seine eigenen Aktionen dauerhaft unter: + +```text +~/.local/state/omarchy-citizen/omarchy-citizen.log +``` + +Darin stehen unter anderem: + +- Plugin-Version +- aufgerufene Plugin-Aktionen +- Start-/Reparaturvorgänge +- LUG-Helper-Aufrufe +- Setup-/Update-Abläufe +- Fehlermeldungen des Plugins + +Das lokale Roh-Log kann in **Erweiterte Optionen → Plugin-Debuglog öffnen** +angesehen werden. + +Das Roh-Log wird nicht automatisch hochgeladen oder versendet. Ab ungefähr 4 MiB wird es automatisch gekürzt; die neuesten ungefähr 2 MiB bleiben erhalten. + +--- + +## 6. Support-Paket für Nutzer + +Klicke: + +**✦SC → Support-Paket erstellen** + +Das Plugin erzeugt automatisch eine Datei wie: + +```text +~/Downloads/Omarchy-Citizen-Support-OC-20260831-123456-abcdef.tar.gz +``` + +Zusätzlich wird eine **Support-ID** angezeigt. + +### Standardmäßig enthalten + +- Omarchy Citizen Status +- Plugin-Version +- `omarchy debug --no-sudo --print` +- Kernel/Desktop-Informationen +- GPU/Vulkan-Zusammenfassung +- RAM +- Dateisystemtyp und freier Speicher +- relevante Paketversionen +- Star-Citizen-Kernel-Limits +- Plugin-Validierung +- letzte Omarchy-Citizen-Debugmeldungen +- letzter Omarchy-Update-Logauszug, falls vorhanden +- Star-Citizen-/LUG-Pfadstatus +- begrenzter `sc-launch.log`-Auszug +- bereinigte Kopie des LUG-Start-Scripts +- Go-Updater-Status (Git/ZIP, Auto-Update, Commit-Stand) +- bereinigter Updater-Logauszug + +### Nicht standardmäßig enthalten + +- Browserdaten +- SSH-Schlüssel +- Passwortdateien +- vollständige Star-Citizen-Game.log-Dateien +- vollständige EAC-Logs +- Dateien aus dem RSI-Account/Browserprofil + +### Automatische Maskierung beim Export + +Das Support-Paket versucht folgende Daten zu maskieren: + +- Home-Pfad +- Benutzername +- Hostname +- E-Mail-Adressen +- IPv4-Adressen +- MAC-Adressen +- Bearer-/JWT-Tokens +- typische `token`, `session`, `cookie`, `password`, `secret`-Werte +- typische Auth-Queryparameter in URLs + +Die Maskierung ist eine Best-Effort-Funktion. Nutzer können das Archiv vor dem +Versenden selbst öffnen und prüfen. + +--- + +## 7. Updates + +### Plugin selbst + +0.6.0 enthält ein statisch gebautes Go-Backend. Es erkennt automatisch, ob Omarchy +Citizen als Git-Plugin oder aus einem ZIP installiert wurde. + +Bei einer Git-Installation gibt es unter **Erweiterte Optionen → Plugin-Updates**: + +- **Plugin-Update prüfen** +- **Plugin jetzt aktualisieren** +- **Auto-Updates einschalten/ausschalten** +- **Updater-Log öffnen** + +Beim Aktivieren von Auto-Updates wird das aktuelle Git-`origin` als vertrauenswürdige +Quelle gespeichert. Ein systemd-User-Timer prüft ungefähr alle sechs Stunden mit einer +zufälligen Verzögerung von bis zu 15 Minuten. + +Ein automatisches Update wird verweigert, wenn: + +- das Git-Remote nachträglich geändert wurde +- lokale Änderungen im Plugin-Checkout liegen +- der lokale Branch vom Upstream abgewichen ist +- bereits ein anderes Plugin-Update läuft +- Omarchys Plugin-Validierung fehlschlägt + +Das Backend baut keinen eigenen Plugin-Paketmanager. Die eigentliche Aktualisierung läuft +über Omarchys offiziellen nicht-interaktiven Weg: + +```bash +omarchy plugin update local.omarchy-citizen --yes +``` + +Dadurch bleiben Fast-Forward-Prüfung, Validierung und Rollback bei Omarchy. + +Bei der aktuellen ZIP-Installation zeigt das Backend bewusst **ZIP / nicht Git-verwaltet**. +Es lädt dann nichts aus einer erfundenen oder unbekannten Quelle herunter. Sobald das +Projekt als GitHub-/Git-Repository veröffentlicht und mit `omarchy plugin add` installiert +wird, erkennt das Backend `origin` automatisch. + +### LUG und Omarchy + +Zusätzlich befinden sich unter **Erweiterte Optionen**: + +- LUG Update prüfen +- LUG aktualisieren +- Omarchy aktualisieren + +Systempakete werden weiter über den normalen `omarchy update`-Weg aktualisiert. +Dadurch bleiben Omarchy-Migrationen und der vorgesehene Update-Ablauf erhalten. + +### Updater-Dateien + +```text +~/.config/omarchy-citizen/updater.json +~/.local/state/omarchy-citizen/updater.log +~/.local/lib/omarchy-citizen/omarchy-citizen-backend +``` + +Der Updater läuft nicht als dauerhafter Webserver. Der systemd-User-Timer startet ihn nur +für die jeweilige Prüfung. + +--- + +## 8. NGL + +NGL bleibt unter **Erweiterte Optionen** und ist deutlich als experimentell markiert. + +Der normale Star-Citizen-Betrieb ist nicht von NGL abhängig. + +--- + +## 9. Supportablauf für dich als Plugin-Betreuer + +Wenn ein Nutzer ein Problem meldet, kannst du ihn bitten: + +1. **✦SC** öffnen +2. **Support-Paket erstellen** klicken +3. dir die erzeugte `.tar.gz` schicken +4. die angezeigte Support-ID nennen + +Damit brauchst du im ersten Supportkontakt normalerweise keine Terminalbefehle +vom Nutzer anzufordern. diff --git a/INSTALLIEREN.sh b/INSTALLIEREN.sh new file mode 100644 index 0000000..84ef328 --- /dev/null +++ b/INSTALLIEREN.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +PLUGIN_ID="local.omarchy-citizen" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DEST="$HOME/.config/omarchy/plugins/$PLUGIN_ID" + +message() { + local title="$1" + local body="$2" + if command -v zenity >/dev/null 2>&1; then + zenity --info --width=500 --title="$title" --text="$body" >/dev/null 2>&1 || true + elif command -v omarchy-notification-send >/dev/null 2>&1; then + omarchy-notification-send "$title" "$body" >/dev/null 2>&1 || true + elif command -v notify-send >/dev/null 2>&1; then + notify-send "$title" "$body" >/dev/null 2>&1 || true + fi +} + +if ! command -v omarchy >/dev/null 2>&1; then + message "Omarchy Citizen" "Omarchy wurde nicht gefunden. Dieses Plugin benötigt Omarchy Quattro." + exit 1 +fi + +mkdir -p "$DEST" + +for file in manifest.json BarWidget.qml Panel.qml citizenctl support-sanitize.py README.md INSTALLATION.md LICENSE uninstall.sh install.sh INSTALLIEREN.sh; do + src="$SCRIPT_DIR/$file" + dst="$DEST/$file" + if [[ "$(readlink -f "$src")" != "$(readlink -f "$dst" 2>/dev/null || printf '%s' "$dst")" ]]; then + cp -f -- "$src" "$dst" + fi +done + +if [[ "$(readlink -f "$SCRIPT_DIR/backend")" != "$(readlink -f "$DEST/backend" 2>/dev/null || printf '%s' "$DEST/backend")" ]]; then + rm -rf -- "$DEST/backend" + cp -a -- "$SCRIPT_DIR/backend" "$DEST/backend" +fi + +chmod +x "$DEST/citizenctl" "$DEST/support-sanitize.py" "$DEST/install.sh" "$DEST/uninstall.sh" "$DEST/INSTALLIEREN.sh" "$DEST/backend/bin/omarchy-citizen-backend" + +if ! omarchy plugin validate "$DEST" >/tmp/omarchy-citizen-plugin-validation.log 2>&1; then + message "Omarchy Citizen" "Das Plugin konnte nicht validiert werden. Details stehen in /tmp/omarchy-citizen-plugin-validation.log" + exit 1 +fi + +omarchy-shell shell rescanPlugins >/dev/null 2>&1 || true +"$DEST/backend/bin/omarchy-citizen-backend" self-sync >/dev/null 2>&1 || true +"$HOME/.local/lib/omarchy-citizen/omarchy-citizen-backend" install-service >/dev/null 2>&1 || true + +omarchy plugin enable "$PLUGIN_ID" >/dev/null 2>&1 || true +omarchy bar move "$PLUGIN_ID" --section right >/dev/null 2>&1 || true + +message "Omarchy Citizen" "Installation abgeschlossen. + +Oben in der Omarchy-Leiste findest du jetzt ✦SC. + +Klicke darauf und anschließend auf „EINRICHTEN & STARTKLAR MACHEN“." diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c553c5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Omarchy Citizen contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Panel.qml b/Panel.qml new file mode 100644 index 0000000..72d0a73 --- /dev/null +++ b/Panel.qml @@ -0,0 +1,799 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Ui + +Panel { + id: root + moduleName: "local.omarchy-citizen" + manageIpc: false + + property var anchorItem: null + property var hostWidget: null + + property string pluginVersion: "0.6.0" + property string health: "checking" + property string depsState: "checking" + property string depsMissing: "" + property string helperState: "checking" + property string helperManaged: "missing" + property string helperVersion: "" + property string updateState: "unknown" + property string updateVersion: "" + property string prefixState: "checking" + property string launcherState: "checking" + property string gameState: "checking" + property bool checkingUpdate: false + property bool advancedVisible: false + property string backendState: "missing" + property string backendVersion: "" + property string pluginManaged: "manual" + property string pluginAuto: "false" + property string pluginUpdate: "unavailable" + property string pluginLocalCommit: "" + property string pluginRemoteCommit: "" + + readonly property color accent: "#56d2ff" + readonly property color accentSoft: "#1c3444" + readonly property color surface: "#0d1822" + readonly property color surfaceRaised: "#122230" + readonly property color border: "#2b485c" + readonly property color success: "#71e7ad" + readonly property color warning: "#ffd276" + readonly property color danger: "#ff8fa1" + readonly property color muted: "#a8bbc9" + + readonly property string controlScript: decodeURIComponent( + Qt.resolvedUrl("citizenctl").toString().replace(/^file:\/\//, "") + ) + + readonly property bool canLaunch: health === "ready" + + function open() { root.controller.show() } + function close() { root.controller.hide() } + + function switchPanel(direction) { + if (root.bar && typeof root.bar.switchPanelFrom === "function") + return root.bar.switchPanelFrom(root.hostWidget || root, direction) + return false + } + + function runAction(action, closePanel) { + if (!root.bar) return + root.bar.run( + "bash " + root.bar.shellQuote(root.controlScript) + " " + root.bar.shellQuote(action) + ) + if (closePanel !== false) root.close() + } + + function refreshStatus() { + if (!statusProc.running) statusProc.running = true + } + + function checkForUpdates() { + if (updateProc.running) return + root.checkingUpdate = true + root.updateState = "checking" + updateProc.running = true + } + + function applyStatus(raw) { + var values = {} + var lines = String(raw || "").trim().split("\n") + for (var i = 0; i < lines.length; i++) { + var p = lines[i].indexOf("=") + if (p <= 0) continue + values[lines[i].slice(0, p)] = lines[i].slice(p + 1) + } + + pluginVersion = values.plugin_version || "0.5.0" + health = values.health || "setup" + depsState = values.deps || "missing" + depsMissing = values.deps_missing || "" + helperState = values.helper || "missing" + helperManaged = values.helper_managed || "missing" + helperVersion = values.helper_version || "" + updateState = values.update || "unknown" + updateVersion = values.update_version || "" + prefixState = values.prefix || "missing" + launcherState = values.launcher || "missing" + gameState = values.game || "missing" + backendState = values.backend || "missing" + backendVersion = values.backend_version || "" + pluginManaged = values.plugin_managed || "manual" + pluginAuto = values.plugin_auto || "false" + pluginUpdate = values.plugin_update || "unavailable" + pluginLocalCommit = values.plugin_local_commit || "" + pluginRemoteCommit = values.plugin_remote_commit || "" + } + + function stateIcon(state) { + if (state === "ready" || state === "current") return "✓" + if (state === "available") return "↑" + if (state === "partial" || state === "unknown") return "…" + if (state === "checking") return "·" + return "!" + } + + function stateColor(state) { + if (state === "ready" || state === "current") return success + if (state === "available") return warning + if (state === "partial" || state === "unknown" || state === "checking") return accent + return danger + } + + function healthColor() { + if (health === "ready") return success + if (health === "repair") return warning + if (health === "checking") return accent + return accent + } + + function headline() { + if (health === "ready") return "FLIGHT READY" + if (health === "repair") return "REPAIR AVAILABLE" + if (health === "checking") return "SYSTEM CHECK" + return "READY FOR SETUP" + } + + function subline() { + if (health === "ready") + return "Alles Wesentliche wurde gefunden. Star Citizen ist einen Klick entfernt." + if (health === "repair") + return "Omarchy Citizen hat eine reparierbare Lücke erkannt." + if (health === "install-game") + return "Die Linux-Basis steht. Der RSI Launcher führt dich durch die Spielinstallation." + if (health === "install") + return "Die Linux-Basis steht. Der grafische Star-Citizen-Assistent übernimmt den Rest." + return "Omarchy Citizen richtet Abhängigkeiten, LUG Helper und Star-Citizen-Basis geführt ein." + } + + function primaryText() { + if (health === "ready") return "▶ STAR CITIZEN STARTEN" + if (health === "repair") return "↻ ERKANNTES PROBLEM BEHEBEN" + return "▶ EINRICHTEN & STARTKLAR MACHEN" + } + + function updateText() { + if (checkingUpdate || updateState === "checking") return "wird geprüft" + if (helperManaged === "external") return "extern" + if (helperState !== "ready") return "nach Setup" + if (updateState === "available") + return updateVersion !== "" ? "Update " + updateVersion : "Update verfügbar" + if (updateState === "current") return "aktuell" + return "noch nicht geprüft" + } + + onOpenedChanged: if (opened) refreshStatus() + + Process { + id: statusProc + command: ["bash", root.controlScript, "status"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.applyStatus(text) + } + } + + Process { + id: updateProc + command: ["bash", root.controlScript, "check-update"] + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + root.checkingUpdate = false + root.refreshStatus() + } + } + } + + Timer { + interval: 10000 + repeat: true + running: root.opened + onTriggered: root.refreshStatus() + } + + KeyboardPanel { + id: panel + anchorItem: root.anchorItem + owner: root.hostWidget || root + bar: root.bar + open: root.opened + focusTarget: keyCatcher + contentWidth: panel.fittedContentWidth(Style.space(450)) + contentHeight: panel.fittedContentHeight(content.implicitHeight) + + PanelKeyCatcher { + id: keyCatcher + anchors.fill: parent + onCloseRequested: root.close() + onTabRequested: function(direction) { root.switchPanel(direction) } + + Column { + id: content + width: parent.width + spacing: Style.space(10) + + Rectangle { + width: parent.width + radius: 18 + color: root.surfaceRaised + border.color: root.healthColor() + border.width: 1 + implicitHeight: heroContent.implicitHeight + Style.space(24) + + Column { + id: heroContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.space(16) + spacing: Style.space(10) + + Row { + width: parent.width + spacing: Style.space(10) + + Rectangle { + width: Style.space(34) + height: Style.space(34) + radius: 10 + color: root.accentSoft + border.color: root.healthColor() + border.width: 1 + + Text { + anchors.centerIn: parent + text: "✦" + color: root.healthColor() + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.title + font.bold: true + } + } + + Column { + spacing: Style.space(2) + + Text { + text: "OMARCHY CITIZEN" + color: root.barForeground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.title + font.bold: true + } + + Text { + text: root.headline() + color: root.healthColor() + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + font.bold: true + } + } + } + + Text { + width: parent.width + text: root.subline() + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Button { + width: parent.width + text: root.primaryText() + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + active: root.health === "ready" + onClicked: root.runAction(root.health === "repair" ? "repair" : "primary") + } + } + } + + Rectangle { + width: parent.width + radius: 16 + color: root.surface + border.color: root.border + border.width: 1 + implicitHeight: simpleActions.implicitHeight + Style.space(20) + + Column { + id: simpleActions + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.space(14) + spacing: Style.space(9) + + Text { + text: "HILFE OHNE LINUX-WISSEN" + color: root.accent + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + font.bold: true + } + + Grid { + id: helpGrid + width: parent.width + columns: 2 + columnSpacing: Style.space(7) + rowSpacing: Style.space(7) + readonly property real cellWidth: (width - columnSpacing) / 2 + + Button { + width: helpGrid.cellWidth + text: "↻ Problem beheben" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("repair") + } + + Button { + width: helpGrid.cellWidth + text: "▣ Support-Paket erstellen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("support") + } + } + + Text { + width: parent.width + text: "Das Support-Paket sammelt Diagnoseinformationen automatisch und maskiert typische persönliche Daten vor dem Export." + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + } + + Rectangle { + width: parent.width + radius: 16 + color: root.surface + border.color: root.border + border.width: 1 + implicitHeight: quickStatus.implicitHeight + Style.space(20) + + Column { + id: quickStatus + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.space(14) + spacing: Style.space(8) + + Row { + width: parent.width + spacing: Style.space(8) + + StatusChip { + label: "LUG" + value: root.helperState === "ready" ? "OK" : "Setup" + chipColor: root.stateColor(root.helperState) + } + + StatusChip { + label: "Launcher" + value: root.launcherState === "ready" ? "OK" : "Setup" + chipColor: root.stateColor(root.launcherState) + } + + StatusChip { + label: "Game" + value: root.gameState === "ready" ? "LIVE" : "Setup" + chipColor: root.stateColor(root.gameState) + } + } + + Text { + width: parent.width + text: "Version " + root.pluginVersion + " · LUG " + + (root.helperVersion !== "" ? root.helperVersion : "nicht eingerichtet") + + " · Plugin: " + (root.pluginManaged === "git" ? (root.pluginAuto === "true" ? "Auto" : "Git") : "ZIP") + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + } + + Button { + width: parent.width + text: root.advancedVisible ? "▲ Erweiterte Optionen ausblenden" : "▼ Erweiterte Optionen anzeigen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.advancedVisible = !root.advancedVisible + } + + Rectangle { + visible: root.advancedVisible + width: parent.width + radius: 16 + color: root.surface + border.color: root.border + border.width: 1 + implicitHeight: visible ? advancedContent.implicitHeight + Style.space(20) : 0 + + Column { + id: advancedContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.space(14) + spacing: Style.space(10) + + Text { + text: "ERWEITERTE OPTIONEN" + color: root.accent + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + font.bold: true + } + + StatusLine { + label: "Abhängigkeiten" + state: root.depsState + value: root.depsState === "ready" ? "bereit" : root.depsMissing + } + + StatusLine { + label: "LUG Helper" + state: root.helperState + value: root.helperState === "ready" + ? ((root.helperVersion !== "" ? root.helperVersion : "gefunden") + + (root.helperManaged === "aur" ? " · AUR" : " · extern")) + : "nicht installiert" + } + + StatusLine { + label: "Wine-Prefix" + state: root.prefixState + value: root.prefixState === "ready" ? "gefunden" : "nicht eingerichtet" + } + + StatusLine { + label: "Start-Script" + state: root.launcherState + value: root.launcherState === "ready" ? "bereit" : "fehlt" + } + + PanelSeparator { foreground: root.barForeground } + + Text { + text: "PLUGIN-UPDATES" + color: root.accent + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + font.bold: true + } + + StatusLine { + label: "Update-Modus" + state: root.backendState === "ready" ? "ready" : "missing" + value: root.pluginManaged === "git" + ? (root.pluginAuto === "true" ? "automatisch · alle ~6h" : "Git · manuell") + : "ZIP · Git-Installation für Auto-Updates nötig" + } + + StatusLine { + label: "Plugin-Stand" + state: root.pluginUpdate === "available" ? "available" : + (root.pluginUpdate === "current" ? "current" : "unknown") + value: root.pluginUpdate === "available" + ? "Update verfügbar" + : (root.pluginUpdate === "current" ? "aktuell" : root.pluginUpdate) + } + + Grid { + id: pluginUpdateGrid + width: parent.width + columns: 2 + columnSpacing: Style.space(7) + rowSpacing: Style.space(7) + readonly property real cellWidth: (width - columnSpacing) / 2 + + Button { + width: pluginUpdateGrid.cellWidth + text: "Plugin-Update prüfen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("plugin-update-check") + } + + Button { + width: pluginUpdateGrid.cellWidth + text: "Plugin jetzt aktualisieren" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + active: root.pluginUpdate === "available" + onClicked: root.runAction("plugin-update-now") + } + + Button { + width: pluginUpdateGrid.cellWidth + text: root.pluginAuto === "true" ? "Auto-Updates ausschalten" : "Auto-Updates einschalten" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction(root.pluginAuto === "true" ? "plugin-auto-disable" : "plugin-auto-enable") + } + + Button { + width: pluginUpdateGrid.cellWidth + text: "Updater-Log öffnen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("updater-log") + } + } + + Text { + width: parent.width + text: root.pluginManaged === "git" + ? "Auto-Updates verwenden Omarchys eigenen Plugin-Updater mit Validierung und Rollback. Das beim Aktivieren vorhandene Git-Remote wird als vertrauenswürdige Quelle gespeichert." + : "Die aktuelle ZIP-Installation bleibt bewusst ohne unbekannte Update-Quelle. Sobald Omarchy Citizen aus einem Git-Repository installiert wird, erkennt das Backend dessen origin automatisch." + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + PanelSeparator { foreground: root.barForeground } + + Grid { + id: setupGrid + width: parent.width + columns: 2 + columnSpacing: Style.space(7) + rowSpacing: Style.space(7) + readonly property real cellWidth: (width - columnSpacing) / 2 + + Button { + width: setupGrid.cellWidth + text: root.checkingUpdate ? "Update wird geprüft…" : "LUG Update prüfen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.checkForUpdates() + } + + Button { + width: setupGrid.cellWidth + text: "LUG aktualisieren" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("update-helper") + } + + Button { + width: setupGrid.cellWidth + text: "Omarchy aktualisieren" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("system-update") + } + + Button { + width: setupGrid.cellWidth + text: "LUG Helper öffnen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("helper") + } + + Button { + width: setupGrid.cellWidth + text: "Wine-Runner" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("runners") + } + + Button { + width: setupGrid.cellWidth + text: "DXVK verwalten" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("dxvk") + } + + Button { + width: setupGrid.cellWidth + text: "RSI reparieren" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("repair-rsi") + } + + Button { + width: setupGrid.cellWidth + text: "Start-Script reparieren" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("repair-launch") + } + + Button { + width: setupGrid.cellWidth + text: "Start-Log öffnen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("log") + } + + Button { + width: setupGrid.cellWidth + text: "Plugin-Debuglog öffnen" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("debug-log") + } + } + + PanelSeparator { foreground: root.barForeground } + + Text { + text: "NGL · EXPERIMENTELL" + color: root.warning + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + font.bold: true + } + + Grid { + id: nglGrid + width: parent.width + columns: 2 + columnSpacing: Style.space(7) + rowSpacing: Style.space(7) + readonly property real cellWidth: (width - columnSpacing) / 2 + + Button { + width: nglGrid.cellWidth + text: "NGL Download" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("ngl-page") + } + + Button { + width: nglGrid.cellWidth + text: "NGL installieren" + foreground: root.barForeground + fontFamily: root.bar ? root.bar.fontFamily : Style.font.family + bordered: true + onClicked: root.runAction("ngl-install") + } + } + + Text { + width: parent.width + text: "NGL bleibt bewusst optional. Der normale RSI/LUG-Weg funktioniert unabhängig davon." + color: root.warning + opacity: 0.82 + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + } + + Text { + width: parent.width + text: "Ziel: spielen statt Linux administrieren. Für Support genügt normalerweise „Support-Paket erstellen“." + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + } + } + + component StatusChip: Rectangle { + property string label: "" + property string value: "" + property color chipColor: root.accent + + radius: 999 + color: Qt.rgba(chipColor.r, chipColor.g, chipColor.b, 0.12) + border.color: chipColor + border.width: 1 + implicitWidth: chipRow.implicitWidth + Style.space(14) + implicitHeight: chipRow.implicitHeight + Style.space(8) + + Row { + id: chipRow + anchors.centerIn: parent + spacing: Style.space(5) + + Text { + text: parent.label + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + } + + Text { + text: parent.value + color: parent.chipColor + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption + font.bold: true + } + } + } + + component StatusLine: Rectangle { + property string label: "" + property string value: "" + property string state: "unknown" + + width: parent.width + radius: 12 + color: root.surfaceRaised + border.color: root.border + border.width: 1 + implicitHeight: lineRow.implicitHeight + Style.space(10) + + Row { + id: lineRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.space(10) + spacing: Style.space(8) + + Rectangle { + width: 10 + height: 10 + radius: 5 + color: root.stateColor(parent.state) + } + + Text { + width: Style.space(118) + text: parent.label + color: root.muted + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + } + + Text { + width: Math.max(0, parent.width - Style.space(158)) + text: root.stateIcon(parent.state) + " " + parent.value + color: root.barForeground + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.bodySmall + elide: Text.ElideRight + } + } + } +} diff --git a/README.md b/README.md index def2c19..201a09c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# omarchy-sc +# Omarchy Citizen 0.6.0 +A plug-and-play Star Citizen control panel for Omarchy. + +## Design goal + +**Play Star Citizen, don't administer Linux.** + +The default UI deliberately exposes only: + +- Setup / Start +- Guided repair +- Create support bundle + +Advanced Wine/DXVK/LUG/NGL controls remain available behind an advanced section. + +## 0.6.0 highlights + +- one primary action that adapts to current system state +- guided first-time setup +- symptom-based repair assistant +- persistent plugin debug log +- one-click sanitized support bundle +- support ID for remote troubleshooting +- Omarchy's native `omarchy debug --no-sudo --print` included in support bundles +- advanced controls hidden by default +- optional NGL remains isolated from the normal launcher path +- Go-based plugin update backend with Git remote pinning +- optional automatic updates via systemd user timer +- updater status/log included in support bundles + +## Local debug log + +```text +~/.local/state/omarchy-citizen/omarchy-citizen.log +``` + +The log rotates automatically at roughly 4 MiB and keeps the newest ~2 MiB. + +## Support bundle + +One click creates: + +```text +~/Downloads/Omarchy-Citizen-Support-.tar.gz +``` + +The exported data is sanitized on a best-effort basis before packaging. + +See [`INSTALLATION.md`](INSTALLATION.md) for details. + +## License + +MIT. + +## Go update backend + +The bundled static Go binary lives at: + +```text +backend/bin/omarchy-citizen-backend +``` + +For Git-managed Omarchy installations it checks the checkout and delegates actual +updates to Omarchy's native `omarchy plugin update local.omarchy-citizen --yes`. +Automatic updates are opt-in and run through a systemd user timer roughly every six +hours. The Git `origin` present when auto-update is enabled is pinned as the trusted +remote; unattended updates stop if that remote changes or the checkout is dirty. + +See [`backend/README.md`](backend/README.md) and [`GIT_DISTRIBUTION.md`](GIT_DISTRIBUTION.md). diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..fd7c7de --- /dev/null +++ b/backend/README.md @@ -0,0 +1,69 @@ +# Omarchy Citizen Go Update Backend + +`omarchy-citizen-backend` is a small, dependency-free Go helper for plugin updates. +It deliberately does **not** implement its own plugin package manager. For Git-managed +Omarchy plugins it delegates the actual update to: + +```bash +omarchy plugin update local.omarchy-citizen --yes +``` + +This preserves Omarchy's own fast-forward, manifest validation and rollback behavior. + +## Commands + +```text +omarchy-citizen-backend status [--json] +omarchy-citizen-backend check [--json] +omarchy-citizen-backend update +omarchy-citizen-backend tick +omarchy-citizen-backend auto enable +omarchy-citizen-backend auto disable +omarchy-citizen-backend auto status +omarchy-citizen-backend install-service +omarchy-citizen-backend uninstall-service +omarchy-citizen-backend self-sync +omarchy-citizen-backend version +``` + +## Update trust model + +Automatic updates can only be enabled when the plugin directory is a Git checkout. +When the user enables automatic updates, the current `origin` URL is stored as the +trusted remote. Future unattended updates are refused if: + +- the Git remote changes, +- the checkout contains local changes, +- the branch has diverged, +- Omarchy validation fails, +- another update is already running. + +## Scheduling + +`auto enable` creates/enables a systemd **user** timer. It checks approximately every +six hours with a randomized delay. No root service and no localhost HTTP server are used. + +## State + +```text +~/.config/omarchy-citizen/updater.json +~/.local/state/omarchy-citizen/updater.log +~/.local/state/omarchy-citizen/update.lock +~/.local/lib/omarchy-citizen/omarchy-citizen-backend +``` + +The external binary location is intentional: the updater can keep running while the +plugin Git checkout itself is fast-forwarded. After a successful plugin update it copies +the newly bundled backend binary into the external location atomically. + +## Building + +```bash +cd backend +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -trimpath -ldflags='-s -w' \ + -o bin/omarchy-citizen-backend \ + ./cmd/omarchy-citizen-backend +``` + +The repository should commit the resulting binary so users do not need a Go toolchain. diff --git a/backend/bin/omarchy-citizen-backend b/backend/bin/omarchy-citizen-backend new file mode 100644 index 0000000..f79e1c5 Binary files /dev/null and b/backend/bin/omarchy-citizen-backend differ diff --git a/backend/build.sh b/backend/build.sh new file mode 100644 index 0000000..618d36e --- /dev/null +++ b/backend/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +mkdir -p bin +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -trimpath -ldflags='-s -w' \ + -o bin/omarchy-citizen-backend \ + ./cmd/omarchy-citizen-backend +sha256sum bin/omarchy-citizen-backend diff --git a/backend/cmd/omarchy-citizen-backend/main.go b/backend/cmd/omarchy-citizen-backend/main.go new file mode 100644 index 0000000..f8c9b8b --- /dev/null +++ b/backend/cmd/omarchy-citizen-backend/main.go @@ -0,0 +1,663 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" +) + +const ( + appVersion = "0.6.0" + pluginID = "local.omarchy-citizen" +) + +type Config struct { + AutoApply bool `json:"auto_apply"` + TrustedRemote string `json:"trusted_remote,omitempty"` + LastCheck string `json:"last_check,omitempty"` + LastUpdate string `json:"last_update,omitempty"` + LastResult string `json:"last_result,omitempty"` +} + +type Status struct { + BackendVersion string `json:"backend_version"` + PluginID string `json:"plugin_id"` + PluginDir string `json:"plugin_dir"` + Managed string `json:"managed"` + AutoApply bool `json:"auto_apply"` + Remote string `json:"remote,omitempty"` + TrustedRemote string `json:"trusted_remote,omitempty"` + Branch string `json:"branch,omitempty"` + LocalCommit string `json:"local_commit,omitempty"` + RemoteCommit string `json:"remote_commit,omitempty"` + Dirty bool `json:"dirty"` + Update string `json:"update"` + LastCheck string `json:"last_check,omitempty"` + LastUpdate string `json:"last_update,omitempty"` + LastResult string `json:"last_result,omitempty"` +} + +type App struct { + home string + pluginDir string + configDir string + stateDir string + configPath string + logPath string + libDir string + selfPath string +} + +func main() { + app, err := newApp() + if err != nil { + fatal(err) + } + app.rotateLog() + + args := os.Args[1:] + if len(args) == 0 { + args = []string{"status"} + } + + switch args[0] { + case "version", "--version", "-v": + fmt.Println(appVersion) + case "status": + st, _ := app.status(false) + if hasArg(args[1:], "--json") { + printJSON(st) + } else { + printStatusKV(st) + } + case "check": + st, err := app.status(true) + app.recordCheck(st, err) + if hasArg(args[1:], "--json") { + printJSON(st) + } else { + printStatusKV(st) + } + if err != nil { + os.Exit(2) + } + case "update": + if err := app.update(false); err != nil { + app.logf("manual update failed: %v", err) + fatal(err) + } + case "tick": + if err := app.tick(); err != nil { + app.logf("tick failed: %v", err) + os.Exit(2) + } + case "auto": + if len(args) < 2 { + fatal(errors.New("usage: auto enable|disable|status")) + } + switch args[1] { + case "enable": + if err := app.enableAuto(); err != nil { + fatal(err) + } + case "disable": + if err := app.disableAuto(); err != nil { + fatal(err) + } + case "status": + cfg := app.loadConfig() + if cfg.AutoApply { + fmt.Println("enabled") + } else { + fmt.Println("disabled") + } + default: + fatal(errors.New("usage: auto enable|disable|status")) + } + case "install-service": + if err := app.installService(); err != nil { + fatal(err) + } + case "uninstall-service": + if err := app.uninstallService(); err != nil { + fatal(err) + } + case "self-sync": + if err := app.selfSync(); err != nil { + fatal(err) + } + default: + fatal(fmt.Errorf("unknown command: %s", args[0])) + } +} + +func newApp() (*App, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + self, _ := os.Executable() + self, _ = filepath.EvalSymlinks(self) + configHome := envOr("XDG_CONFIG_HOME", filepath.Join(home, ".config")) + stateHome := envOr("XDG_STATE_HOME", filepath.Join(home, ".local", "state")) + return &App{ + home: home, + pluginDir: filepath.Join(configHome, "omarchy", "plugins", pluginID), + configDir: filepath.Join(configHome, "omarchy-citizen"), + stateDir: filepath.Join(stateHome, "omarchy-citizen"), + configPath: filepath.Join(configHome, "omarchy-citizen", "updater.json"), + logPath: filepath.Join(stateHome, "omarchy-citizen", "updater.log"), + libDir: filepath.Join(home, ".local", "lib", "omarchy-citizen"), + selfPath: self, + }, nil +} + +func (a *App) status(fetch bool) (Status, error) { + cfg := a.loadConfig() + st := Status{ + BackendVersion: appVersion, + PluginID: pluginID, + PluginDir: a.pluginDir, + Managed: "manual", + AutoApply: cfg.AutoApply, + TrustedRemote: cfg.TrustedRemote, + Update: "unavailable", + LastCheck: cfg.LastCheck, + LastUpdate: cfg.LastUpdate, + LastResult: cfg.LastResult, + } + + if !isGitRepo(a.pluginDir) { + return st, nil + } + st.Managed = "git" + + remote, _ := git(a.pluginDir, "remote", "get-url", "origin") + st.Remote = strings.TrimSpace(remote) + branch, _ := git(a.pluginDir, "branch", "--show-current") + st.Branch = strings.TrimSpace(branch) + local, _ := git(a.pluginDir, "rev-parse", "HEAD") + st.LocalCommit = short(strings.TrimSpace(local)) + dirty, _ := git(a.pluginDir, "status", "--porcelain") + st.Dirty = strings.TrimSpace(dirty) != "" + + if fetch { + if st.Dirty { + st.Update = "blocked-dirty" + return st, errors.New("plugin checkout has local changes") + } + if cfg.TrustedRemote != "" && st.Remote != cfg.TrustedRemote { + st.Update = "blocked-remote-changed" + return st, errors.New("git origin differs from trusted remote") + } + if _, err := git(a.pluginDir, "fetch", "--quiet", "origin"); err != nil { + st.Update = "check-failed" + return st, err + } + } + + upstream, err := git(a.pluginDir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") + if err != nil { + st.Update = "no-upstream" + return st, nil + } + upstream = strings.TrimSpace(upstream) + remoteCommit, err := git(a.pluginDir, "rev-parse", upstream) + if err != nil { + st.Update = "unknown" + return st, err + } + remoteCommit = strings.TrimSpace(remoteCommit) + st.RemoteCommit = short(remoteCommit) + localFull, _ := git(a.pluginDir, "rev-parse", "HEAD") + localFull = strings.TrimSpace(localFull) + + switch { + case st.Dirty: + st.Update = "blocked-dirty" + case localFull == remoteCommit: + st.Update = "current" + default: + ancestor, err := runExit(a.pluginDir, "git", "merge-base", "--is-ancestor", localFull, remoteCommit) + if err == nil && ancestor == 0 { + st.Update = "available" + } else { + st.Update = "diverged" + } + } + return st, nil +} + +func (a *App) tick() error { + cfg := a.loadConfig() + if !cfg.AutoApply { + a.logf("tick: auto updates disabled") + return nil + } + st, err := a.status(true) + a.recordCheck(st, err) + if err != nil { + return err + } + if st.Update != "available" { + a.logf("tick: no update applied; state=%s", st.Update) + return nil + } + return a.update(true) +} + +func (a *App) update(automatic bool) error { + unlock, err := a.acquireUpdateLock() + if err != nil { + return err + } + defer unlock() + + st, err := a.status(true) + a.recordCheck(st, err) + if err != nil { + return err + } + if st.Managed != "git" { + return errors.New("automatic updates require a git-managed Omarchy plugin installation") + } + cfg := a.loadConfig() + if cfg.TrustedRemote != "" && st.Remote != cfg.TrustedRemote { + return errors.New("refusing update: git origin changed after auto-update trust was granted") + } + if st.Dirty { + return errors.New("refusing update: plugin contains local changes") + } + if st.Update == "current" { + a.logf("update: already current at %s", st.LocalCommit) + return nil + } + if st.Update != "available" { + return fmt.Errorf("update cannot be applied in state %q", st.Update) + } + + before := st.LocalCommit + a.logf("update begin automatic=%v remote=%q from=%s to=%s", automatic, st.Remote, st.LocalCommit, st.RemoteCommit) + + cmd := exec.Command("omarchy", "plugin", "update", pluginID, "--yes") + cmd.Stdout = io.MultiWriter(os.Stdout, a.logWriter()) + cmd.Stderr = io.MultiWriter(os.Stderr, a.logWriter()) + if err := cmd.Run(); err != nil { + a.updateConfig(func(c *Config) { + c.LastResult = "update-failed" + }) + return fmt.Errorf("omarchy plugin update failed: %w", err) + } + + if err := a.validatePlugin(); err != nil { + a.updateConfig(func(c *Config) { + c.LastResult = "post-validation-failed" + }) + return fmt.Errorf("post-update validation failed: %w", err) + } + + after, _ := git(a.pluginDir, "rev-parse", "HEAD") + after = short(strings.TrimSpace(after)) + a.updateConfig(func(c *Config) { + c.LastUpdate = time.Now().Format(time.RFC3339) + c.LastResult = "updated:" + before + "->" + after + }) + a.logf("update success from=%s to=%s", before, after) + a.notify("Omarchy Citizen", "Plugin-Update installiert: "+before+" → "+after) + + if err := a.selfSync(); err != nil { + a.logf("backend self-sync warning: %v", err) + } + _ = run("", "omarchy-shell", "shell", "rescanPlugins") + return nil +} + +func (a *App) enableAuto() error { + st, err := a.status(true) + if err != nil { + return err + } + if st.Managed != "git" || st.Remote == "" { + return errors.New("auto updates can only be enabled after installing Omarchy Citizen as a git-managed Omarchy plugin") + } + if st.Dirty { + return errors.New("cannot enable auto updates while the plugin checkout has local changes") + } + if err := a.validatePlugin(); err != nil { + return err + } + if err := a.installService(); err != nil { + return err + } + a.updateConfig(func(c *Config) { + c.AutoApply = true + c.TrustedRemote = st.Remote + c.LastResult = "auto-enabled" + }) + if err := run("", "systemctl", "--user", "enable", "--now", "omarchy-citizen-update.timer"); err != nil { + return err + } + a.logf("auto updates enabled trusted_remote=%q", st.Remote) + fmt.Println("enabled") + return nil +} + +func (a *App) disableAuto() error { + a.updateConfig(func(c *Config) { + c.AutoApply = false + c.LastResult = "auto-disabled" + }) + _ = run("", "systemctl", "--user", "disable", "--now", "omarchy-citizen-update.timer") + a.logf("auto updates disabled") + fmt.Println("disabled") + return nil +} + +func (a *App) installService() error { + if err := os.MkdirAll(filepath.Join(a.home, ".config", "systemd", "user"), 0o755); err != nil { + return err + } + if err := os.MkdirAll(a.libDir, 0o755); err != nil { + return err + } + if err := a.selfSync(); err != nil { + return err + } + + servicePath := filepath.Join(a.home, ".config", "systemd", "user", "omarchy-citizen-update.service") + timerPath := filepath.Join(a.home, ".config", "systemd", "user", "omarchy-citizen-update.timer") + backendPath := filepath.Join(a.libDir, "omarchy-citizen-backend") + + service := `[Unit]\nDescription=Omarchy Citizen automatic plugin updater\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=` + backendPath + ` tick\n` + timer := `[Unit]\nDescription=Check Omarchy Citizen updates periodically\n\n[Timer]\nOnBootSec=10min\nOnUnitActiveSec=6h\nRandomizedDelaySec=15min\nPersistent=true\nUnit=omarchy-citizen-update.service\n\n[Install]\nWantedBy=timers.target\n` + + if err := os.WriteFile(servicePath, []byte(strings.ReplaceAll(service, `\n`, "\n")), 0o644); err != nil { + return err + } + if err := os.WriteFile(timerPath, []byte(strings.ReplaceAll(timer, `\n`, "\n")), 0o644); err != nil { + return err + } + return run("", "systemctl", "--user", "daemon-reload") +} + +func (a *App) uninstallService() error { + _ = run("", "systemctl", "--user", "disable", "--now", "omarchy-citizen-update.timer") + _ = os.Remove(filepath.Join(a.home, ".config", "systemd", "user", "omarchy-citizen-update.service")) + _ = os.Remove(filepath.Join(a.home, ".config", "systemd", "user", "omarchy-citizen-update.timer")) + _ = run("", "systemctl", "--user", "daemon-reload") + return nil +} + +func (a *App) selfSync() error { + bundled := filepath.Join(a.pluginDir, "backend", "bin", "omarchy-citizen-backend") + if _, err := os.Stat(bundled); err != nil { + // During development or before first git install, copy current executable instead. + if a.selfPath == "" { + return fmt.Errorf("bundled backend not found: %s", bundled) + } + bundled = a.selfPath + } + if err := os.MkdirAll(a.libDir, 0o755); err != nil { + return err + } + target := filepath.Join(a.libDir, "omarchy-citizen-backend") + same, _ := sameFileHash(bundled, target) + if same { + return nil + } + tmp := target + ".new" + if err := copyFile(bundled, tmp, 0o755); err != nil { + return err + } + if err := os.Rename(tmp, target); err != nil { + return err + } + a.logf("backend self-sync installed %s", target) + return nil +} + +func (a *App) acquireUpdateLock() (func(), error) { + if err := os.MkdirAll(a.stateDir, 0o755); err != nil { + return nil, err + } + path := filepath.Join(a.stateDir, "update.lock") + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, err + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { + f.Close() + return nil, errors.New("another Omarchy Citizen update is already running") + } + return func() { + _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + _ = f.Close() + }, nil +} + +func (a *App) notify(title, body string) { + if _, err := exec.LookPath("omarchy-notification-send"); err == nil { + _ = exec.Command("omarchy-notification-send", title, body).Run() + return + } + if _, err := exec.LookPath("notify-send"); err == nil { + _ = exec.Command("notify-send", title, body).Run() + } +} + +func (a *App) validatePlugin() error { + cmd := exec.Command("omarchy", "plugin", "validate", a.pluginDir) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("plugin validation failed: %s", strings.TrimSpace(string(out))) + } + return nil +} + +func (a *App) recordCheck(st Status, err error) { + a.updateConfig(func(c *Config) { + c.LastCheck = time.Now().Format(time.RFC3339) + if err != nil { + c.LastResult = "check-failed:" + err.Error() + } else { + c.LastResult = "check:" + st.Update + } + }) + a.logf("check managed=%s update=%s local=%s remote=%s dirty=%v err=%v", st.Managed, st.Update, st.LocalCommit, st.RemoteCommit, st.Dirty, err) +} + +func (a *App) loadConfig() Config { + var cfg Config + data, err := os.ReadFile(a.configPath) + if err == nil { + _ = json.Unmarshal(data, &cfg) + } + return cfg +} + +func (a *App) updateConfig(fn func(*Config)) { + cfg := a.loadConfig() + fn(&cfg) + _ = os.MkdirAll(a.configDir, 0o755) + data, _ := json.MarshalIndent(cfg, "", " ") + data = append(data, '\n') + tmp := a.configPath + ".tmp" + if os.WriteFile(tmp, data, 0o600) == nil { + _ = os.Rename(tmp, a.configPath) + } +} + +func (a *App) rotateLog() { + _ = os.MkdirAll(a.stateDir, 0o755) + info, err := os.Stat(a.logPath) + if err != nil || info.Size() <= 4*1024*1024 { + return + } + f, err := os.Open(a.logPath) + if err != nil { + return + } + defer f.Close() + keep := int64(2 * 1024 * 1024) + if _, err := f.Seek(-keep, io.SeekEnd); err != nil { + return + } + data, _ := io.ReadAll(f) + _ = os.WriteFile(a.logPath, data, 0o600) +} + +func (a *App) logWriter() io.Writer { + _ = os.MkdirAll(a.stateDir, 0o755) + f, err := os.OpenFile(a.logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + return io.Discard + } + return &autoCloseWriter{f: f} +} + +func (a *App) logf(format string, args ...any) { + _ = os.MkdirAll(a.stateDir, 0o755) + f, err := os.OpenFile(a.logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) + if err != nil { + return + } + defer f.Close() + fmt.Fprintf(f, "%s version=%s %s\n", time.Now().Format(time.RFC3339), appVersion, fmt.Sprintf(format, args...)) +} + +type autoCloseWriter struct{ f *os.File } + +func (w *autoCloseWriter) Write(p []byte) (int, error) { return w.f.Write(p) } + +func git(dir string, args ...string) (string, error) { + cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), fmt.Errorf("git %s: %s", strings.Join(args, " "), strings.TrimSpace(string(out))) + } + return string(out), nil +} + +func run(dir, name string, args ...string) error { + cmd := exec.Command(name, args...) + if dir != "" { + cmd.Dir = dir + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func runExit(dir, name string, args ...string) (int, error) { + cmd := exec.Command(name, args...) + if dir != "" { + cmd.Dir = dir + } + err := cmd.Run() + if err == nil { + return 0, nil + } + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return exitErr.ExitCode(), nil + } + return -1, err +} + +func isGitRepo(dir string) bool { + out, err := git(dir, "rev-parse", "--is-inside-work-tree") + return err == nil && strings.TrimSpace(out) == "true" +} + +func copyFile(src, dst string, mode os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) + if err != nil { + return err + } + if _, err := io.Copy(out, in); err != nil { + out.Close() + return err + } + if err := out.Sync(); err != nil { + out.Close() + return err + } + return out.Close() +} + +func sameFileHash(a, b string) (bool, error) { + ha, err := fileHash(a) + if err != nil { + return false, err + } + hb, err := fileHash(b) + if err != nil { + return false, err + } + return ha == hb, nil +} + +func fileHash(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +func envOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} +func short(s string) string { + if len(s) > 12 { + return s[:12] + } + return s +} +func hasArg(args []string, needle string) bool { + for _, a := range args { + if a == needle { + return true + } + } + return false +} +func printJSON(v any) { enc := json.NewEncoder(os.Stdout); enc.SetIndent("", " "); _ = enc.Encode(v) } +func printStatusKV(s Status) { + fmt.Printf("backend_version=%s\n", s.BackendVersion) + fmt.Printf("managed=%s\n", s.Managed) + fmt.Printf("auto_apply=%t\n", s.AutoApply) + fmt.Printf("update=%s\n", s.Update) + fmt.Printf("remote=%s\n", s.Remote) + fmt.Printf("trusted_remote=%s\n", s.TrustedRemote) + fmt.Printf("branch=%s\n", s.Branch) + fmt.Printf("local_commit=%s\n", s.LocalCommit) + fmt.Printf("remote_commit=%s\n", s.RemoteCommit) + fmt.Printf("dirty=%t\n", s.Dirty) + fmt.Printf("last_check=%s\n", s.LastCheck) + fmt.Printf("last_update=%s\n", s.LastUpdate) + fmt.Printf("last_result=%s\n", s.LastResult) +} +func fatal(err error) { fmt.Fprintln(os.Stderr, "omarchy-citizen-backend:", err); os.Exit(1) } diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..1e21382 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,3 @@ +module omarchy-citizen/backend + +go 1.23 diff --git a/backend/integration-test.sh b/backend/integration-test.sh new file mode 100644 index 0000000..d3f462d --- /dev/null +++ b/backend/integration-test.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd -- "$(dirname -- "$0")" && pwd)" +BACK="$ROOT/bin/omarchy-citizen-backend" +TEST="$(mktemp -d -t omarchy-citizen-updater.XXXXXX)" +trap 'rm -rf "$TEST"' EXIT + +mkdir -p "$TEST/home/.config/omarchy/plugins" "$TEST/bin" +export HOME="$TEST/home" +export XDG_CONFIG_HOME="$HOME/.config" +export XDG_STATE_HOME="$HOME/.local/state" +export PATH="$TEST/bin:/usr/bin:/bin" + +printf '#!/usr/bin/env bash\nexit 0\n' > "$TEST/bin/systemctl" +printf '#!/usr/bin/env bash\nexit 0\n' > "$TEST/bin/omarchy-shell" +chmod +x "$TEST/bin/systemctl" "$TEST/bin/omarchy-shell" + +git init --bare -q "$TEST/origin.git" +git clone -q "$TEST/origin.git" "$TEST/publisher" +git -C "$TEST/publisher" config user.email test@example.com +git -C "$TEST/publisher" config user.name Test +mkdir -p "$TEST/publisher/backend/bin" +cp "$BACK" "$TEST/publisher/backend/bin/" +printf '%s\n' '{"schemaVersion":1,"id":"local.omarchy-citizen","name":"Test","version":"0.6.0","kinds":["bar-widget"],"entryPoints":{"barWidget":"BarWidget.qml"},"barWidget":{"displayName":"Test","category":"Gaming","allowMultiple":false}}' > "$TEST/publisher/manifest.json" +printf '%s\n' 'import QtQuick; Item {}' > "$TEST/publisher/BarWidget.qml" +git -C "$TEST/publisher" add . +git -C "$TEST/publisher" commit -qm v1 +git -C "$TEST/publisher" push -qu origin HEAD:master + +git clone -q "$TEST/origin.git" "$HOME/.config/omarchy/plugins/local.omarchy-citizen" + +cat > "$TEST/bin/omarchy" <<'SCRIPT' +#!/usr/bin/env bash +set -e +PLUGIN="$HOME/.config/omarchy/plugins/local.omarchy-citizen" +if [[ "$1 $2" == "plugin validate" ]]; then + [[ -f "$3/manifest.json" && -f "$3/BarWidget.qml" ]] + exit 0 +fi +if [[ "$1 $2 $3" == "plugin update local.omarchy-citizen" ]]; then + git -C "$PLUGIN" pull --ff-only -q + exit 0 +fi +exit 2 +SCRIPT +chmod +x "$TEST/bin/omarchy" + +[[ "$("$BACK" status | sed -n 's/^update=//p')" == "current" ]] +printf 'v2\n' > "$TEST/publisher/new-file.txt" +git -C "$TEST/publisher" add new-file.txt +git -C "$TEST/publisher" commit -qm v2 +git -C "$TEST/publisher" push -q +[[ "$("$BACK" check | sed -n 's/^update=//p')" == "available" ]] +"$BACK" update +[[ "$("$BACK" status | sed -n 's/^update=//p')" == "current" ]] +"$BACK" auto enable >/dev/null +[[ "$("$BACK" status | sed -n 's/^auto_apply=//p')" == "true" ]] + +git -C "$HOME/.config/omarchy/plugins/local.omarchy-citizen" remote set-url origin "$TEST/changed.git" +set +e +CHECK_OUTPUT="$("$HOME/.local/lib/omarchy-citizen/omarchy-citizen-backend" check 2>&1)" +RC=$? +set -e +[[ "$RC" -ne 0 ]] +grep -q 'update=blocked-remote-changed' <<<"$CHECK_OUTPUT" + +echo 'integration test: PASS' diff --git a/citizenctl b/citizenctl new file mode 100644 index 0000000..f60217d --- /dev/null +++ b/citizenctl @@ -0,0 +1,1025 @@ +#!/usr/bin/env bash +set -u + +PLUGIN_ID="local.omarchy-citizen" +PLUGIN_VERSION="0.6.0" + +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/starcitizen-lug" +DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy-citizen" +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy-citizen" +UPDATE_CACHE="$CACHE_DIR/lug-update-status" +LOG_FILE="$STATE_DIR/omarchy-citizen.log" +WINE_CONF="$CONFIG_DIR/winedir.conf" +GAME_CONF="$CONFIG_DIR/gamedir.conf" +SELF="$(readlink -f "${BASH_SOURCE[0]}")" +PLUGIN_DIR="$(cd -- "$(dirname -- "$SELF")" && pwd)" +SANITIZER="$PLUGIN_DIR/support-sanitize.py" +BACKEND_EXTERNAL="$HOME/.local/lib/omarchy-citizen/omarchy-citizen-backend" +BACKEND_BUNDLED="$PLUGIN_DIR/backend/bin/omarchy-citizen-backend" + +GUI_PACKAGES=(zenity polkit cabextract unzip) + +mkdir -p "$CACHE_DIR" "$STATE_DIR" + +rotate_log() { + local max_bytes=$((4 * 1024 * 1024)) + local keep_bytes=$((2 * 1024 * 1024)) + if [[ -f "$LOG_FILE" ]] && (( $(stat -c '%s' "$LOG_FILE" 2>/dev/null || echo 0) > max_bytes )); then + tail -c "$keep_bytes" "$LOG_FILE" > "$LOG_FILE.tmp" 2>/dev/null || true + mv -f "$LOG_FILE.tmp" "$LOG_FILE" 2>/dev/null || true + fi +} + +rotate_log + +ACTION="${1:-primary}" + +log_line() { + local message="$*" + printf '%s [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$$" "$message" >> "$LOG_FILE" 2>/dev/null || true +} + +log_line "action=$ACTION version=$PLUGIN_VERSION" + + +find_backend() { + if [[ -x "$BACKEND_EXTERNAL" ]]; then + printf '%s\n' "$BACKEND_EXTERNAL" + return 0 + fi + if [[ -x "$BACKEND_BUNDLED" ]]; then + printf '%s\n' "$BACKEND_BUNDLED" + return 0 + fi + return 1 +} + +backend_status() { + local backend + backend="$(find_backend 2>/dev/null)" || return 1 + "$backend" status +} + +plugin_update_check() { + local backend output state + backend="$(find_backend 2>/dev/null)" || { + notify_error "Das Omarchy-Citizen-Update-Backend wurde nicht gefunden. Installiere das Plugin erneut." + return 1 + } + output="$($backend check 2>&1)" || true + printf '%s\n' "$output" >> "$LOG_FILE" + state="$(printf '%s\n' "$output" | sed -n 's/^update=//p' | head -n1)" + case "$state" in + available) notify "Omarchy Citizen" "Ein Plugin-Update ist verfügbar." ;; + current) notify "Omarchy Citizen" "Omarchy Citizen ist aktuell." ;; + unavailable) notify "Omarchy Citizen" "Diese Installation ist noch nicht Git-verwaltet. Automatische Plugin-Updates werden nach der Git-Installation verfügbar." ;; + blocked-dirty) notify_error "Plugin-Update blockiert: Im Plugin-Verzeichnis gibt es lokale Änderungen." ;; + blocked-remote-changed) notify_error "Plugin-Update aus Sicherheitsgründen blockiert: Das Git-Remote hat sich geändert." ;; + *) notify_error "Plugin-Update konnte nicht eindeutig geprüft werden. Das Support-Paket enthält Details." ;; + esac +} + +plugin_update_now() { + local backend + backend="$(find_backend 2>/dev/null)" || { + notify_error "Das Update-Backend wurde nicht gefunden." + return 1 + } + if "$backend" update >>"$LOG_FILE" 2>&1; then + notify "Omarchy Citizen" "Plugin-Update abgeschlossen." + else + notify_error "Plugin-Update fehlgeschlagen oder nicht verfügbar. Erstelle bei Bedarf ein Support-Paket." + fi +} + +plugin_auto_enable() { + local backend + backend="$(find_backend 2>/dev/null)" || { notify_error "Update-Backend fehlt."; return 1; } + if "$backend" auto enable >>"$LOG_FILE" 2>&1; then + notify "Omarchy Citizen" "Automatische Plugin-Updates sind aktiviert." + else + notify_error "Auto-Updates konnten nicht aktiviert werden. Bei ZIP-Installationen ist zuerst eine Git-verwaltete Plugin-Installation nötig." + fi +} + +plugin_auto_disable() { + local backend + backend="$(find_backend 2>/dev/null)" || { notify_error "Update-Backend fehlt."; return 1; } + "$backend" auto disable >>"$LOG_FILE" 2>&1 || true + notify "Omarchy Citizen" "Automatische Plugin-Updates sind deaktiviert." +} + +open_updater_log() { + local log="$STATE_DIR/updater.log" + if [[ ! -f "$log" ]]; then + notify_error "Noch kein Updater-Log vorhanden." + return 1 + fi + xdg-open "$log" >/dev/null 2>&1 & +} + +notify() { + local title="$1" + local message="$2" + if command -v omarchy-notification-send >/dev/null 2>&1; then + omarchy-notification-send "$title" "$message" >/dev/null 2>&1 || true + elif command -v notify-send >/dev/null 2>&1; then + notify-send "$title" "$message" >/dev/null 2>&1 || true + fi +} + +notify_error() { + local message="$1" + log_line "error=$message" + if command -v zenity >/dev/null 2>&1; then + zenity --error --width=500 --title="Omarchy Citizen" --text="$message" >/dev/null 2>&1 & + else + notify "Omarchy Citizen" "$message" + printf 'Omarchy Citizen: %s\n' "$message" >&2 + fi +} + +find_helper() { + local candidate + + if command -v lug-helper >/dev/null 2>&1; then + command -v lug-helper + return 0 + fi + + if command -v lug-helper.sh >/dev/null 2>&1; then + command -v lug-helper.sh + return 0 + fi + + for candidate in \ + "$HOME/.local/bin/lug-helper" \ + "$HOME/.local/bin/lug-helper.sh" \ + "$DATA_DIR/lug-helper/lug-helper.sh" \ + "$HOME/lug-helper/lug-helper.sh" + do + if [[ -f "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + shopt -s nullglob + for candidate in \ + "$HOME/Applications"/*LUG*Helper*.AppImage \ + "$HOME/Applications"/lug-helper*.AppImage \ + "$HOME/Downloads"/*LUG*Helper*.AppImage \ + "$HOME/Downloads"/lug-helper*.AppImage + do + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + shopt -u nullglob + return 0 + fi + done + shopt -u nullglob + + return 1 +} + +run_helper_sync() { + local helper + helper="$(find_helper)" || { + notify_error "Der Star Citizen LUG Helper wurde nicht gefunden." + return 1 + } + + log_line "lug-helper sync args=$*" + if [[ "$helper" == *.sh ]]; then + bash "$helper" "$@" >>"$LOG_FILE" 2>&1 + else + "$helper" "$@" >>"$LOG_FILE" 2>&1 + fi +} + +run_helper_gui() { + local helper + helper="$(find_helper)" || { + notify_error "Der Star Citizen LUG Helper wurde nicht gefunden. + +Wähle im SC-Panel „Einrichten“ – Omarchy Citizen installiert die benötigten Komponenten." + return 1 + } + + log_line "lug-helper gui args=$*" + if [[ "$helper" == *.sh ]]; then + nohup bash "$helper" "$@" >>"$LOG_FILE" 2>&1 & + else + nohup "$helper" "$@" >>"$LOG_FILE" 2>&1 & + fi +} + +read_prefix() { + [[ -f "$WINE_CONF" ]] || return 1 + local prefix + IFS= read -r prefix < "$WINE_CONF" + [[ -n "$prefix" && -d "$prefix" ]] || return 1 + printf '%s\n' "$prefix" +} + +read_game_dir() { + [[ -f "$GAME_CONF" ]] || return 1 + local game_dir + IFS= read -r game_dir < "$GAME_CONF" + [[ -n "$game_dir" ]] || return 1 + printf '%s\n' "$game_dir" +} + +package_installed() { + pacman -Qq "$1" >/dev/null 2>&1 +} + +installed_lug_version() { + pacman -Q lug-helper 2>/dev/null | awk '{print $2}' +} + +read_update_cache() { + local key="$1" + [[ -f "$UPDATE_CACHE" ]] || return 1 + sed -n "s/^${key}=//p" "$UPDATE_CACHE" | head -n 1 +} + +write_update_cache() { + local state="$1" + local version="${2:-}" + mkdir -p "$CACHE_DIR" + { + printf 'state=%s\n' "$state" + printf 'version=%s\n' "$version" + printf 'checked_at=%s\n' "$(date +%s)" + } > "$UPDATE_CACHE" +} + +check_update() { + local installed output rc line target="" + + if ! package_installed lug-helper; then + write_update_cache "not-installed" "" + printf 'not-installed\n' + return 0 + fi + + installed="$(installed_lug_version)" + + if ! command -v yay >/dev/null 2>&1; then + write_update_cache "unknown" "" + printf 'unknown\n' + return 1 + fi + + set +e + output="$(timeout 25s yay -Qua 2>/dev/null)" + rc=$? + set -e + + if (( rc != 0 )); then + write_update_cache "unknown" "" + printf 'unknown\n' + return 1 + fi + + line="$(printf '%s\n' "$output" | awk '$1=="lug-helper"{print; exit}')" + if [[ -n "$line" ]]; then + target="$(printf '%s\n' "$line" | awk '{print $NF}')" + write_update_cache "available" "$target" + printf 'available=%s\n' "$target" + else + write_update_cache "current" "$installed" + printf 'current=%s\n' "$installed" + fi +} + +status() { + local helper="" version="" prefix="" game_dir="" dep missing_deps="" + local helper_state="missing" helper_managed="missing" deps_state="ready" + local prefix_state="missing" launcher_state="missing" game_state="missing" + local update_state="unknown" update_version="" health="setup" + + for dep in "${GUI_PACKAGES[@]}"; do + if ! package_installed "$dep"; then + deps_state="missing" + if [[ -n "$missing_deps" ]]; then missing_deps+=","; fi + missing_deps+="$dep" + fi + done + + if package_installed lug-helper; then + helper_state="ready" + helper_managed="aur" + version="$(installed_lug_version)" + elif helper="$(find_helper 2>/dev/null)"; then + helper_state="ready" + helper_managed="external" + if [[ "$helper" == *.sh ]]; then + version="$(bash "$helper" --version 2>/dev/null | head -n 1 || true)" + else + version="$("$helper" --version 2>/dev/null | head -n 1 || true)" + fi + version="${version//$'\n'/ }" + version="${version//$'\r'/ }" + fi + + update_state="$(read_update_cache state 2>/dev/null || printf 'unknown')" + update_version="$(read_update_cache version 2>/dev/null || true)" + + if prefix="$(read_prefix 2>/dev/null)"; then + prefix_state="ready" + if [[ -f "$prefix/sc-launch.sh" ]]; then + launcher_state="ready" + fi + fi + + if game_dir="$(read_game_dir 2>/dev/null)"; then + if [[ -f "$game_dir/LIVE/Bin64/StarCitizen.exe" ]]; then + game_state="ready" + elif [[ -d "$game_dir" ]]; then + game_state="partial" + fi + fi + + if [[ "$deps_state" != "ready" || "$helper_state" != "ready" ]]; then + health="setup" + elif [[ "$prefix_state" != "ready" ]]; then + health="install" + elif [[ "$launcher_state" != "ready" ]]; then + health="repair" + elif [[ "$game_state" != "ready" ]]; then + health="install-game" + else + health="ready" + fi + + printf 'plugin_version=%s\n' "$PLUGIN_VERSION" + printf 'health=%s\n' "$health" + printf 'deps=%s\n' "$deps_state" + printf 'deps_missing=%s\n' "$missing_deps" + printf 'helper=%s\n' "$helper_state" + printf 'helper_managed=%s\n' "$helper_managed" + printf 'helper_version=%s\n' "$version" + printf 'update=%s\n' "$update_state" + printf 'update_version=%s\n' "$update_version" + printf 'prefix=%s\n' "$prefix_state" + printf 'launcher=%s\n' "$launcher_state" + printf 'game=%s\n' "$game_state" + + local backend_output="" backend_state="missing" + if backend_output="$(backend_status 2>/dev/null)"; then + backend_state="ready" + fi + printf 'backend=%s\n' "$backend_state" + printf 'backend_version=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^backend_version=//p' | head -n1)" + printf 'plugin_managed=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^managed=//p' | head -n1)" + printf 'plugin_auto=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^auto_apply=//p' | head -n1)" + printf 'plugin_update=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^update=//p' | head -n1)" + printf 'plugin_local_commit=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^local_commit=//p' | head -n1)" + printf 'plugin_remote_commit=%s\n' "$(printf '%s\n' "$backend_output" | sed -n 's/^remote_commit=//p' | head -n1)" +} + +launch_terminal_action() { + local action="$1" + + log_line "launch-terminal action=$action" + + if command -v omarchy-launch-floating-terminal-with-presentation >/dev/null 2>&1; then + nohup omarchy-launch-floating-terminal-with-presentation bash "$SELF" "$action" \ + >>"$LOG_FILE" 2>&1 & + return 0 + fi + + if command -v xdg-terminal-exec >/dev/null 2>&1; then + nohup xdg-terminal-exec -e bash "$SELF" "$action" \ + >>"$LOG_FILE" 2>&1 & + return 0 + fi + + notify_error "Es konnte kein Omarchy-Terminal für die Paketinstallation gestartet werden." + return 1 +} + +guided_setup_interactive() { + log_line "guided-setup begin" + + if ! find_helper >/dev/null 2>&1; then + launch_terminal_action install-stack-terminal + return 0 + fi + + run_helper_sync --preflight-check || true + + if ! read_prefix >/dev/null 2>&1; then + run_helper_sync --install || true + elif ! prefix="$(read_prefix 2>/dev/null)" || [[ ! -f "$prefix/sc-launch.sh" ]]; then + run_helper_sync --update-launch-script || true + fi + + prefix="$(read_prefix 2>/dev/null || true)" + if [[ -n "$prefix" && -f "$prefix/sc-launch.sh" ]]; then + game_dir="$(read_game_dir 2>/dev/null || true)" + if [[ -z "$game_dir" || ! -f "$game_dir/LIVE/Bin64/StarCitizen.exe" ]]; then + log_line "guided-setup opening RSI Launcher for game installation" + nohup bash "$prefix/sc-launch.sh" >>"$LOG_FILE" 2>&1 & + fi + fi + + notify "Omarchy Citizen" "Einrichtung abgeschlossen. Falls Star Citizen noch fehlt, ist der RSI Launcher für die Installation geöffnet." + log_line "guided-setup end" +} + +guided_setup() { + if ! package_installed zenity || ! find_helper >/dev/null 2>&1; then + launch_terminal_action install-stack-terminal + return + fi + nohup bash "$SELF" guided-setup-interactive >>"$LOG_FILE" 2>&1 & +} + +install_stack_terminal() { + set -e + exec > >(tee -a "$LOG_FILE") 2>&1 + + echo + echo "==================================================" + echo " Omarchy Citizen – Plug & Play Setup" + echo "==================================================" + echo + echo "Omarchy Citizen richtet die Linux-Gaming-Basis ein." + echo "Du musst keine Paketnamen oder Linux-Befehle kennen." + echo + echo "Installiert/überprüft werden:" + echo " - zenity / polkit" + echo " - cabextract / unzip" + echo " - Star Citizen LUG Helper aus dem AUR" + echo + + omarchy pkg add zenity polkit cabextract unzip + + if package_installed lug-helper; then + echo "LUG Helper ist bereits installiert." + else + echo "Installiere LUG Helper..." + omarchy pkg aur add lug-helper + fi + + rm -f "$UPDATE_CACHE" + + echo + echo "Systempakete sind bereit." + echo "Als Nächstes startet der grafische Star-Citizen-Assistent." + echo + + run_helper_sync --preflight-check || true + + if ! read_prefix >/dev/null 2>&1; then + run_helper_sync --install || true + fi + + echo + echo "Plug-&-Play-Setup beendet." + echo "Du kannst dieses Fenster schließen und ✦SC öffnen." + notify "Omarchy Citizen" "Grundsetup abgeschlossen. Öffne ✦SC und starte Star Citizen." + log_line "install-stack complete" +} + +update_helper_terminal() { + set -e + exec > >(tee -a "$LOG_FILE") 2>&1 + + if ! package_installed lug-helper; then + echo "LUG Helper ist noch nicht als AUR-Paket installiert." + echo "Starte bitte zuerst das normale Omarchy-Citizen-Setup." + exit 1 + fi + + echo "Aktualisiere den LUG Helper..." + omarchy pkg aur add lug-helper + rm -f "$UPDATE_CACHE" + echo "LUG Helper ist aktuell." + installed_lug_version + log_line "lug-helper update complete" +} + +system_update_terminal() { + set -e + log_line "omarchy update begin" + omarchy update + rm -f "$UPDATE_CACHE" + log_line "omarchy update end rc=0" +} + +launch_game() { + local prefix script + prefix="$(read_prefix 2>/dev/null)" || { + guided_setup + return + } + + script="$prefix/sc-launch.sh" + if [[ ! -f "$script" ]]; then + log_line "launch script missing; opening repair" + run_helper_gui --update-launch-script + return + fi + + log_line "launch game via sc-launch.sh" + nohup bash "$script" >>"$LOG_FILE" 2>&1 & +} + +primary_action() { + local st health + st="$("$SELF" status)" + health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)" + + case "$health" in + ready) + launch_game + ;; + setup) + launch_terminal_action install-stack-terminal + ;; + install) + guided_setup + ;; + install-game) + launch_game + ;; + repair) + run_helper_gui --update-launch-script + ;; + *) + guided_setup + ;; + esac +} + +repair_assistant_interactive() { + local st health choice prefix + + st="$("$SELF" status)" + health="$(printf '%s\n' "$st" | sed -n 's/^health=//p' | head -n1)" + log_line "repair health=$health" + + case "$health" in + setup) + launch_terminal_action install-stack-terminal + return + ;; + install) + guided_setup_interactive + return + ;; + repair) + run_helper_sync --update-launch-script || true + notify "Omarchy Citizen" "Start-Script wurde geprüft/repariert. Versuche den Start erneut." + return + ;; + esac + + if ! command -v zenity >/dev/null 2>&1; then + run_helper_gui --preflight-check + return + fi + + choice="$(zenity --list \ + --width=610 --height=420 \ + --title="Omarchy Citizen – Problem beheben" \ + --text="Was funktioniert nicht? Du brauchst keine Linux-Diagnose zu kennen." \ + --column="Problem" \ + "Automatisch prüfen (empfohlen)" \ + "RSI Launcher startet nicht / ist kaputt" \ + "Star Citizen startet nicht" \ + "Grafik / Performance / Abstürze" \ + "NGL macht Probleme" \ + "Problem ist unklar – Support-Paket erstellen" \ + 2>/dev/null)" || return 0 + + log_line "repair choice=$choice" + + case "$choice" in + "Automatisch prüfen (empfohlen)") + run_helper_sync --preflight-check || true + run_helper_sync --update-launch-script || true + notify "Omarchy Citizen" "Automatische Basisreparatur abgeschlossen. Bitte erneut starten." + ;; + "RSI Launcher startet nicht / ist kaputt") + run_helper_sync --update-rsi-launcher || true + run_helper_sync --update-launch-script || true + notify "Omarchy Citizen" "RSI Launcher und Start-Script wurden repariert." + ;; + "Star Citizen startet nicht") + run_helper_sync --preflight-check || true + run_helper_sync --update-launch-script || true + notify "Omarchy Citizen" "Startumgebung wurde geprüft. Falls es weiter nicht startet, erstelle bitte das Support-Paket." + ;; + "Grafik / Performance / Abstürze") + run_helper_sync --preflight-check || true + run_helper_gui --manage-runners + ;; + "NGL macht Probleme") + if zenity --question --width=540 \ + --title="NGL Problem" \ + --ok-label="Vanilla RSI Launcher reparieren" \ + --cancel-label="Abbrechen" \ + --text="Omarchy Citizen kann den offiziellen RSI Launcher über den LUG Helper neu installieren. NGL kann danach optional erneut installiert werden." \ + 2>/dev/null; then + run_helper_sync --update-rsi-launcher || true + run_helper_sync --update-launch-script || true + fi + ;; + "Problem ist unklar – Support-Paket erstellen") + "$SELF" support-build + ;; + esac +} + +repair_assistant() { + nohup bash "$SELF" repair-interactive >>"$LOG_FILE" 2>&1 & +} + +sanitize_to() { + local source="$1" + local target="$2" + + if [[ ! -f "$source" ]]; then + return 0 + fi + + if command -v python3 >/dev/null 2>&1 && [[ -f "$SANITIZER" ]]; then + python3 "$SANITIZER" < "$source" > "$target" + else + sed \ + -e "s#${HOME//\#/\\#}#~#g" \ + -e 's/[A-Za-z0-9._%+-]\+@[A-Za-z0-9.-]\+\.[A-Za-z]\{2,\}//g' \ + "$source" > "$target" + fi +} + +capture_sanitized() { + local target="$1" + shift + local tmp="$CACHE_DIR/capture-$$-$RANDOM.txt" + { + "$@" + } >"$tmp" 2>&1 || true + sanitize_to "$tmp" "$target" + rm -f "$tmp" +} + +support_build() { + local stamp support_id work downloads archive prefix="" game_dir="" + stamp="$(date '+%Y%m%d-%H%M%S')" + support_id="OC-${stamp}-$(printf '%s-%s-%s' "$stamp" "$$" "$RANDOM" | sha256sum | cut -c1-6)" + work="$CACHE_DIR/support-$support_id" + + if command -v xdg-user-dir >/dev/null 2>&1; then + downloads="$(xdg-user-dir DOWNLOAD 2>/dev/null || true)" + else + downloads="" + fi + [[ -n "$downloads" && -d "$downloads" ]] || downloads="$HOME/Downloads" + mkdir -p "$downloads" "$work" + + log_line "support build id=$support_id" + + cat > "$work/README_SUPPORT.txt" < "$work/citizen-status.txt" + + if backend="$(find_backend 2>/dev/null)"; then + capture_sanitized "$work/updater-status.json" "$backend" status --json + fi + if [[ -f "$STATE_DIR/updater.log" ]]; then + tail -n 1200 "$STATE_DIR/updater.log" > "$CACHE_DIR/updater-log-$$.txt" + sanitize_to "$CACHE_DIR/updater-log-$$.txt" "$work/updater.log" + rm -f "$CACHE_DIR/updater-log-$$.txt" + fi + + capture_sanitized "$work/omarchy-debug.txt" omarchy debug --no-sudo --print + + { + echo "=== Kernel / Desktop ===" + uname -rmo 2>/dev/null || true + printf 'XDG_SESSION_TYPE=%s\n' "${XDG_SESSION_TYPE:-unknown}" + printf 'XDG_CURRENT_DESKTOP=%s\n' "${XDG_CURRENT_DESKTOP:-unknown}" + printf 'WAYLAND_DISPLAY=%s\n' "${WAYLAND_DISPLAY:-unknown}" + echo + echo "=== Memory ===" + free -h 2>/dev/null || true + echo + echo "=== Filesystems ===" + df -hT / "$HOME" 2>/dev/null || true + echo + echo "=== Block devices ===" + lsblk -o NAME,SIZE,FSTYPE,FSAVAIL,MOUNTPOINTS 2>/dev/null || true + echo + echo "=== GPU ===" + lspci -nnk 2>/dev/null | grep -A4 -Ei 'VGA|3D|Display' || true + echo + echo "=== Vulkan ===" + if command -v vulkaninfo >/dev/null 2>&1; then + vulkaninfo --summary 2>/dev/null || true + else + echo "vulkaninfo not installed" + fi + echo + echo "=== Star Citizen kernel limits ===" + sysctl vm.max_map_count 2>/dev/null || true + printf 'hard_open_files=%s\n' "$(ulimit -Hn 2>/dev/null || true)" + } > "$CACHE_DIR/system-$$.txt" + sanitize_to "$CACHE_DIR/system-$$.txt" "$work/system.txt" + rm -f "$CACHE_DIR/system-$$.txt" + + { + echo "=== Relevant packages ===" + pacman -Q omarchy omarchy-dev lug-helper zenity polkit cabextract unzip 2>/dev/null || true + echo + echo "=== AUR helper ===" + yay --version 2>/dev/null | head -n 4 || true + echo + echo "=== Commands ===" + for cmd in omarchy lug-helper yay zenity wine winetricks gamescope gamemoderun; do + if command -v "$cmd" >/dev/null 2>&1; then + printf '%s=present\n' "$cmd" + else + printf '%s=missing\n' "$cmd" + fi + done + } > "$work/packages.txt" + + if command -v omarchy >/dev/null 2>&1; then + capture_sanitized "$work/plugin-validation.txt" omarchy plugin validate "$PLUGIN_DIR" + fi + + if [[ -f "$LOG_FILE" ]]; then + tail -n 1500 "$LOG_FILE" > "$CACHE_DIR/plugin-log-$$.txt" + sanitize_to "$CACHE_DIR/plugin-log-$$.txt" "$work/omarchy-citizen.log" + rm -f "$CACHE_DIR/plugin-log-$$.txt" + fi + + if [[ -f /tmp/omarchy-update.log ]]; then + tail -n 500 /tmp/omarchy-update.log > "$CACHE_DIR/update-log-$$.txt" + sanitize_to "$CACHE_DIR/update-log-$$.txt" "$work/omarchy-update-tail.log" + rm -f "$CACHE_DIR/update-log-$$.txt" + fi + + prefix="$(read_prefix 2>/dev/null || true)" + game_dir="$(read_game_dir 2>/dev/null || true)" + + { + printf 'wine_prefix=%s\n' "${prefix:-not-configured}" + printf 'game_dir=%s\n' "${game_dir:-not-configured}" + printf 'launch_script=%s\n' "$([[ -n "$prefix" && -f "$prefix/sc-launch.sh" ]] && echo present || echo missing)" + printf 'game_binary=%s\n' "$([[ -n "$game_dir" && -f "$game_dir/LIVE/Bin64/StarCitizen.exe" ]] && echo present || echo missing)" + } > "$CACHE_DIR/paths-$$.txt" + sanitize_to "$CACHE_DIR/paths-$$.txt" "$work/star-citizen-path-status.txt" + rm -f "$CACHE_DIR/paths-$$.txt" + + if [[ -n "$prefix" && -f "$prefix/sc-launch.log" ]]; then + tail -n 600 "$prefix/sc-launch.log" > "$CACHE_DIR/sc-launch-$$.txt" + sanitize_to "$CACHE_DIR/sc-launch-$$.txt" "$work/sc-launch-tail.log" + rm -f "$CACHE_DIR/sc-launch-$$.txt" + fi + + if [[ -n "$prefix" && -f "$prefix/sc-launch.sh" ]]; then + head -n 350 "$prefix/sc-launch.sh" > "$CACHE_DIR/sc-script-$$.txt" + sanitize_to "$CACHE_DIR/sc-script-$$.txt" "$work/sc-launch-script-sanitized.txt" + rm -f "$CACHE_DIR/sc-script-$$.txt" + fi + + archive="$downloads/Omarchy-Citizen-Support-$support_id.tar.gz" + tar -C "$work" -czf "$archive" . + rm -rf "$work" + + log_line "support archive=$archive" + + if command -v wl-copy >/dev/null 2>&1; then + printf '%s' "$archive" | wl-copy + fi + + if command -v zenity >/dev/null 2>&1; then + zenity --info --width=580 \ + --title="Support-Paket erstellt" \ + --text="Support-ID: +$support_id + +Das bereinigte Support-Paket wurde in deinem Downloads-Ordner erstellt: + +$(basename "$archive") + +Der Dateipfad wurde – falls die Zwischenablage verfügbar ist – kopiert. + +Schicke dieses Archiv zusammen mit der Support-ID an den Plugin-Support." \ + >/dev/null 2>&1 || true + else + notify "Omarchy Citizen" "Support-Paket erstellt: $(basename "$archive")" + fi + + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$downloads" >/dev/null 2>&1 & + fi + + printf '%s\n' "$archive" +} + +support_start() { + nohup bash "$SELF" support-build >>"$LOG_FILE" 2>&1 & +} + +open_debug_log() { + if [[ ! -f "$LOG_FILE" ]]; then + notify_error "Noch kein Debug-Log vorhanden." + return 1 + fi + xdg-open "$LOG_FILE" >/dev/null 2>&1 & +} + +open_log() { + local prefix log + prefix="$(read_prefix 2>/dev/null)" || { + notify_error "Noch kein Star-Citizen-Wine-Prefix gefunden." + return 1 + } + + log="$prefix/sc-launch.log" + if [[ ! -f "$log" ]]; then + notify_error "Noch kein Start-Log gefunden. + +Starte den RSI Launcher mindestens einmal." + return 1 + fi + + xdg-open "$log" >/dev/null 2>&1 & +} + +open_wiki() { + xdg-open "https://wiki.starcitizen-lug.org" >/dev/null 2>&1 & +} + +open_ngl_page() { + xdg-open "https://www.nglproject.com/de#downloads" >/dev/null 2>&1 & +} + +ngl_info() { + local msg + msg="NGL ist ein optionaler Community-Mod für den Windows-RSI-Launcher. + +Omarchy Citizen behandelt NGL als EXPERIMENTELL und installiert es nie ohne deine Auswahl und Bestätigung. + +Der Mod läuft im selben Wine-Prefix wie dein RSI Launcher." + + if command -v zenity >/dev/null 2>&1; then + zenity --info --width=520 --title="Omarchy Citizen – NGL" --text="$msg" >/dev/null 2>&1 & + else + notify "Omarchy Citizen – NGL" "$msg" + fi +} + +ngl_install_interactive() { + local prefix script selected workdir exe + + if ! command -v zenity >/dev/null 2>&1; then + notify_error "Die NGL-Installation benötigt die grafische Dateiauswahl. Bitte zuerst das normale Setup ausführen." + return 1 + fi + + prefix="$(read_prefix 2>/dev/null)" || { + notify_error "Star Citizen muss zuerst eingerichtet werden." + return 1 + } + + script="$prefix/sc-launch.sh" + [[ -f "$script" ]] || { + notify_error "Das Star-Citizen-Start-Script fehlt. Nutze zuerst „Problem beheben“." + return 1 + } + + selected="$(zenity \ + --file-selection \ + --title="NGL Installer auswählen" \ + --filename="$HOME/Downloads/" \ + --file-filter="NGL Installer | *.zip *.ZIP *.exe *.EXE" \ + --file-filter="Alle Dateien | *" 2>/dev/null)" || return 0 + + [[ -f "$selected" ]] || return 1 + + workdir="$CACHE_DIR/ngl-installer" + rm -rf -- "$workdir" + mkdir -p "$workdir" + + case "${selected,,}" in + *.zip) + unzip -q "$selected" -d "$workdir" || { + notify_error "Der NGL-Installer konnte nicht entpackt werden." + return 1 + } + exe="$(find "$workdir" -type f \( -iname '*ngl*.exe' -o -iname '*installer*.exe' -o -iname '*setup*.exe' -o -iname '*.exe' \) \ + -printf '%p\n' | head -n 1)" + ;; + *.exe) + exe="$selected" + ;; + *) + notify_error "Bitte eine NGL-ZIP- oder EXE-Datei auswählen." + return 1 + ;; + esac + + [[ -n "${exe:-}" && -f "$exe" ]] || { + notify_error "Kein Windows-Installer in der ausgewählten Datei gefunden." + return 1 + } + + if ! zenity --question --width=560 \ + --title="NGL experimentell installieren?" \ + --ok-label="Installer starten" \ + --cancel-label="Abbrechen" \ + --text="NGL ist Community-Code und läuft im selben Wine-Prefix wie dein RSI Launcher. + +Bitte nutze ausschließlich Downloads von nglproject.com. + +Fortfahren?" 2>/dev/null; then + return 0 + fi + + log_line "NGL installer confirmed by user" + + { + printf 'wine ' + printf '%q' "$exe" + printf '\nexit\n' + } | bash "$script" shell >>"$LOG_FILE" 2>&1 +} + +ngl_install() { + nohup bash "$SELF" ngl-install-interactive >>"$LOG_FILE" 2>&1 & +} + +case "$ACTION" in + status) status ;; + primary) primary_action ;; + setup) guided_setup ;; + guided-setup-interactive) guided_setup_interactive ;; + repair) repair_assistant ;; + repair-interactive) repair_assistant_interactive ;; + support) support_start ;; + support-build) support_build ;; + debug-log) open_debug_log ;; + plugin-update-check) plugin_update_check ;; + plugin-update-now) plugin_update_now ;; + plugin-auto-enable) plugin_auto_enable ;; + plugin-auto-disable) plugin_auto_disable ;; + updater-log) open_updater_log ;; + check-update) check_update ;; + install-stack) launch_terminal_action install-stack-terminal ;; + install-stack-terminal) install_stack_terminal ;; + update-helper) launch_terminal_action update-helper-terminal ;; + update-helper-terminal) update_helper_terminal ;; + system-update) launch_terminal_action system-update-terminal ;; + system-update-terminal) system_update_terminal ;; + launch) launch_game ;; + helper) run_helper_gui ;; + install) run_helper_gui --install ;; + preflight) run_helper_gui --preflight-check ;; + runners) run_helper_gui --manage-runners ;; + dxvk) run_helper_gui --manage-dxvk ;; + repair-launch) run_helper_gui --update-launch-script ;; + repair-rsi) run_helper_gui --update-rsi-launcher ;; + dirs) run_helper_gui --show-directories ;; + log) open_log ;; + wiki) open_wiki ;; + ngl-page) open_ngl_page ;; + ngl-info) ngl_info ;; + ngl-install) ngl_install ;; + ngl-install-interactive) ngl_install_interactive ;; + *) + notify_error "Unbekannte Omarchy-Citizen-Aktion: $ACTION" + exit 2 + ;; +esac diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..9b5d228 --- /dev/null +++ b/install.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +PLUGIN_ID="local.omarchy-citizen" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DEST="$HOME/.config/omarchy/plugins/$PLUGIN_ID" + +if ! command -v omarchy >/dev/null 2>&1; then + echo "Fehler: 'omarchy' wurde nicht gefunden." + echo "Dieses Plugin benötigt Omarchy Quattro mit dem Plugin-System." + exit 1 +fi + +mkdir -p "$DEST" + +for file in manifest.json BarWidget.qml Panel.qml citizenctl support-sanitize.py README.md INSTALLATION.md LICENSE uninstall.sh install.sh INSTALLIEREN.sh; do + src="$SCRIPT_DIR/$file" + dst="$DEST/$file" + if [[ "$(readlink -f "$src")" != "$(readlink -f "$dst" 2>/dev/null || printf '%s' "$dst")" ]]; then + cp -f -- "$src" "$dst" + fi +done + +if [[ "$(readlink -f "$SCRIPT_DIR/backend")" != "$(readlink -f "$DEST/backend" 2>/dev/null || printf '%s' "$DEST/backend")" ]]; then + rm -rf -- "$DEST/backend" + cp -a -- "$SCRIPT_DIR/backend" "$DEST/backend" +fi + +chmod +x "$DEST/citizenctl" "$DEST/support-sanitize.py" "$DEST/uninstall.sh" "$DEST/INSTALLIEREN.sh" "$DEST/backend/bin/omarchy-citizen-backend" + +echo "Validiere Plugin..." +omarchy plugin validate "$DEST" + +echo "Lade Plugin neu..." +omarchy-shell shell rescanPlugins + +echo "Aktiviere Plugin..." +"$DEST/backend/bin/omarchy-citizen-backend" self-sync >/dev/null 2>&1 || true +"$HOME/.local/lib/omarchy-citizen/omarchy-citizen-backend" install-service >/dev/null 2>&1 || true + +omarchy plugin enable "$PLUGIN_ID" + +echo +echo "Omarchy Citizen 0.6.0 wurde installiert:" +echo " $DEST" +echo +echo "Links-Klick auf 'SC' in der Omarchy-Leiste öffnet das Star-Citizen-Panel." +echo +echo "Falls 'SC' nicht sichtbar ist:" +echo " omarchy bar move $PLUGIN_ID --section right" +echo +echo "LUG Helper Status:" +bash "$DEST/citizenctl" status diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..d5a54bf --- /dev/null +++ b/manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "local.omarchy-citizen", + "name": "Omarchy Citizen", + "version": "0.6.0", + "author": "Community prototype", + "license": "MIT", + "description": "Plug-and-play Star Citizen controls for Omarchy with guided repair, support bundles and a Go-based automatic plugin update backend.", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Omarchy Citizen", + "category": "Gaming", + "allowMultiple": false, + "defaultSection": "right" + } +} diff --git a/support-sanitize.py b/support-sanitize.py new file mode 100644 index 0000000..b5f5047 --- /dev/null +++ b/support-sanitize.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +import os +import re +import socket +import sys + +HOME = os.path.expanduser("~") +USER = os.environ.get("USER", "") +HOST = socket.gethostname() + +patterns = [ + (re.compile(r"(?i)(https?://)[^/@\s]+@"), r"\1@"), + (re.compile(re.escape(HOME)), "~"), + (re.compile(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"), ""), + (re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), ""), + (re.compile(r"(?i)\b(?:[0-9a-f]{2}:){5}[0-9a-f]{2}\b"), ""), + (re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+"), "Bearer "), + (re.compile(r"(?i)\b(authorization|password|passwd|secret|token|access_token|refresh_token|session|cookie)\s*[:=]\s*[^\s,;]+"), + r"\1="), + (re.compile(r"(?i)([?&](?:token|access_token|refresh_token|session|auth|key|secret)=)[^&#\s]+"), + r"\1"), + (re.compile(r"\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"), ""), +] + +if USER: + patterns.append((re.compile(rf"(?")) +if HOST: + patterns.append((re.compile(rf"(?")) + +for line in sys.stdin: + for pattern, repl in patterns: + line = pattern.sub(repl, line) + sys.stdout.write(line) diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..0ef62a4 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +PLUGIN_ID="local.omarchy-citizen" +DEST="$HOME/.config/omarchy/plugins/$PLUGIN_ID" + +omarchy plugin disable "$PLUGIN_ID" 2>/dev/null || true + +BACKEND="$HOME/.local/lib/omarchy-citizen/omarchy-citizen-backend" +if [[ -x "$BACKEND" ]]; then + "$BACKEND" auto disable >/dev/null 2>&1 || true + "$BACKEND" uninstall-service >/dev/null 2>&1 || true +fi +rm -f -- "$BACKEND" +rmdir "$HOME/.local/lib/omarchy-citizen" 2>/dev/null || true + +if [[ -d "$DEST" ]]; then + rm -rf -- "$DEST" +fi + +omarchy-shell shell rescanPlugins 2>/dev/null || true +echo "Omarchy Citizen wurde entfernt." +echo "Star Citizen und der LUG Helper wurden NICHT entfernt."