Linux-Fassung (Ubuntu) ergaenzen

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:52:08 +02:00
co-authored by Claude Opus 4.8
parent 868f31fa9a
commit c1b899ec8b
9 changed files with 294 additions and 23 deletions
+84
View File
@@ -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 };
+62 -13
View File
@@ -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();
});
+3
View File
@@ -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;
+1 -1
View File
@@ -36,7 +36,7 @@
</div>
<div class="row">
<label for="autostart">Mit Windows starten</label>
<label for="autostart" id="autostart-label">Automatisch starten</label>
<div class="control">
<input type="checkbox" id="autostart" />
<span class="hint">Wird sofort übernommen, unabhängig vom Speichern.</span>