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