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>
127 lines
3.5 KiB
JavaScript
127 lines
3.5 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Erzeugt die Tray-Symbole als PNG — ohne Bildbibliothek, nur mit zlib aus der
|
||
* Standardbibliothek. Einmalig aufzurufen:
|
||
*
|
||
* node tools/make-icon.js
|
||
*
|
||
* Motiv: ein Ring mit einem Sektor, der den Füllstand andeutet.
|
||
*/
|
||
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const zlib = require('node:zlib');
|
||
|
||
const OUT_DIR = path.join(__dirname, '..', 'assets');
|
||
|
||
/** CRC32 nach PNG-Spezifikation. */
|
||
const CRC_TABLE = (() => {
|
||
const table = new Int32Array(256);
|
||
for (let n = 0; n < 256; n++) {
|
||
let c = n;
|
||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||
table[n] = c;
|
||
}
|
||
return table;
|
||
})();
|
||
|
||
function crc32(buf) {
|
||
let c = -1;
|
||
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||
return (c ^ -1) >>> 0;
|
||
}
|
||
|
||
function chunk(type, data) {
|
||
const out = Buffer.alloc(data.length + 12);
|
||
out.writeUInt32BE(data.length, 0);
|
||
out.write(type, 4, 'ascii');
|
||
data.copy(out, 8);
|
||
const crcInput = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
||
out.writeUInt32BE(crc32(crcInput), data.length + 8);
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* @param {number} size Kantenlänge
|
||
* @param {(x: number, y: number) => [number, number, number, number]} shade RGBA je Pixel
|
||
*/
|
||
function makePng(size, shade) {
|
||
// Scanlines mit Filterbyte 0 je Zeile
|
||
const raw = Buffer.alloc(size * (size * 4 + 1));
|
||
let p = 0;
|
||
for (let y = 0; y < size; y++) {
|
||
raw[p++] = 0;
|
||
for (let x = 0; x < size; x++) {
|
||
const [r, g, b, a] = shade(x, y);
|
||
raw[p++] = r;
|
||
raw[p++] = g;
|
||
raw[p++] = b;
|
||
raw[p++] = a;
|
||
}
|
||
}
|
||
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(size, 0);
|
||
ihdr.writeUInt32BE(size, 4);
|
||
ihdr[8] = 8; // Bittiefe
|
||
ihdr[9] = 6; // Farbtyp RGBA
|
||
ihdr[10] = 0;
|
||
ihdr[11] = 0;
|
||
ihdr[12] = 0;
|
||
|
||
return Buffer.concat([
|
||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||
chunk('IHDR', ihdr),
|
||
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
||
chunk('IEND', Buffer.alloc(0)),
|
||
]);
|
||
}
|
||
|
||
/** Ring mit Füllsektor; kantengeglättet über 4×4-Supersampling. */
|
||
function ringShader(size, color, fillFraction = 0.68) {
|
||
const c = (size - 1) / 2;
|
||
const rOuter = size * 0.46;
|
||
const rInner = size * 0.28;
|
||
const SS = 4;
|
||
|
||
return (x, y) => {
|
||
let hits = 0;
|
||
for (let sy = 0; sy < SS; sy++) {
|
||
for (let sx = 0; sx < SS; sx++) {
|
||
const px = x + (sx + 0.5) / SS - 0.5;
|
||
const py = y + (sy + 0.5) / SS - 0.5;
|
||
const dx = px - c;
|
||
const dy = py - c;
|
||
const dist = Math.hypot(dx, dy);
|
||
if (dist > rOuter || dist < rInner) continue;
|
||
// Winkel ab 12 Uhr im Uhrzeigersinn
|
||
let ang = Math.atan2(dx, -dy);
|
||
if (ang < 0) ang += Math.PI * 2;
|
||
if (ang <= fillFraction * Math.PI * 2) hits++;
|
||
}
|
||
}
|
||
const a = Math.round((hits / (SS * SS)) * 255);
|
||
return [color[0], color[1], color[2], a];
|
||
};
|
||
}
|
||
|
||
function main() {
|
||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
const orange = [217, 89, 38]; // Slot 2 der Palette
|
||
|
||
for (const size of [16, 32]) {
|
||
const file = path.join(OUT_DIR, `tray${size === 16 ? '' : `@${size}`}.png`);
|
||
fs.writeFileSync(file, makePng(size, ringShader(size, orange)));
|
||
console.log(`geschrieben: ${file}`);
|
||
}
|
||
|
||
// 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(512, ringShader(512, orange)));
|
||
console.log(`geschrieben: ${file}`);
|
||
}
|
||
|
||
main();
|