v0.6.0
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
+275
@@ -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.
|
||||
@@ -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“."
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-<support-id>.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).
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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) }
|
||||
@@ -0,0 +1,3 @@
|
||||
module omarchy-citizen/backend
|
||||
|
||||
go 1.23
|
||||
@@ -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'
|
||||
+1025
File diff suppressed because it is too large
Load Diff
+53
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<credentials>@"),
|
||||
(re.compile(re.escape(HOME)), "~"),
|
||||
(re.compile(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"), "<email>"),
|
||||
(re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), "<ip>"),
|
||||
(re.compile(r"(?i)\b(?:[0-9a-f]{2}:){5}[0-9a-f]{2}\b"), "<mac>"),
|
||||
(re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+"), "Bearer <redacted>"),
|
||||
(re.compile(r"(?i)\b(authorization|password|passwd|secret|token|access_token|refresh_token|session|cookie)\s*[:=]\s*[^\s,;]+"),
|
||||
r"\1=<redacted>"),
|
||||
(re.compile(r"(?i)([?&](?:token|access_token|refresh_token|session|auth|key|secret)=)[^&#\s]+"),
|
||||
r"\1<redacted>"),
|
||||
(re.compile(r"\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"), "<jwt>"),
|
||||
]
|
||||
|
||||
if USER:
|
||||
patterns.append((re.compile(rf"(?<![A-Za-z0-9_.-]){re.escape(USER)}(?![A-Za-z0-9_.-])"), "<user>"))
|
||||
if HOST:
|
||||
patterns.append((re.compile(rf"(?<![A-Za-z0-9_.-]){re.escape(HOST)}(?![A-Za-z0-9_.-])"), "<host>"))
|
||||
|
||||
for line in sys.stdin:
|
||||
for pattern, repl in patterns:
|
||||
line = pattern.sub(repl, line)
|
||||
sys.stdout.write(line)
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user