diff --git a/README.md b/README.md index 4f21cfa..9d83820 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f789850..a6212a1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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. diff --git a/backend/bin/citizen-launcher b/backend/bin/citizen-launcher index 3ca1c3c..5cecb9b 100644 Binary files a/backend/bin/citizen-launcher and b/backend/bin/citizen-launcher differ diff --git a/backend/cmd/citizen-launcher/gui.go b/backend/cmd/citizen-launcher/gui.go index 7567430..7563b07 100644 --- a/backend/cmd/citizen-launcher/gui.go +++ b/backend/cmd/citizen-launcher/gui.go @@ -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" diff --git a/backend/cmd/citizen-launcher/main.go b/backend/cmd/citizen-launcher/main.go index 75e4219..204a1de 100644 --- a/backend/cmd/citizen-launcher/main.go +++ b/backend/cmd/citizen-launcher/main.go @@ -16,7 +16,7 @@ import ( ) const ( - appVersion = "0.9.0" + appVersion = "0.9.1" pluginID = "local.omarchy-citizen" appID = "io.github.citizenlauncher.CitizenLauncher" ) diff --git a/backend/cmd/citizen-launcher/stack.go b/backend/cmd/citizen-launcher/stack.go index e7fba57..b260e63 100644 --- a/backend/cmd/citizen-launcher/stack.go +++ b/backend/cmd/citizen-launcher/stack.go @@ -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 } diff --git a/backend/cmd/citizen-launcher/stack_test.go b/backend/cmd/citizen-launcher/stack_test.go new file mode 100644 index 0000000..b5a8030 --- /dev/null +++ b/backend/cmd/citizen-launcher/stack_test.go @@ -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") + } + } +} diff --git a/backend/cmd/citizen-launcher/web/app.js b/backend/cmd/citizen-launcher/web/app.js index 036a012..f9a28b2 100644 --- a/backend/cmd/citizen-launcher/web/app.js +++ b/backend/cmd/citizen-launcher/web/app.js @@ -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); \ No newline at end of file +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); \ No newline at end of file diff --git a/backend/cmd/citizen-launcher/web/index.html b/backend/cmd/citizen-launcher/web/index.html index 732be6c..68c6d8d 100644 --- a/backend/cmd/citizen-launcher/web/index.html +++ b/backend/cmd/citizen-launcher/web/index.html @@ -2,15 +2,15 @@ Citizen Launcher
-
✦
LINUX FLIGHT SYSTEM

Citizen Launcher

Linux wird erkannt …

-
SYSTEM STATE

SYSTEM CHECK

Hardware und Gaming-Stack werden geprüft.

-
-

Hardware

GPU—
Vulkan—
RAM—
-

Gaming Stack

Wine—
DXVK—
Launcher—
-

Autopilot

Wartung—
Distribution—
Session—
+
✦
LINUX FLIGHT SYSTEM

Citizen Launcher

Linux wird erkannt …

+
SYSTEM STATE

SYSTEM CHECK

Hardware und Gaming-Stack werden geprüft.

+
+

Hardware

GPU—
Vulkan—
RAM—
+

Gaming Stack

Wine—
DXVK—
Launcher—
+

Autopilot

