This commit is contained in:
2026-08-31 12:30:29 +02:00
parent c5a83a899d
commit 88bb7c3229
18 changed files with 3310 additions and 1 deletions
+69
View File
@@ -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")
}
}
}
+52
View File
@@ -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
View File
@@ -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.
+58
View File
@@ -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“."
+21
View File
@@ -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.
+799
View File
@@ -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
}
}
}
}
+69 -1
View File
@@ -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).
+69
View File
@@ -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.
+9
View File
@@ -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
+663
View File
@@ -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) }
+3
View File
@@ -0,0 +1,3 @@
module omarchy-citizen/backend
go 1.23
+68
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -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
+21
View File
@@ -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"
}
}
+33
View File
@@ -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)
+23
View File
@@ -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."