From c1b899ec8b0cb587633a161f7de669b60188c91d Mon Sep 17 00:00:00 2001 From: Stefan Winkler Date: Wed, 22 Jul 2026 21:52:08 +0200 Subject: [PATCH] Linux-Fassung (Ubuntu) ergaenzen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Fachlogik war bereits plattformneutral — paths.js loeste ~/.config schon auf, jsonl.js liest ueber os.homedir(). Zu tun waren Autostart, Build-Targets und drei Linux-Eigenheiten der Oberflaeche. XWayland erzwingen: Ab Electron 36 waehlt Chromium in einer Wayland-Sitzung von sich aus Wayland. Dort darf sich ein Fenster weder selbst positionieren noch dauerhaft in den Vordergrund legen — beides ist der Kern dieses Widgets. Der Schalter muss auf der Kommandozeile stehen, weil Ozone die Plattform vor dem Main-Skript festlegt; appendSwitch() und ELECTRON_OZONE_PLATFORM_HINT kommen beide zu spaet. Bleibt ein einmaliger Neustart mit --ozone-platform=x11, der sich am Argument selbst als Abbruchbedingung erkennt. Im AppImage muss dabei execPath auf APPIMAGE zeigen, sonst laeuft der Nachfolger in den /tmp-Einhaengepunkt, den der Vorgaenger beim Beenden wegraeumt. Autostart: app.setLoginItemSettings() gibt es unter Linux nicht. Das neue Modul autostart.js kapselt die Weiche und schreibt bzw. loescht dort ~/.config/autostart/claude-live-dashboard.desktop. Der Startbefehl kommt aus APPIMAGE, falls gesetzt. Ausserdem: enable-transparent-visuals fuer echte Transparenz, das 32-px-Tray-Symbol in der GNOME-Leiste, plattformabhaengige Beschriftungen in Tray-Menue und Einstellungsdialog, AppImage- und deb-Targets, icon.png auf 512 px (electron-builder rechnet daraus die Linux-Groessen herunter) und start.sh als Gegenstueck zu start.cmd. Getestet unter GNOME/Wayland: Fenster auf X11 an erwarteter Position, gemerkte Position wird wiederhergestellt, Tray registriert sich auf DBus, dump liest die Transkripte, beide Pakete bauen und das AppImage startet ohne Schalter. Co-Authored-By: Claude Opus 4.8 --- README.md | 96 +++++++++++++++++++++++++++++++++++++--- assets/icon.png | Bin 2348 -> 5169 bytes package.json | 34 +++++++++++++- src/main/autostart.js | 84 +++++++++++++++++++++++++++++++++++ src/main/index.js | 75 +++++++++++++++++++++++++------ src/settings/app.js | 3 ++ src/settings/index.html | 2 +- start.sh | 18 ++++++++ tools/make-icon.js | 5 ++- 9 files changed, 294 insertions(+), 23 deletions(-) create mode 100644 src/main/autostart.js create mode 100755 start.sh diff --git a/README.md b/README.md index 60111ee..706e0ae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Claude Live Dashboard -Ein rahmenloses Always-on-Top-Widget für Windows, das die Auslastung des +Ein rahmenloses Always-on-Top-Widget für Windows und Linux, das die Auslastung des Claude-Plans live anzeigt: 5-Stunden-Fenster, Wochenverbrauch, Reset-Countdown, Tokenverbrauch je Modell und die Hochrechnung auf das Fensterende. @@ -10,8 +10,12 @@ mitgeschnitten und standardmäßig keine Netzwerkverbindung aufgebaut. ## Start -Node.js wird benötigt. Ist es nicht im `PATH` (etwa bei nvm-windows ohne -aktive Version), genügt der Versionspfad: +Node.js (ab Version 20) wird benötigt. + +### Windows + +Ist Node nicht im `PATH` (etwa bei nvm-windows ohne aktive Version), genügt der +Versionspfad: ```powershell $env:PATH = "$env:APPDATA\nvm\v22.22.1;$env:PATH" @@ -19,9 +23,51 @@ npm install npm start ``` +Alternativ `start.cmd` doppelklicken. + Das Tray-Symbol landet unter Windows zunächst im Überlaufbereich hinter dem `^` in der Taskleiste; per Drag lässt es sich dauerhaft sichtbar machen. +### Linux (Ubuntu) + +```bash +sudo apt install nodejs npm # falls noch nicht vorhanden +./start.sh +``` + +`start.sh` installiert fehlende Abhängigkeiten selbst und startet Electron; +`npm install && npm start` tut dasselbe. + +Das Tray-Symbol braucht unter GNOME die Erweiterung **AppIndicator** — beim +Ubuntu-Desktop ist sie vorinstalliert und aktiv. Auf einer Minimalinstallation +oder unter anderen Oberflächen: + +```bash +sudo apt install gnome-shell-extension-appindicator +``` + +Ohne sie läuft das Widget weiter, nur das Symbol in der oberen Leiste fehlt. +Anders als unter Windows ist im StatusNotifierItem-Standard nicht festgelegt, +welcher Klick als Aktivierung zählt — verlässlich ist deshalb das Kontextmenü +(Rechtsklick) mit „Dashboard anzeigen“ als erstem Eintrag. + +**Wayland:** Ab Electron 36 wählt Chromium in einer Wayland-Sitzung von sich +aus Wayland — dort darf sich ein Fenster aber weder selbst positionieren noch +dauerhaft in den Vordergrund legen, und genau das ist der Kern dieses Widgets. +Die App startet sich deshalb unter Wayland einmalig selbst mit +`--ozone-platform=x11` neu und läuft über XWayland. Das geschieht automatisch, +es ist nichts einzustellen. + +Der Schalter *muss* auf der Kommandozeile stehen: Ozone legt die Plattform +fest, bevor das Main-Skript läuft — `app.commandLine.appendSwitch()` und +`ELECTRON_OZONE_PLATFORM_HINT` kommen beide zu spät. Wer die Automatik +umgehen will, setzt eine der beiden Größen von Hand; dann greift der Neustart +nicht mehr: + +```bash +ELECTRON_OZONE_PLATFORM_HINT=wayland ./start.sh # nativ Wayland, ohne Positionierung +``` + ## Bedienung | Aktion | Wirkung | @@ -157,6 +203,7 @@ src/main/oauth-usage.js Serverabfrage, gekapselt und ausfallsicher src/main/calibrate.js Kalibrierung gegen die echte Anzeige src/main/store.js state.json, config.json src/main/paths.js Speicherorte für Konfiguration und Zustand +src/main/autostart.js Anmeldestart je Betriebssystem src/preload.js contextBridge fürs Widget src/settings-preload.js contextBridge für den Einstellungsdialog src/renderer/ Widget-Oberfläche @@ -178,6 +225,12 @@ Zwei Details, die nicht offensichtlich sind: ## Release bauen +Gebaut wird jeweils auf dem Zielsystem — electron-builder kann `.deb` nicht +sinnvoll von Windows aus erzeugen und umgekehrt keine signierten `.exe` unter +Linux. + +### Windows + ```powershell npm run dist # Installer + portable Fassung nach release/ npm run pack # nur entpackt nach release/win-unpacked (schneller Test) @@ -201,16 +254,49 @@ Start einen SmartScreen-Hinweis („Der Computer wurde durch Windows geschützt Weitergabe an andere wäre ein Zertifikat nötig; für den Eigenbedarf ist es verzichtbar. +### Linux + +```bash +npm run dist:linux # AppImage + .deb nach release/ +npm run pack:linux # nur entpackt nach release/linux-unpacked +``` + +Ergebnis in `release/`: + +| Datei | Zweck | +|---|---| +| `Claude Live Dashboard-1.0.0-x64.AppImage` | läuft ohne Installation | +| `Claude Live Dashboard-1.0.0-amd64.deb` | Installation per `sudo apt install ./…deb` | + +Das AppImage muss einmal ausführbar gemacht werden (`chmod +x`) und braucht +`libfuse2`. Es trägt sich nicht selbst ins Anwendungsmenü ein — der Autostart +über das Tray-Menü funktioniert trotzdem, weil die App den Pfad des Abbilds aus +`APPIMAGE` liest und nicht den temporären Einhängepunkt. Das `.deb` bringt +Menüeintrag und Symbol mit und installiert nach `/opt/Claude Live Dashboard`. + +> **Ubuntu 24.04 und AppImage.** Seit 24.04 beschränkt AppArmor unprivilegierte +> User-Namespaces; die Chromium-Sandbox kann dann beim Start fehlschlagen +> (`SUID sandbox helper binary … is not configured correctly`). Zwei Auswege: +> das AppImage mit `--no-sandbox` starten, oder ein AppArmor-Profil unter +> `/etc/apparmor.d/` hinterlegen. Beim `.deb` tritt das nicht auf — dessen +> `postinst` setzt das SUID-Bit auf `chrome-sandbox`. + ### Konfiguration und Zustand Beides liegt im Benutzerprofil, nicht im Programmverzeichnis — dort wäre es nach der Installation im asar-Archiv und schreibgeschützt: ``` -%APPDATA%\Claude Live Dashboard\config.json -%APPDATA%\Claude Live Dashboard\state.json +Windows: %APPDATA%\Claude Live Dashboard\config.json + %APPDATA%\Claude Live Dashboard\state.json + +Linux: ~/.config/Claude Live Dashboard/config.json + ~/.config/Claude Live Dashboard/state.json ``` +Der Autostart ist kein Feld dieser Dateien: unter Windows steckt er in der +Registry, unter Linux in `~/.config/autostart/claude-live-dashboard.desktop`. + Beim ersten Start wird `config.json` aus der mitgelieferten Vorlage angelegt. Erreichbar über das Tray-Menü (*Konfiguration öffnen* / *Datenordner öffnen*). diff --git a/assets/icon.png b/assets/icon.png index 3ef818fa0fcfa243494363757ca42cfbcee87271..f2d9a48f9242d265fb48b2cf456d61226385f7db 100644 GIT binary patch literal 5169 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 diff --git a/package.json b/package.json index 956fc0b..2d4f038 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "Claude Live Dashboard", "version": "1.0.0", "private": true, - "description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows", + "description": "Always-on-Top Mini-Dashboard für Claude-Plan-Auslastung unter Windows und Linux", "main": "src/main/index.js", "scripts": { "start": "electron .", @@ -11,10 +11,14 @@ "calibrate": "node src/main/calibrate.js", "icons": "node tools/make-icon.js", "dist": "electron-builder --win", - "pack": "electron-builder --win --dir" + "pack": "electron-builder --win --dir", + "dist:linux": "electron-builder --linux", + "pack:linux": "electron-builder --linux --dir" }, "author": "", "license": "MIT", + "_homepage": "Platzhalter — nur nötig, weil das .deb-Paket ein Homepage-Feld verlangt.", + "homepage": "https://github.com/exilkieler/claude-live-dashboard", "devDependencies": { "electron": "^43.2.0", "electron-builder": "^25.1.8" @@ -66,6 +70,32 @@ }, "portable": { "artifactName": "${productName}-${version}-portable.${ext}" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": [ + "x64" + ] + }, + { + "target": "deb", + "arch": [ + "x64" + ] + } + ], + "icon": "assets/icon.png", + "executableName": "claude-live-dashboard", + "category": "Utility", + "maintainer": "Stefan ", + "synopsis": "Auslastung des Claude-Abos im Blick", + "description": "Rahmenloses Always-on-Top-Widget, das die Auslastung des Claude-Abos aus den lokalen Claude-Code-Transkripten liest.", + "desktop": { + "StartupWMClass": "claude-live-dashboard" + }, + "artifactName": "${productName}-${version}-${arch}.${ext}" } } } diff --git a/src/main/autostart.js b/src/main/autostart.js new file mode 100644 index 0000000..2aadb98 --- /dev/null +++ b/src/main/autostart.js @@ -0,0 +1,84 @@ +'use strict'; + +/** + * Autostart — die einzige Stelle, die weiß, wie die jeweilige Arbeitsumgebung + * einen Anmeldestart hinterlegt. + * + * Unter Windows und macOS erledigt Electron das selbst. Unter Linux gibt es + * `app.setLoginItemSettings()` nicht; dort ist der Autostart eine + * `.desktop`-Datei in `~/.config/autostart`, deren bloße Existenz den Start + * auslöst. + */ + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { app } = require('electron'); + +const IS_LINUX = process.platform === 'linux'; + +const AUTOSTART_DIR = path.join( + process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), + 'autostart', +); +const DESKTOP_FILE = path.join(AUTOSTART_DIR, 'claude-live-dashboard.desktop'); + +/** + * Der Befehl, mit dem sich die Anwendung selbst wieder startet. + * + * Ein AppImage entpackt sich beim Start in ein temporäres Verzeichnis — + * `process.execPath` zeigt dann dorthin und wäre beim nächsten Anmelden + * verschwunden. Den Pfad zum Abbild selbst liefert nur `APPIMAGE`. + */ +function launchCommand() { + if (process.env.APPIMAGE) return `"${process.env.APPIMAGE}"`; + if (!app.isPackaged) return `"${process.execPath}" "${app.getAppPath()}"`; + return `"${process.execPath}"`; +} + +function desktopEntry() { + return [ + '[Desktop Entry]', + 'Type=Application', + 'Version=1.0', + 'Name=Claude Live Dashboard', + 'Comment=Auslastung des Claude-Abos im Blick', + `Exec=${launchCommand()}`, + 'Icon=claude-live-dashboard', + 'Terminal=false', + 'X-GNOME-Autostart-enabled=true', + '', + ].join('\n'); +} + +/** @returns {boolean} true, wenn die Anwendung beim Anmelden startet */ +function getAutostart() { + if (!IS_LINUX) return app.getLoginItemSettings().openAtLogin; + return fs.existsSync(DESKTOP_FILE); +} + +/** + * Schaltet den Anmeldestart ein oder aus. + * @param {boolean} enabled + */ +function setAutostart(enabled) { + if (!IS_LINUX) { + app.setLoginItemSettings({ openAtLogin: enabled, args: [] }); + return; + } + + try { + if (enabled) { + fs.mkdirSync(AUTOSTART_DIR, { recursive: true }); + fs.writeFileSync(DESKTOP_FILE, desktopEntry(), 'utf8'); + } else { + fs.rmSync(DESKTOP_FILE, { force: true }); + } + } catch (err) { + // Ein fehlgeschlagener Autostart ist kein Grund, die laufende Anwendung + // zu stören — der Haken im Menü zeigt beim nächsten Aufbau den echten Stand. + console.warn(`[autostart] konnte nicht gesetzt werden: ${err.message}`); + } +} + +module.exports = { getAutostart, setAutostart, DESKTOP_FILE }; diff --git a/src/main/index.js b/src/main/index.js index 823a740..f90766c 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -12,6 +12,9 @@ const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeImage, shell } = const { Store, loadConfig } = require('./store'); const { Collector } = require('./collector'); const { CONFIG_FILE, STATE_FILE, USER_DATA_DIR, ensureUserConfig } = require('./paths'); +const { getAutostart, setAutostart: applyAutostart } = require('./autostart'); + +const IS_LINUX = process.platform === 'linux'; const COMPACT_HEIGHT = 352; const EXPANDED_HEIGHT = 640; @@ -30,11 +33,59 @@ let collector = null; let store = null; let config = {}; +/** + * Erzwingt XWayland, sobald das Widget in einer Wayland-Sitzung startet. + * + * Electron wählt seit Version 36 von sich aus Wayland. Dort darf sich ein + * Fenster aber weder selbst positionieren noch dauerhaft in den Vordergrund + * legen — beides ist der Kern dieses Widgets. Über XWayland funktioniert es. + * + * Der Schalter muss auf der Kommandozeile stehen: Ozone wählt die Plattform, + * bevor dieses Skript läuft, deshalb kommen `appendSwitch()` und + * `ELECTRON_OZONE_PLATFORM_HINT` beide zu spät. Bleibt ein Neustart mit + * ergänztem Argument. Das Argument ist zugleich die Abbruchbedingung — im + * zweiten Durchlauf steht es in `argv` und der Zweig greift nicht mehr. + * + * Muss vor `requestSingleInstanceLock()` stehen, damit die verworfene Instanz + * die Sperre gar nicht erst nimmt. + */ +function forceX11IfWayland() { + if (!IS_LINUX || !process.env.WAYLAND_DISPLAY) return false; + if (process.env.ELECTRON_OZONE_PLATFORM_HINT) return false; // ausdrücklich übersteuert + if (process.argv.some((a) => a.startsWith('--ozone-platform'))) return false; + + const args = process.argv.slice(1).concat('--ozone-platform=x11'); + + // Ein AppImage hängt sich beim Start unter /tmp ein und wirft die Einhängung + // beim Beenden wieder weg. `process.execPath` zeigt dorthin und wäre für den + // Nachfolger schon verschwunden — neu gestartet werden muss das Abbild. + if (process.env.APPIMAGE) app.relaunch({ execPath: process.env.APPIMAGE, args }); + else app.relaunch({ args }); + + app.exit(0); + return true; +} + +if (!forceX11IfWayland()) { + // Ohne diesen Schalter zeichnet Chromium unter Linux hinter das rahmenlose + // Fenster einen schwarzen Kasten statt echter Transparenz. + if (IS_LINUX) app.commandLine.appendSwitch('enable-transparent-visuals'); + + bootstrap(); +} + /** Verhindert, dass mehrere Instanzen dieselben Zustandsdateien beschreiben. */ -if (!app.requestSingleInstanceLock()) { - app.quit(); -} else { +function bootstrap() { + if (!app.requestSingleInstanceLock()) { + app.quit(); + return; + } app.on('second-instance', () => showWindow()); + + main().catch((err) => { + console.error(err); + app.quit(); + }); } /** @@ -142,13 +193,13 @@ function setClickThrough(enabled) { } function setAutostart(enabled) { - app.setLoginItemSettings({ openAtLogin: enabled, args: [] }); + applyAutostart(enabled); buildTrayMenu(); } function buildTrayMenu() { if (!tray) return; - const autostart = app.getLoginItemSettings().openAtLogin; + const autostart = getAutostart(); tray.setContextMenu( Menu.buildFromTemplate([ @@ -171,7 +222,7 @@ function buildTrayMenu() { click: (item) => setClickThrough(item.checked), }, { - label: 'Mit Windows starten', + label: IS_LINUX ? 'Mit dem System starten' : 'Mit Windows starten', type: 'checkbox', checked: autostart, click: (item) => setAutostart(item.checked), @@ -186,7 +237,9 @@ function buildTrayMenu() { } function createTray() { - const icon = nativeImage.createFromPath(path.join(ROOT, 'assets', 'tray.png')); + // In der GNOME-Leiste wirkt das 16-px-Symbol ausgefranst — dort das größere. + const file = IS_LINUX ? 'tray@32.png' : 'tray.png'; + const icon = nativeImage.createFromPath(path.join(ROOT, 'assets', file)); tray = new Tray(icon); tray.setToolTip('Claude Live Dashboard'); tray.on('click', toggleWindow); @@ -297,7 +350,8 @@ function registerIpc() { config, meta: { configPath: CONFIG_FILE, - autostart: app.getLoginItemSettings().openAtLogin, + autostart: getAutostart(), + platform: process.platform, }, })); @@ -366,8 +420,3 @@ app.on('window-all-closed', () => {}); app.on('before-quit', () => { if (collector) collector.stop(); }); - -main().catch((err) => { - console.error(err); - app.quit(); -}); diff --git a/src/settings/app.js b/src/settings/app.js index 46cfc61..56b556d 100644 --- a/src/settings/app.js +++ b/src/settings/app.js @@ -20,6 +20,7 @@ const el = { opacityOut: $('opacity-out'), clickThrough: $('clickThrough'), autostart: $('autostart'), + autostartLabel: $('autostart-label'), oauthEnabled: $('oauthEnabled'), oauthEndpoint: $('oauthEndpoint'), @@ -80,6 +81,8 @@ function fill(config, meta) { el.opacityOut.textContent = `${opacity} %`; el.clickThrough.checked = Boolean(config.clickThrough); el.autostart.checked = Boolean(meta.autostart); + el.autostartLabel.textContent = + meta.platform === 'win32' ? 'Mit Windows starten' : 'Mit dem System starten'; const oauth = config.oauth || {}; el.oauthEnabled.checked = oauth.enabled !== false; diff --git a/src/settings/index.html b/src/settings/index.html index 80f0b0b..035ebb0 100644 --- a/src/settings/index.html +++ b/src/settings/index.html @@ -36,7 +36,7 @@
- +
Wird sofort übernommen, unabhängig vom Speichern. diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..9df6249 --- /dev/null +++ b/start.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Startet das Dashboard aus dem Quellcode. Gegenstück zu start.cmd; unter +# Ubuntu liegt Node im PATH, deshalb genügt die Abhängigkeitsprüfung. + +set -euo pipefail +cd "$(dirname "$0")" + +if ! command -v node >/dev/null 2>&1; then + echo "Node wurde nicht gefunden. Installiere es z.B. mit: sudo apt install nodejs npm" + exit 1 +fi + +if [ ! -d node_modules/electron ]; then + echo "Abhaengigkeiten werden installiert..." + npm install +fi + +exec node_modules/.bin/electron . "$@" diff --git a/tools/make-icon.js b/tools/make-icon.js index ecbb589..317e2b3 100644 --- a/tools/make-icon.js +++ b/tools/make-icon.js @@ -116,9 +116,10 @@ function main() { console.log(`geschrieben: ${file}`); } - // Fenster- und Anwendungssymbol + // Fenster- und Anwendungssymbol. 512 px, weil electron-builder daraus die + // Linux-Symbolgrößen herunterrechnet und mindestens 256 px verlangt. const file = path.join(OUT_DIR, 'icon.png'); - fs.writeFileSync(file, makePng(256, ringShader(256, orange))); + fs.writeFileSync(file, makePng(512, ringShader(512, orange))); console.log(`geschrieben: ${file}`); }