Wartung—
Distribution—
Session—
-
- +
+
-
Citizen Launcher verwaltet Star Citizen im Benutzerkonto. System-/GPU-Treiber bleiben Aufgabe der Distribution.
+
\ No newline at end of file diff --git a/backend/cmd/citizen-launcher/web/style.css b/backend/cmd/citizen-launcher/web/style.css index 8d9a3ef..40a378f 100644 --- a/backend/cmd/citizen-launcher/web/style.css +++ b/backend/cmd/citizen-launcher/web/style.css @@ -1 +1 @@ -:root{color-scheme:dark;--bg:#070d13;--panel:#0b1822;--panel2:#0f2230;--line:#1d526d;--cyan:#55d8ff;--green:#75e9b1;--amber:#ffd275;--red:#ff879c;--text:#e8f5fb;--muted:#8da7b5}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 70% -20%,#123047 0,transparent 42%),linear-gradient(150deg,#050a10,#09131c 60%,#06101a);color:var(--text);font:15px Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh}.scan{position:fixed;inset:0;pointer-events:none;opacity:.12;background:repeating-linear-gradient(0deg,transparent 0 3px,#66dbff10 4px)}main{width:min(1180px,calc(100% - 32px));margin:28px auto 48px}header{display:flex;align-items:center;gap:16px;padding:0 4px 20px}.mark{width:54px;height:54px;display:grid;place-items:center;border:1px solid var(--cyan);border-radius:14px;color:var(--cyan);font-size:25px;box-shadow:0 0 25px #4bd5ff22 inset}.eyebrow{font-size:11px;letter-spacing:.18em;color:var(--cyan);font-weight:800}h1,h2,h3,p{margin:0}h1{font-size:25px;letter-spacing:.02em}header p{color:var(--muted);margin-top:3px}.version{margin-left:auto;color:var(--muted);font-family:ui-monospace,monospace}.hero,.grid article,.job,.details{border:1px solid #1c4053;border-radius:20px;background:linear-gradient(155deg,#0c1b26e8,#09131be8);box-shadow:0 18px 60px #0007}.hero{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:28px;border-color:#2c7798}.hero h2{font-size:35px;letter-spacing:.04em;margin:5px 0 7px}.hero p{color:var(--muted);max-width:690px}.primary{padding:17px 28px!important;font-weight:900;letter-spacing:.08em;border-color:var(--cyan)!important;color:#08141c!important;background:linear-gradient(135deg,#7de4ff,#45c8f7)!important;min-width:270px}.grid{display:grid;gap:14px;margin-top:14px}.three{grid-template-columns:repeat(3,1fr)}article{padding:20px}article h3{color:var(--cyan);font-size:13px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:12px}.metric{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid #153345}.metric:last-child{border:0}.metric span{color:var(--muted)}.metric b{text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:65%}.actions{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:14px 0}button{border:1px solid #2d5368;background:#0d1b25;color:var(--text);padding:13px 16px;border-radius:10px;cursor:pointer;font:inherit;transition:.15s}button:hover{border-color:var(--cyan);background:#102737}.job{display:flex;align-items:center;gap:14px;padding:16px;margin:14px 0}.job p{color:var(--muted);margin-top:3px}.spinner{width:22px;height:22px;border:2px solid #285165;border-top-color:var(--cyan);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.details{padding:12px 14px}.details>button{width:100%;background:transparent;border:0;color:var(--muted)}pre{white-space:pre-wrap;color:#a9c7d4;background:#061018;padding:14px;border-radius:12px;max-height:330px;overflow:auto;font-size:12px}.small-actions{display:flex;gap:8px}.hidden{display:none!important}.bad{color:var(--red)!important}.warn{color:var(--amber)!important}.good{color:var(--green)!important}footer{text-align:center;color:#597584;font-size:12px;padding:22px}@media(max-width:850px){.three,.actions{grid-template-columns:1fr}.hero{align-items:stretch;flex-direction:column}.primary{width:100%}} \ No newline at end of file +:root{color-scheme:dark;--bg:#060b11;--panel:#0a1721;--panel2:#0d202d;--line:#183d50;--line2:#2b6f8d;--cyan:#54d8ff;--green:#70e7ad;--amber:#ffd06b;--red:#ff8398;--text:#edf8fc;--muted:#8da7b5;--shadow:#0008}*{box-sizing:border-box}html{background:var(--bg)}body{margin:0;min-height:100vh;color:var(--text);font:15px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:radial-gradient(circle at 68% -15%,#14354d 0,transparent 40%),linear-gradient(145deg,#050a0f,#09151f 60%,#06111a)}.scan{position:fixed;inset:0;pointer-events:none;opacity:.07;background:repeating-linear-gradient(0deg,transparent 0 4px,#66dbff12 5px)}main{width:min(1280px,calc(100% - 40px));margin:26px auto 48px;position:relative}header{display:flex;align-items:center;gap:15px;padding:0 3px 20px;min-width:0}.mark{width:52px;height:52px;flex:0 0 52px;display:grid;place-items:center;border:1px solid var(--cyan);border-radius:14px;color:var(--cyan);font-size:24px;background:#0c2030aa;box-shadow:0 0 25px #4bd5ff1d inset}.brand{min-width:0}.eyebrow{font-size:10px;letter-spacing:.2em;color:var(--cyan);font-weight:850}h1,h2,h3,p{margin:0}h1{font-size:24px;letter-spacing:.015em}header p{color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.version{margin-left:auto;align-self:flex-start;padding-top:13px;color:var(--muted);font:13px ui-monospace,SFMono-Regular,Consolas,monospace}.hero,.card,.job,.details{border:1px solid var(--line);background:linear-gradient(155deg,#0c1b26ed,#08131bed);box-shadow:0 20px 60px var(--shadow)}.hero{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:26px;padding:26px;border-color:var(--line2);border-radius:18px;min-width:0}.hero-copy{min-width:0}.hero h2{font-size:clamp(28px,3vw,38px);line-height:1.05;letter-spacing:.045em;margin:7px 0 9px}.hero p{color:var(--muted);max-width:740px}.primary{min-width:250px;padding:16px 24px!important;font-weight:900;letter-spacing:.075em;border-color:var(--cyan)!important;color:#06131b!important;background:linear-gradient(135deg,#7de5ff,#45c8f7)!important;box-shadow:0 8px 30px #37c7f522}.dashboard{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(0,.78fr) minmax(0,.9fr);gap:14px;margin-top:14px;align-items:stretch}.card{padding:19px;border-radius:17px;min-width:0;overflow:hidden}.card h3{color:var(--cyan);font-size:12px;letter-spacing:.14em;text-transform:uppercase;margin-bottom:10px}.metric{display:grid;grid-template-columns:minmax(88px,.48fr) minmax(0,1fr);align-items:start;gap:12px;padding:9px 0;border-bottom:1px solid #143445;min-width:0}.metric:last-child{border:0}.metric span{color:var(--muted)}.metric b{min-width:0;text-align:right;overflow-wrap:anywhere;word-break:break-word}.actions{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:14px 0}.actions button{display:flex;flex-direction:column;align-items:flex-start;text-align:left;gap:2px;min-width:0}.actions strong{font-weight:700}.actions small{color:var(--muted);font-size:11px;white-space:normal}button{border:1px solid #294e63;background:#0c1b25;color:var(--text);padding:12px 15px;border-radius:10px;cursor:pointer;font:inherit;transition:border-color .15s,background .15s,transform .15s}button:hover{border-color:var(--cyan);background:#102837}button:active{transform:translateY(1px)}.job{display:grid;grid-template-columns:auto minmax(0,1fr);gap:13px;align-items:start;padding:16px 17px;margin:14px 0;border-radius:15px}.job.error{border-color:#7e3142;background:linear-gradient(155deg,#23131a,#0b141b)}.job.done{border-color:#285e4b}.job-icon{width:26px;height:26px;display:grid;place-items:center}.job-body{min-width:0}.job p{color:var(--muted);margin-top:4px;white-space:pre-wrap;overflow-wrap:anywhere}.job-symbol{font-size:18px;font-weight:900}.job.error .job-symbol{color:var(--red)}.job.done .job-symbol{color:var(--green)}.spinner{width:21px;height:21px;border:2px solid #285165;border-top-color:var(--cyan);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.job-details{margin-top:10px}.job-details summary{cursor:pointer;color:#b8d0db;font-size:12px}.job-details pre{margin-top:8px}.details{padding:10px 13px;border-radius:15px}.details>button{width:100%;background:transparent;border:0;color:var(--muted)}pre{white-space:pre-wrap;overflow-wrap:anywhere;color:#a9c7d4;background:#050f16;padding:13px;border:1px solid #112e3d;border-radius:10px;max-height:330px;overflow:auto;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace}.small-actions{display:flex;flex-wrap:wrap;gap:8px}.hidden{display:none!important}.bad{color:var(--red)!important}.warn{color:var(--amber)!important}.good{color:var(--green)!important}footer{text-align:center;color:#597584;font-size:11px;padding:22px 8px}@media(max-width:1080px){main{width:min(980px,calc(100% - 32px))}.dashboard{grid-template-columns:repeat(2,minmax(0,1fr))}.dashboard .hardware{grid-column:1/-1}.actions{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){main{width:calc(100% - 22px);margin-top:14px}.hero{grid-template-columns:1fr;padding:20px}.primary{width:100%;min-width:0}.dashboard{grid-template-columns:1fr}.dashboard .hardware{grid-column:auto}.actions{grid-template-columns:1fr}header{padding-bottom:14px}.version{font-size:11px}.metric{grid-template-columns:minmax(80px,.42fr) minmax(0,1fr)}} \ No newline at end of file diff --git a/dist/citizen-launcher-0.9.1-linux-amd64.tar.gz b/dist/citizen-launcher-0.9.1-linux-amd64.tar.gz new file mode 100644 index 0000000..f7bab73 Binary files /dev/null and b/dist/citizen-launcher-0.9.1-linux-amd64.tar.gz differ diff --git a/dist/citizen-launcher_0.9.1_amd64.deb b/dist/citizen-launcher_0.9.1_amd64.deb new file mode 100644 index 0000000..68b417a Binary files /dev/null and b/dist/citizen-launcher_0.9.1_amd64.deb differ diff --git a/integrations/omarchy/Panel.qml b/integrations/omarchy/Panel.qml index 901af23..2847b7c 100644 --- a/integrations/omarchy/Panel.qml +++ b/integrations/omarchy/Panel.qml @@ -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 || "" diff --git a/integrations/omarchy/citizenctl b/integrations/omarchy/citizenctl index 5532e1e..6c6332e 100644 --- a/integrations/omarchy/citizenctl +++ b/integrations/omarchy/citizenctl @@ -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}" diff --git a/integrations/omarchy/manifest.json b/integrations/omarchy/manifest.json index c406771..0201c2b 100644 --- a/integrations/omarchy/manifest.json +++ b/integrations/omarchy/manifest.json @@ -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.", diff --git a/packaging/build-deb.sh b/packaging/build-deb.sh index cfcae8c..4ccf7a2 100644 --- a/packaging/build-deb.sh +++ b/packaging/build-deb.sh @@ -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" diff --git a/packaging/build-tarball.sh b/packaging/build-tarball.sh index 263611a..023e7ae 100644 --- a/packaging/build-tarball.sh +++ b/packaging/build-tarball.sh @@ -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 diff --git a/packaging/citizen-launcher.spec b/packaging/citizen-launcher.spec index e311be5..5125ef9 100644 --- a/packaging/citizen-launcher.spec +++ b/packaging/citizen-launcher.spec @@ -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 diff --git a/tests/verify.sh b/tests/verify.sh index e37a93e..e582317 100644 --- a/tests/verify.sh +++ b/tests/verify.sh @@ -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"