This commit is contained in:
2026-08-31 19:19:54 +02:00
parent 20fcf46c29
commit 129fbe485b
19 changed files with 233 additions and 79 deletions
+10 -1
View File
@@ -1,4 +1,4 @@
# Citizen Launcher 0.9.0
# Citizen Launcher 0.9.1
Citizen Launcher is the distro-neutral successor to Omarchy Citizen.
@@ -79,3 +79,12 @@ For Omarchy plus bar integration:
## Security boundaries
Citizen Launcher manages its Wine/DXVK/RSI stack entirely as the current user. It does not create passwordless sudo/pacman/apt rules. GPU/kernel/base-system updates remain owned by the distribution.
## 0.9.1 fixes
- PowerShell is no longer a mandatory prefix component.
- RSI `latest.yml` parser accepts Electron Builder `path:` and nested `files: - url:` formats.
- Repair also refreshes DXVK.
- Responsive dashboard prevents long GPU/distribution names from overflowing.
- GUI shows concise user-facing errors with expandable technical details.
+7 -34
View File
@@ -1,36 +1,9 @@
# Citizen Launcher 0.9.0 — distro-neutral preview
# Citizen Launcher 0.9.1
This release turns Omarchy Citizen into an optional integration instead of the product core.
Maintenance/UX hotfix for the distro-neutral preview.
## New
- standalone distro-neutral `citizen-launcher` Go binary
- embedded polished local GUI (`citizen-launcher gui`)
- Debian/Ubuntu `.deb` packaging
- generic Linux amd64 tarball
- Fedora RPM spec template
- distro detection via `/etc/os-release`
- package-manager detection for apt/dnf/pacman/zypper/apk diagnostics
- XDG-native config/data/state/cache paths with legacy Omarchy-Citizen migration
- generic systemd user maintenance timer, with launch-time maintenance fallback on non-systemd desktops
- support bundles generated entirely in Go
- Omarchy moved to `integrations/omarchy/`
- system tools preferred; LUG AppImage runtime is only a lazy portability fallback when cabextract/curl/unzip are unavailable
## Validation performed
- `go test ./...`
- `go vet ./...`
- static Linux amd64 Go build
- shell syntax verification
- embedded GUI HTTP/API smoke test
- generic user installer smoke test in an isolated HOME
- Omarchy updater integration test
- Debian package build and metadata inspection
## Still needs real-hardware testing
- clean Debian/Ubuntu desktop → RSI login/install → Star Citizen launch
- Fedora/openSUSE hardware testing
- AMD/NVIDIA/Intel Vulkan permutations
- production standalone self-update release channel once an official repository/release URL is chosen
- Removed mandatory PowerShell Core from base Winetricks setup.
- Robust RSI Electron Builder `latest.yml` parsing (`path`, nested `url`, version fallback).
- DXVK is included in automatic repair.
- Responsive GUI with safer grid sizing and readable diagnostics.
- Added regression tests for RSI metadata formats and the minimal prefix contract.
Binary file not shown.
+25
View File
@@ -42,6 +42,30 @@ type jobStore struct {
jobs map[string]*GUIJob
}
func friendlyActionError(action string, err error) string {
if err == nil {
return ""
}
s := err.Error()
switch {
case strings.Contains(s, "Windows-Komponente"):
return "Eine Windows-Komponente konnte nicht eingerichtet werden. Der technische Fehler ist unten verfügbar."
case strings.Contains(s, "RSI latest.yml") || strings.Contains(s, "RSI Installer"):
return "Die aktuelle RSI-Launcher-Version konnte nicht zuverlässig ermittelt werden."
case strings.Contains(s, "RSI Launcher Installation"):
return "Der RSI Launcher konnte nicht installiert oder aktualisiert werden."
case strings.Contains(strings.ToLower(s), "vulkan"):
return "Vulkan ist auf diesem System nicht spielbereit."
case strings.Contains(strings.ToLower(s), "wine"):
return "Der Wine-Stack konnte den Selbsttest oder die Reparatur nicht abschließen."
default:
if action == "repair" {
return "Die automatische Reparatur konnte nicht vollständig abgeschlossen werden."
}
return "Die Aktion konnte nicht vollständig abgeschlossen werden."
}
}
func (a *App) runGUI(args []string) error {
noOpen := hasArg(args, "--no-open")
listener, err := net.Listen("tcp", "127.0.0.1:0")
@@ -143,6 +167,7 @@ func (a *App) runGUI(args []string) error {
j.Finished = time.Now().Format(time.RFC3339)
if err != nil {
j.State = "error"
j.Message = friendlyActionError(action, err)
j.Error = err.Error()
} else {
j.State = "done"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
)
const (
appVersion = "0.9.0"
appVersion = "0.9.1"
pluginID = "local.omarchy-citizen"
appID = "io.github.citizenlauncher.CitizenLauncher"
)
+103 -24
View File
@@ -27,6 +27,12 @@ const (
rsiLatestYML = rsiBaseURL + "/latest.yml"
)
// Keep the mandatory prefix small and boring. PowerShell is deliberately not
// a base dependency: current LUG documentation exposes it as a separate
// maintenance/troubleshooting action, and PowerShell Core MSI installation can
// fail under otherwise healthy Wine prefixes.
var basePrefixWinetricksVerbs = []string{"arial", "tahoma", "win11"}
type GameConfig struct {
Prefix string `json:"prefix"`
GameDir string `json:"game_dir"`
@@ -427,6 +433,78 @@ func (a *App) syncWinetricks() (string, string, error) {
return target, tag, nil
}
func parseRSILatestYML(b []byte) (string, string, error) {
var version, topPath, topSHA, firstURL, firstURLSHA string
urlIndent := -1
scanner := bufio.NewScanner(strings.NewReader(string(b)))
for scanner.Scan() {
raw := strings.TrimRight(scanner.Text(), "\r\n")
trimmed := strings.TrimSpace(raw)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
indent := len(raw) - len(strings.TrimLeft(raw, " \t"))
norm := strings.TrimSpace(strings.TrimPrefix(trimmed, "- "))
key, value, ok := strings.Cut(norm, ":")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = yamlScalar(value)
switch key {
case "version":
if version == "" {
version = value
}
case "path":
if indent == 0 && topPath == "" {
topPath = value
}
case "url":
if firstURL == "" {
firstURL = value
urlIndent = indent
}
case "sha512":
if indent == 0 && topSHA == "" {
topSHA = value
} else if firstURL != "" && firstURLSHA == "" && indent > urlIndent {
firstURLSHA = value
}
}
}
if err := scanner.Err(); err != nil {
return "", "", err
}
file := topPath
sha := topSHA
if file == "" {
file = firstURL
if sha == "" {
sha = firstURLSHA
}
}
// Electron-builder normally supplies path/url, but the version fallback
// keeps us resilient if CIG trims redundant fields from latest.yml.
if file == "" && version != "" {
file = "RSI Launcher-Setup-" + version + ".exe"
}
if file == "" {
return "", "", errors.New("RSI latest.yml enthält weder path, url noch version")
}
return file, sha, nil
}
func yamlScalar(v string) string {
v = strings.TrimSpace(v)
if len(v) >= 2 {
if (v[0] == '\'' && v[len(v)-1] == '\'') || (v[0] == '"' && v[len(v)-1] == '"') {
v = v[1 : len(v)-1]
}
}
return strings.TrimSpace(v)
}
func (a *App) latestRSIInstaller() (string, string, string, error) {
req, _ := http.NewRequest("GET", rsiLatestYML, nil)
req.Header.Set("User-Agent", "citizen-launcher/"+appVersion)
@@ -443,22 +521,19 @@ func (a *App) latestRSIInstaller() (string, string, string, error) {
if err != nil {
return "", "", "", err
}
reURL := regexp.MustCompile(`(?m)^url:\s*(.+?)\s*$`)
m := reURL.FindSubmatch(b)
if len(m) < 2 {
return "", "", "", errors.New("RSI latest.yml Format unbekannt")
file, sha, err := parseRSILatestYML(b)
if err != nil {
return "", "", "", err
}
file := strings.TrimSpace(string(m[1]))
file = strings.Trim(file, "\"'")
sha := ""
reSHA := regexp.MustCompile(`(?m)^sha512:\s*(.+?)\s*$`)
if sm := reSHA.FindSubmatch(b); len(sm) >= 2 {
sha = strings.TrimSpace(string(sm[1]))
sha = strings.Trim(sha, "\"'")
ref, err := neturl.Parse(file)
if err != nil {
return "", "", "", fmt.Errorf("RSI Installer-Pfad ungültig: %w", err)
}
if ref.IsAbs() {
return filepath.Base(ref.Path), ref.String(), sha, nil
}
base, _ := neturl.Parse(rsiBaseURL + "/")
ref := &neturl.URL{Path: file}
return file, base.ResolveReference(ref).String(), sha, nil
return filepath.Base(ref.Path), base.ResolveReference(ref).String(), sha, nil
}
func verifySHA512Base64(path, want string) error {
@@ -609,18 +684,22 @@ func (a *App) ensurePrefixComponents(gc GameConfig) error {
cache := filepath.Join(a.cacheDir, "winetricks-cache", tag)
_ = os.MkdirAll(cache, 0o755)
env = append(env, "W_CACHE="+cache, "WINETRICKS_DOWNLOADER=curl")
cmd := exec.Command(wt, "-q", "arial", "tahoma", "powershell", "win11")
cmd.Env = env
out, err := cmd.CombinedOutput()
a.logf("winetricks %s output=%s", tag, compactDiagnostic(string(out)))
if err != nil {
return fmt.Errorf("Prefix-Komponenten: %s", formatCommandFailure(err, out))
for _, verb := range basePrefixWinetricksVerbs {
cmd := exec.Command(wt, "-q", verb)
cmd.Env = env
out, runErr := cmd.CombinedOutput()
a.logf("winetricks %s verb=%s output=%s", tag, verb, compactDiagnostic(string(out)))
if runErr != nil {
return fmt.Errorf("Windows-Komponente %s: %s", verb, formatCommandFailure(runErr, out))
}
}
runner, _ := a.currentRunner()
wine := filepath.Join(runner, "bin", "wine")
reg := exec.Command(wine, "reg", "add", `HKEY_CURRENT_USER\Software\Wine\FileOpenAssociations`, `/v`, `Enable`, `/d`, `N`, `/f`)
reg := exec.Command(wine, "reg", "add", `HKEY_CURRENT_USER\\Software\\Wine\\FileOpenAssociations`, `/v`, `Enable`, `/d`, `N`, `/f`)
reg.Env = env
out, err = reg.CombinedOutput()
out, err := reg.CombinedOutput()
if err != nil {
return fmt.Errorf("Wine Registry: %s", formatCommandFailure(err, out))
}
@@ -788,9 +867,6 @@ func (a *App) gameRepair() error {
if hs.State == "blocked" {
return errors.New(hs.Reason)
}
if err := a.syncLUGHelper(); err != nil {
a.logf("LUG runtime warning: %v", err)
}
if err := a.syncWineRunner(); err != nil {
return err
}
@@ -803,6 +879,9 @@ func (a *App) gameRepair() error {
if err := a.ensureRSILauncher(gc, false); err != nil {
return err
}
if err := a.syncDXVK(); err != nil {
return fmt.Errorf("DXVK: %w", err)
}
if err := a.writeOwnedLaunchFiles(gc); err != nil {
return err
}
@@ -0,0 +1,58 @@
package main
import (
"strings"
"testing"
)
func TestParseRSILatestElectronBuilder(t *testing.T) {
y := `version: 2.15.0
files:
- url: RSI Launcher-Setup-2.15.0.exe
sha512: nested-sha
size: 123
path: RSI Launcher-Setup-2.15.0.exe
sha512: top-sha
releaseDate: '2026-07-27T18:00:00.000Z'
`
file, sha, err := parseRSILatestYML([]byte(y))
if err != nil {
t.Fatal(err)
}
if file != "RSI Launcher-Setup-2.15.0.exe" || sha != "top-sha" {
t.Fatalf("unexpected: file=%q sha=%q", file, sha)
}
}
func TestParseRSILatestNestedOnly(t *testing.T) {
y := `version: 2.15.0
files:
- url: "RSI Launcher-Setup-2.15.0.exe"
sha512: "nested-sha"
`
file, sha, err := parseRSILatestYML([]byte(y))
if err != nil {
t.Fatal(err)
}
if file != "RSI Launcher-Setup-2.15.0.exe" || sha != "nested-sha" {
t.Fatalf("unexpected: file=%q sha=%q", file, sha)
}
}
func TestParseRSILatestVersionFallback(t *testing.T) {
file, _, err := parseRSILatestYML([]byte("version: 2.15.0\n"))
if err != nil {
t.Fatal(err)
}
if file != "RSI Launcher-Setup-2.15.0.exe" {
t.Fatalf("unexpected file %q", file)
}
}
func TestBasePrefixDoesNotRequirePowerShell(t *testing.T) {
for _, v := range basePrefixWinetricksVerbs {
if strings.EqualFold(v, "powershell") {
t.Fatalf("PowerShell must remain optional")
}
}
}
+11 -1
View File
@@ -1 +1,11 @@
const B=window.CITIZEN_BASE;let current=null;const $=id=>document.getElementById(id);async function api(path,opts){const r=await fetch(B+path,opts);if(!r.ok)throw new Error(await r.text());return r.json()}function cls(el,state){el.classList.remove('good','bad','warn');if(state)el.classList.add(state)}function txt(id,v,c){const e=$(id);e.textContent=v||'—';cls(e,c)}function primaryFor(h){if(h==='ready')return['STAR CITIZEN STARTEN','launch'];if(h==='hardware-blocked')return['HARDWARE-DETAILS','doctor'];if(h==='repair'||h==='launcher-repair')return['AUTOMATISCH REPARIEREN','repair'];return['EINRICHTEN & STARTKLAR MACHEN','setup']}async function refresh(){try{const s=await api('/api/status');current=s;$('version').textContent='v'+s.version;$('platform').textContent=[s.platform.name,s.platform.desktop].filter(Boolean).join(' · ');$('distro').textContent=s.platform.name;$('session').textContent=s.platform.session||'—';const g=s.game;let head='READY FOR SETUP',sub='Citizen Launcher übernimmt Wine, DXVK, Prefix und RSI Launcher.';if(g.health==='ready'){head='FLIGHT READY';sub='Star Citizen ist startbereit. Autopilot hält den Gaming-Stack aktuell.'}else if(g.health==='hardware-blocked'){head='HARDWARE CHECK';sub=g.hardware_reason||'Die Hardware erfüllt eine Voraussetzung nicht.'}else if(g.health==='repair'||g.health==='launcher-repair'){head='REPAIR AVAILABLE';sub='Ein reparierbarer Zustand wurde erkannt.'}$('headline').textContent=head;$('subline').textContent=sub;const [label,act]=primaryFor(g.health);$('primary').textContent=label;$('primary').dataset.action=act;txt('gpu',g.gpu||'nicht eindeutig erkannt',g.hardware==='blocked'?'bad':'good');txt('vulkan',g.vulkan,g.vulkan==='blocked'?'bad':g.vulkan==='ready'?'good':'warn');txt('ram',g.ram_gib+' GiB',g.ram_gib>=16?'good':'warn');txt('wine',g.wine_version||'wird eingerichtet',g.wine_version?'good':'warn');txt('dxvk',g.dxvk_version||'wird eingerichtet',g.dxvk_version?'good':'warn');txt('rsi',g.launcher_state==='ready'?'bereit':'Setup',g.launcher_state==='ready'?'good':'warn');txt('autopilot',g.autopilot?'aktiv':'aus',g.autopilot?'good':'warn');$('raw').textContent=JSON.stringify(s,null,2)}catch(e){$('headline').textContent='BACKEND ERROR';$('subline').textContent=e.message}}async function action(name){try{if(name==='launch'){await api('/api/action/launch',{method:'POST'});return}const j=await api('/api/action/'+name,{method:'POST'});$('job').classList.remove('hidden');$('jobTitle').textContent='Aktion: '+name;$('jobText').textContent='läuft …';poll(j.id)}catch(e){$('job').classList.remove('hidden');$('jobTitle').textContent='Fehler';$('jobText').textContent=e.message}}async function poll(id){try{const j=await api('/api/job/'+id);if(j.state==='running'){setTimeout(()=>poll(id),1000);return}$('jobText').textContent=j.state==='done'?(j.message||'Abgeschlossen'):j.error;$('job').querySelector('.spinner').classList.toggle('hidden',j.state!=='running');await refresh()}catch(e){$('jobText').textContent=e.message}}document.addEventListener('click',e=>{const a=e.target.dataset.action;if(a)action(a);const o=e.target.dataset.open;if(o)api('/api/open/'+o,{method:'POST'});});$('toggle').onclick=()=>$('advanced').classList.toggle('hidden');$('primary').onclick=()=>action($('primary').dataset.action);refresh();setInterval(refresh,10000);
const B=window.CITIZEN_BASE;let current=null;const $=id=>document.getElementById(id);
async function api(path,opts){const r=await fetch(B+path,opts);if(!r.ok)throw new Error(await r.text());return r.json()}
function cls(el,state){el.classList.remove('good','bad','warn');if(state)el.classList.add(state)}
function txt(id,v,c){const e=$(id);const value=(v===undefined||v===null||v==='')?'—':String(v);e.textContent=value;e.title=value;cls(e,c)}
function primaryFor(h){if(h==='ready')return['STAR CITIZEN STARTEN','launch'];if(h==='hardware-blocked')return['HARDWARE-DETAILS','doctor'];if(h==='repair'||h==='launcher-repair')return['AUTOMATISCH REPARIEREN','repair'];return['EINRICHTEN & STARTKLAR MACHEN','setup']}
function setHeroHealth(h){$('hero').dataset.health=h||'setup'}
async function refresh(){try{const s=await api('/api/status');current=s;$('version').textContent='v'+s.version;$('platform').textContent=[s.platform.name,s.platform.desktop].filter(Boolean).join(' · ');$('distro').textContent=s.platform.name;$('session').textContent=s.platform.session||'—';const g=s.game;setHeroHealth(g.health);let head='READY FOR SETUP',sub='Citizen Launcher übernimmt Wine, DXVK, Prefix und RSI Launcher.';if(g.health==='ready'){head='FLIGHT READY';sub='Star Citizen ist startbereit. Autopilot hält den Gaming-Stack aktuell.'}else if(g.health==='hardware-blocked'){head='HARDWARE CHECK';sub=g.hardware_reason||'Die Hardware erfüllt eine Voraussetzung nicht.'}else if(g.health==='repair'||g.health==='launcher-repair'){head='REPAIR AVAILABLE';sub='Ein reparierbarer Zustand wurde erkannt. Die Reparatur verändert keine Spieldaten.'}else if(g.health==='install-game'){head='LAUNCHER READY';sub='Der RSI Launcher ist bereit. Melde dich an und installiere Star Citizen.'}$('headline').textContent=head;$('subline').textContent=sub;const [label,act]=primaryFor(g.health);$('primary').textContent=label;$('primary').dataset.action=act;txt('gpu',g.gpu||'nicht eindeutig erkannt',g.hardware==='blocked'?'bad':'good');txt('vulkan',g.vulkan,g.vulkan==='blocked'?'bad':g.vulkan==='ready'?'good':'warn');txt('ram',g.ram_gib+' GiB',g.ram_gib>=16?'good':'warn');txt('wine',g.wine_version||'wird eingerichtet',g.wine_version?'good':'warn');txt('dxvk',g.dxvk_version||'wird eingerichtet',g.dxvk_version?'good':'warn');txt('rsi',g.launcher_state==='ready'?'bereit':'Setup',g.launcher_state==='ready'?'good':'warn');txt('autopilot',g.autopilot?'aktiv':'aus',g.autopilot?'good':'warn');$('raw').textContent=JSON.stringify(s,null,2)}catch(e){$('headline').textContent='BACKEND ERROR';$('subline').textContent=e.message}}
function showJob(state,title,message,raw=''){const box=$('job');box.classList.remove('hidden','error','done');if(state==='error')box.classList.add('error');if(state==='done')box.classList.add('done');$('jobTitle').textContent=title;$('jobText').textContent=message||'';const spin=box.querySelector('.spinner');const symbol=$('jobSymbol');spin.classList.toggle('hidden',state!=='running');symbol.classList.toggle('hidden',state==='running');symbol.textContent=state==='done'?'✓':'!';const details=$('jobDetails');if(raw&&raw!==message){details.classList.remove('hidden');$('jobRaw').textContent=raw}else{details.classList.add('hidden');$('jobRaw').textContent=''}}
async function action(name){try{if(name==='launch'){await api('/api/action/launch',{method:'POST'});showJob('done','RSI Launcher gestartet','Der Launcher wurde gestartet.');return}const j=await api('/api/action/'+name,{method:'POST'});showJob('running','Aktion: '+name,'Wird ausgeführt …');poll(j.id)}catch(e){showJob('error','Aktion konnte nicht gestartet werden',e.message,e.message)}}
async function poll(id){try{const j=await api('/api/job/'+id);if(j.state==='running'){setTimeout(()=>poll(id),800);return}if(j.state==='done')showJob('done','Abgeschlossen',j.message||'Die Aktion wurde erfolgreich abgeschlossen.');else showJob('error','Aktion fehlgeschlagen',j.message||'Die Aktion konnte nicht abgeschlossen werden.',j.error||'');await refresh()}catch(e){showJob('error','Statusfehler','Der Aktionsstatus konnte nicht gelesen werden.',e.message)}}
document.addEventListener('click',e=>{const button=e.target.closest('[data-action],[data-open]');if(!button)return;const a=button.dataset.action;if(a)action(a);const o=button.dataset.open;if(o)api('/api/open/'+o,{method:'POST'});});$('toggle').onclick=()=>$('advanced').classList.toggle('hidden');$('primary').onclick=()=>action($('primary').dataset.action);refresh();setInterval(refresh,10000);
+9 -9
View File
@@ -2,15 +2,15 @@
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Citizen Launcher</title><link rel="stylesheet" href="__BASE__/assets/style.css"></head>
<body><div class="scan"></div><main>
<header><div class="mark">✦</div><div><div class="eyebrow">LINUX FLIGHT SYSTEM</div><h1>Citizen Launcher</h1><p id="platform">Linux wird erkannt …</p></div><div class="version" id="version"></div></header>
<section class="hero" id="hero"><div><div class="eyebrow">SYSTEM STATE</div><h2 id="headline">SYSTEM CHECK</h2><p id="subline">Hardware und Gaming-Stack werden geprüft.</p></div><button class="primary" id="primary">PRÜFEN …</button></section>
<section class="grid three">
<article><h3>Hardware</h3><div class="metric"><span>GPU</span><b id="gpu">—</b></div><div class="metric"><span>Vulkan</span><b id="vulkan">—</b></div><div class="metric"><span>RAM</span><b id="ram">—</b></div></article>
<article><h3>Gaming Stack</h3><div class="metric"><span>Wine</span><b id="wine">—</b></div><div class="metric"><span>DXVK</span><b id="dxvk">—</b></div><div class="metric"><span>Launcher</span><b id="rsi">—</b></div></article>
<article><h3>Autopilot</h3><div class="metric"><span>Wartung</span><b id="autopilot">—</b></div><div class="metric"><span>Distribution</span><b id="distro">—</b></div><div class="metric"><span>Session</span><b id="session">—</b></div></article>
<header><div class="mark">✦</div><div class="brand"><div class="eyebrow">LINUX FLIGHT SYSTEM</div><h1>Citizen Launcher</h1><p id="platform">Linux wird erkannt …</p></div><div class="version" id="version"></div></header>
<section class="hero" id="hero"><div class="hero-copy"><div class="eyebrow">SYSTEM STATE</div><h2 id="headline">SYSTEM CHECK</h2><p id="subline">Hardware und Gaming-Stack werden geprüft.</p></div><button class="primary" id="primary">PRÜFEN …</button></section>
<section class="dashboard">
<article class="card hardware"><h3>Hardware</h3><div class="metric"><span>GPU</span><b id="gpu">—</b></div><div class="metric"><span>Vulkan</span><b id="vulkan">—</b></div><div class="metric"><span>RAM</span><b id="ram">—</b></div></article>
<article class="card"><h3>Gaming Stack</h3><div class="metric"><span>Wine</span><b id="wine">—</b></div><div class="metric"><span>DXVK</span><b id="dxvk">—</b></div><div class="metric"><span>Launcher</span><b id="rsi">—</b></div></article>
<article class="card"><h3>Autopilot</h3><div class="metric"><span>Wartung</span><b id="autopilot">—</b></div><div class="metric"><span>Distribution</span><b id="distro">—</b></div><div class="metric"><span>Session</span><b id="session">—</b></div></article>
</section>
<section class="actions"><button data-action="maintain">Gaming-Stack aktualisieren</button><button data-action="repair">Problem automatisch beheben</button><button data-action="doctor">Wine-Selbsttest</button><button data-action="support">Support-Paket erstellen</button></section>
<section class="job hidden" id="job"><div class="spinner"></div><div><b id="jobTitle">Aktion läuft</b><p id="jobText"></p></div></section>
<section class="actions"><button data-action="maintain"><strong>Stack aktualisieren</strong><small>Wine, DXVK & Launcher prüfen</small></button><button data-action="repair"><strong>Automatisch reparieren</strong><small>Setup und Launcher heilen</small></button><button data-action="doctor"><strong>Wine-Selbsttest</strong><small>Prefix isoliert prüfen</small></button><button data-action="support"><strong>Support-Paket</strong><small>Diagnose für Hilfe erstellen</small></button></section>
<section class="job hidden" id="job"><div class="job-icon"><div class="spinner"></div><span class="job-symbol hidden" id="jobSymbol">!</span></div><div class="job-body"><b id="jobTitle">Aktion läuft</b><p id="jobText"></p><details class="job-details hidden" id="jobDetails"><summary>Technische Details</summary><pre id="jobRaw"></pre></details></div></section>
<section class="details"><button id="toggle">Erweiterte Informationen</button><div id="advanced" class="hidden"><pre id="raw"></pre><div class="small-actions"><button data-open="logs">Logs öffnen</button><button data-open="downloads">Downloads öffnen</button></div></div></section>
<footer>Citizen Launcher verwaltet Star Citizen im Benutzerkonto. System-/GPU-Treiber bleiben Aufgabe der Distribution.</footer>
<footer>Citizen Launcher verwaltet den Gaming-Stack im Benutzerkonto. System- und GPU-Treiber bleiben bei deiner Distribution.</footer>
</main><script>window.CITIZEN_BASE="__BASE__";</script><script src="__BASE__/assets/app.js"></script></body></html>
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -12,7 +12,7 @@ Panel {
property var anchorItem: null
property var hostWidget: null
property string pluginVersion: "0.9.0"
property string pluginVersion: "0.9.1"
property string health: "checking"
property string depsState: "checking"
property string depsMissing: ""
@@ -106,7 +106,7 @@ Panel {
values[lines[i].slice(0, p)] = lines[i].slice(p + 1)
}
pluginVersion = values.plugin_version || "0.9.0"
pluginVersion = values.plugin_version || "0.9.1"
health = values.health || "setup"
depsState = values.deps || "missing"
depsMissing = values.deps_missing || ""
+1 -1
View File
@@ -2,7 +2,7 @@
set -u
PLUGIN_ID="local.omarchy-citizen"
PLUGIN_VERSION="0.9.0"
PLUGIN_VERSION="0.9.1"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
+1 -1
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"id": "local.omarchy-citizen",
"name": "Citizen Launcher · Omarchy",
"version": "0.9.0",
"version": "0.9.1",
"author": "Community prototype",
"license": "MIT",
"description": "Optional Omarchy bar integration for the distro-neutral Citizen Launcher.",
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION="0.9.0"; ARCH="amd64"; WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
VERSION="0.9.1"; ARCH="amd64"; WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
PKG="$WORK/citizen-launcher_${VERSION}_${ARCH}"
mkdir -p "$PKG/DEBIAN" "$PKG/usr/bin" "$PKG/usr/share/applications"
install -m755 "$ROOT/backend/bin/citizen-launcher" "$PKG/usr/bin/citizen-launcher"
+1 -1
View File
@@ -2,4 +2,4 @@
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
mkdir -p "$ROOT/dist"
tar -C "$ROOT/backend/bin" -czf "$ROOT/dist/citizen-launcher-0.9.0-linux-amd64.tar.gz" citizen-launcher
tar -C "$ROOT/backend/bin" -czf "$ROOT/dist/citizen-launcher-0.9.1-linux-amd64.tar.gz" citizen-launcher
+1 -1
View File
@@ -1,5 +1,5 @@
Name: citizen-launcher
Version: 0.9.0
Version: 0.9.1
Release: 1%{?dist}
Summary: Star Citizen launcher and self-maintaining Wine stack for Linux
License: MIT
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
for f in build.sh install.sh install-omarchy.sh uninstall.sh packaging/build-deb.sh packaging/build-tarball.sh integrations/omarchy/citizenctl; do bash -n "$ROOT/$f"; done
(cd "$ROOT/backend" && gofmt -w cmd/citizen-launcher/*.go && go test ./... && go vet ./... && ./build.sh)
"$ROOT/backend/bin/citizen-launcher" --version | grep -qx '0.9.0'
"$ROOT/backend/bin/citizen-launcher" --version | grep -qx '0.9.1'
"$ROOT/backend/bin/citizen-launcher" platform --json | grep -q '"id"'
# Ensure GUI assets are embedded by looking for a distinctive string in binary.
grep -a -q 'FLIGHT READY' "$ROOT/backend/bin/citizen-launcher"