theseus/nsis/make-icons.mjs

143 lines
6.2 KiB
JavaScript
Raw Permalink Normal View History

Ship Theseus 0.2.3 3abaeff7 (new brand icon + multi-panel picker + Startpage default) Setup 3abaeff73afcecc9e4f05f749765e168a97a377890211dbae4345654c84ce2f1 Portable 71017563d6ba107cb25e24be240abcb78e34491d8f7aa42e095421edbc482a85 Brand: the red-N compass from theseus.x is now the taskbar / titlebar / File Explorer icon everywhere. Source SVG lives at site-theseus-x/assets/favicon.svg so brand + browser icon stay in sync. nsis/make-icons.mjs renders it to build/icon.png (512x512) and build/icon.ico (multi-resolution: 16/24/32/48/64/128/256). Wired into package.json: build.win.icon, build.nsis.installerIcon + uninstaller + header; the ico ships as an extraResource so main.js's BrowserWindow also uses it at runtime. Icons live in build/ which is gitignored - run `node nsis/make-icons.mjs` when the SVG changes. Sidebar: multi-panel picker strip. When 2+ extensions register sidebar panels, sidebar-preload.js injects a 32-px tab strip at the top of every panel's document. Click a tab -> ipcRenderer sidebar-open -> loadFile switch. Preload also injects box-sizing:border-box + a 33-px body padding so height:100% panels don't overflow. Solo-panel case is unchanged (strip only appears when panels.length >= 2). Search: Startpage is the new default. Both the default id and the enabled-list ordering put it first. DDG stays enabled by default too. Deploy: scp installers + manifest + tools/ + releases/ to VPS, sia-upload of both trees, verified HEAD 200 and manifest 0.2.3. Icons regenerated from theseus.x's favicon.svg; the .svg itself shipped in the 0.1.x window when the theseus.x site went live.
2026-08-31 16:32:16 +02:00
// One-shot script: render site-theseus-x/assets/favicon.svg to the icon
// files electron-builder wants — a 512x512 PNG (main icon) and a multi-
// resolution .ico (Windows taskbar / installer / shortcut icon).
//
// Not in the runtime app bundle; run manually when the brand changes:
// node nsis/make-icons.mjs
// Outputs build/icon.png and build/icon.ico (both gitignored regenerated).
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import sharp from "sharp";
import pngToIco from "png-to-ico";
Ship Theseus 0.3.2 09331b2f (tab context menu + branded installer) Setup 09331b2fd9ccf136e2183b7cd85354cfd56e2ed50260b7aadeed63c7ea450251 Portable 21752d0fc85fb39ec1e65192920461e9ae395a22d9a68abd27f12e638d0fdd07 Right-click a tab: floating context menu with Reload, Duplicate, Group (submenu: None / Red / Orange / Yellow / Green / Cyan / Blue / Purple), Add to Bookmarks, Mute (also Unmute; 🔇 shows next to the title when muted), Close. Menus close on outside click or Escape. Group state is per-tab. A grouped tab shows a colored dot before the title and a matching 2-px accent stripe on the top edge, so a cluster of same-group tabs reads visually. Palette is drawn from existing provenance colors (err/warn/acid/srv/sia/blue). Backend IPCs are all tab-scoped (not "active tab"): tab-reload, tab-duplicate, tab-mute (toggle or explicit boolean), tab-group, tab-bookmark. emitTabs payload gains muted, group, and url so the menu can read current state. Installer wizard branding: 164×314 sidebar BMP with the compass mark centered + "Theseus / NAVIGATOR" wordmark under it, plus a 150×57 top-strip header with a mini compass on the right. Sharp can't write BMP directly (only png/webp/etc), so nsis/make-icons.mjs renders raw RGB via sharp and wraps it in a hand-rolled 24-bit uncompressed BMP header. Uninstaller reuses the same sidebar. Silent-install fix: nsis/installer.nsh's AriadnePageCreate now checks IfSilent BEFORE touching nsDialogs::Create. In /S mode the flag is zeroed and the function returns cleanly, so the installer no longer hangs waiting for a page it will never draw. This is why 0.3.2 needed two builds — the first hung on /S install; the fixed hash is the one that ships. Deployed: scp + sia-upload of both trees. Verified VPS hash matches local 09331b2f. Fresh /S install to D:\Program Files\Theseus Navigator\ placed 0.3.2 with the correct HKCU Uninstall registry entry.
2026-08-31 19:30:30 +02:00
// Minimal 24-bit uncompressed BMP writer. Sharp can produce RGB raw pixels
// but not BMP output; NSIS's MUI wizard branding needs BMP.
function writeBmp24(outPath, width, height, rgb) {
const rowBytes = width * 3;
const rowPad = (4 - (rowBytes % 4)) % 4;
const stride = rowBytes + rowPad;
const pixelBytes = stride * height;
const fileSize = 14 + 40 + pixelBytes;
const buf = Buffer.alloc(fileSize);
// BITMAPFILEHEADER
buf.write("BM", 0);
buf.writeUInt32LE(fileSize, 2);
buf.writeUInt32LE(0, 6);
buf.writeUInt32LE(54, 10);
// BITMAPINFOHEADER
buf.writeUInt32LE(40, 14);
buf.writeInt32LE(width, 18);
buf.writeInt32LE(height, 22);
buf.writeUInt16LE(1, 26);
buf.writeUInt16LE(24, 28);
buf.writeUInt32LE(0, 30);
buf.writeUInt32LE(pixelBytes, 34);
buf.writeInt32LE(2835, 38); // 72 DPI
buf.writeInt32LE(2835, 42);
buf.writeUInt32LE(0, 46);
buf.writeUInt32LE(0, 50);
// Pixel rows bottom-up, BGR order.
for (let y = 0; y < height; y++) {
const srcRow = (height - 1 - y) * width * 3;
const dstRow = 54 + y * stride;
for (let x = 0; x < width; x++) {
buf[dstRow + x * 3] = rgb[srcRow + x * 3 + 2]; // B
buf[dstRow + x * 3 + 1] = rgb[srcRow + x * 3 + 1]; // G
buf[dstRow + x * 3 + 2] = rgb[srcRow + x * 3]; // R
}
// trailing padding bytes are already 0-filled by alloc()
}
fs.writeFileSync(outPath, buf);
}
Ship Theseus 0.2.3 3abaeff7 (new brand icon + multi-panel picker + Startpage default) Setup 3abaeff73afcecc9e4f05f749765e168a97a377890211dbae4345654c84ce2f1 Portable 71017563d6ba107cb25e24be240abcb78e34491d8f7aa42e095421edbc482a85 Brand: the red-N compass from theseus.x is now the taskbar / titlebar / File Explorer icon everywhere. Source SVG lives at site-theseus-x/assets/favicon.svg so brand + browser icon stay in sync. nsis/make-icons.mjs renders it to build/icon.png (512x512) and build/icon.ico (multi-resolution: 16/24/32/48/64/128/256). Wired into package.json: build.win.icon, build.nsis.installerIcon + uninstaller + header; the ico ships as an extraResource so main.js's BrowserWindow also uses it at runtime. Icons live in build/ which is gitignored - run `node nsis/make-icons.mjs` when the SVG changes. Sidebar: multi-panel picker strip. When 2+ extensions register sidebar panels, sidebar-preload.js injects a 32-px tab strip at the top of every panel's document. Click a tab -> ipcRenderer sidebar-open -> loadFile switch. Preload also injects box-sizing:border-box + a 33-px body padding so height:100% panels don't overflow. Solo-panel case is unchanged (strip only appears when panels.length >= 2). Search: Startpage is the new default. Both the default id and the enabled-list ordering put it first. DDG stays enabled by default too. Deploy: scp installers + manifest + tools/ + releases/ to VPS, sia-upload of both trees, verified HEAD 200 and manifest 0.2.3. Icons regenerated from theseus.x's favicon.svg; the .svg itself shipped in the 0.1.x window when the theseus.x site went live.
2026-08-31 16:32:16 +02:00
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// SVG source lives in the theseus.x standalone site so the browser icon
// and the site brand stay in lockstep.
const SVG = path.join(__dirname, "..", "..", "site-theseus-x", "assets", "favicon.svg");
// Icons drop into build/, which is gitignored (regenerated artifacts).
// electron-builder picks them up from there via package.json.
const OUT_DIR = path.join(__dirname, "..", "build");
Ship Theseus 0.3.2 09331b2f (tab context menu + branded installer) Setup 09331b2fd9ccf136e2183b7cd85354cfd56e2ed50260b7aadeed63c7ea450251 Portable 21752d0fc85fb39ec1e65192920461e9ae395a22d9a68abd27f12e638d0fdd07 Right-click a tab: floating context menu with Reload, Duplicate, Group (submenu: None / Red / Orange / Yellow / Green / Cyan / Blue / Purple), Add to Bookmarks, Mute (also Unmute; 🔇 shows next to the title when muted), Close. Menus close on outside click or Escape. Group state is per-tab. A grouped tab shows a colored dot before the title and a matching 2-px accent stripe on the top edge, so a cluster of same-group tabs reads visually. Palette is drawn from existing provenance colors (err/warn/acid/srv/sia/blue). Backend IPCs are all tab-scoped (not "active tab"): tab-reload, tab-duplicate, tab-mute (toggle or explicit boolean), tab-group, tab-bookmark. emitTabs payload gains muted, group, and url so the menu can read current state. Installer wizard branding: 164×314 sidebar BMP with the compass mark centered + "Theseus / NAVIGATOR" wordmark under it, plus a 150×57 top-strip header with a mini compass on the right. Sharp can't write BMP directly (only png/webp/etc), so nsis/make-icons.mjs renders raw RGB via sharp and wraps it in a hand-rolled 24-bit uncompressed BMP header. Uninstaller reuses the same sidebar. Silent-install fix: nsis/installer.nsh's AriadnePageCreate now checks IfSilent BEFORE touching nsDialogs::Create. In /S mode the flag is zeroed and the function returns cleanly, so the installer no longer hangs waiting for a page it will never draw. This is why 0.3.2 needed two builds — the first hung on /S install; the fixed hash is the one that ships. Deployed: scp + sia-upload of both trees. Verified VPS hash matches local 09331b2f. Fresh /S install to D:\Program Files\Theseus Navigator\ placed 0.3.2 with the correct HKCU Uninstall registry entry.
2026-08-31 19:30:30 +02:00
const INSTALLER_SIDEBAR = path.join(OUT_DIR, "installerSidebar.bmp");
const UNINSTALLER_SIDEBAR = path.join(OUT_DIR, "uninstallerSidebar.bmp");
const INSTALLER_HEADER = path.join(OUT_DIR, "installerHeader.bmp");
Ship Theseus 0.2.3 3abaeff7 (new brand icon + multi-panel picker + Startpage default) Setup 3abaeff73afcecc9e4f05f749765e168a97a377890211dbae4345654c84ce2f1 Portable 71017563d6ba107cb25e24be240abcb78e34491d8f7aa42e095421edbc482a85 Brand: the red-N compass from theseus.x is now the taskbar / titlebar / File Explorer icon everywhere. Source SVG lives at site-theseus-x/assets/favicon.svg so brand + browser icon stay in sync. nsis/make-icons.mjs renders it to build/icon.png (512x512) and build/icon.ico (multi-resolution: 16/24/32/48/64/128/256). Wired into package.json: build.win.icon, build.nsis.installerIcon + uninstaller + header; the ico ships as an extraResource so main.js's BrowserWindow also uses it at runtime. Icons live in build/ which is gitignored - run `node nsis/make-icons.mjs` when the SVG changes. Sidebar: multi-panel picker strip. When 2+ extensions register sidebar panels, sidebar-preload.js injects a 32-px tab strip at the top of every panel's document. Click a tab -> ipcRenderer sidebar-open -> loadFile switch. Preload also injects box-sizing:border-box + a 33-px body padding so height:100% panels don't overflow. Solo-panel case is unchanged (strip only appears when panels.length >= 2). Search: Startpage is the new default. Both the default id and the enabled-list ordering put it first. DDG stays enabled by default too. Deploy: scp installers + manifest + tools/ + releases/ to VPS, sia-upload of both trees, verified HEAD 200 and manifest 0.2.3. Icons regenerated from theseus.x's favicon.svg; the .svg itself shipped in the 0.1.x window when the theseus.x site went live.
2026-08-31 16:32:16 +02:00
const PNG_512 = path.join(OUT_DIR, "icon.png");
const ICO = path.join(OUT_DIR, "icon.ico");
const TMP_DIR = path.join(OUT_DIR, ".icon-tmp");
const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
if (!fs.existsSync(SVG)) throw new Error("SVG not found at " + SVG);
console.log("source:", SVG);
// Main 512x512 PNG for macOS / linux / electron-builder auto-derived Windows.
await sharp(SVG).resize(512, 512).png().toFile(PNG_512);
console.log("wrote", PNG_512);
// Write individual sized PNGs to a temp dir, then feed their paths to
// png-to-ico so it preserves each as a distinct image inside the .ico.
fs.mkdirSync(TMP_DIR, { recursive: true });
const paths = [];
for (const size of ICO_SIZES) {
const p = path.join(TMP_DIR, `icon-${size}.png`);
await sharp(SVG).resize(size, size).png().toFile(p);
paths.push(p);
}
const ico = await pngToIco(paths);
fs.writeFileSync(ICO, ico);
console.log("wrote", ICO, `(${ICO_SIZES.length} sizes)`);
// Clean up the temp PNGs.
for (const p of paths) fs.unlinkSync(p);
fs.rmdirSync(TMP_DIR);
Ship Theseus 0.3.2 09331b2f (tab context menu + branded installer) Setup 09331b2fd9ccf136e2183b7cd85354cfd56e2ed50260b7aadeed63c7ea450251 Portable 21752d0fc85fb39ec1e65192920461e9ae395a22d9a68abd27f12e638d0fdd07 Right-click a tab: floating context menu with Reload, Duplicate, Group (submenu: None / Red / Orange / Yellow / Green / Cyan / Blue / Purple), Add to Bookmarks, Mute (also Unmute; 🔇 shows next to the title when muted), Close. Menus close on outside click or Escape. Group state is per-tab. A grouped tab shows a colored dot before the title and a matching 2-px accent stripe on the top edge, so a cluster of same-group tabs reads visually. Palette is drawn from existing provenance colors (err/warn/acid/srv/sia/blue). Backend IPCs are all tab-scoped (not "active tab"): tab-reload, tab-duplicate, tab-mute (toggle or explicit boolean), tab-group, tab-bookmark. emitTabs payload gains muted, group, and url so the menu can read current state. Installer wizard branding: 164×314 sidebar BMP with the compass mark centered + "Theseus / NAVIGATOR" wordmark under it, plus a 150×57 top-strip header with a mini compass on the right. Sharp can't write BMP directly (only png/webp/etc), so nsis/make-icons.mjs renders raw RGB via sharp and wraps it in a hand-rolled 24-bit uncompressed BMP header. Uninstaller reuses the same sidebar. Silent-install fix: nsis/installer.nsh's AriadnePageCreate now checks IfSilent BEFORE touching nsDialogs::Create. In /S mode the flag is zeroed and the function returns cleanly, so the installer no longer hangs waiting for a page it will never draw. This is why 0.3.2 needed two builds — the first hung on /S install; the fixed hash is the one that ships. Deployed: scp + sia-upload of both trees. Verified VPS hash matches local 09331b2f. Fresh /S install to D:\Program Files\Theseus Navigator\ placed 0.3.2 with the correct HKCU Uninstall registry entry.
2026-08-31 19:30:30 +02:00
// ---- NSIS wizard branding ----------------------------------------------
// MUI2's Welcome/Finish sidebar is 164×314 BMP (no alpha). Compose the
// compass mark centered on the Silent Mode dark background so the wizard
// stops shouting "default Electron installer" at the user.
async function makeSidebar(outFile, W, H, markSize) {
const bg = { r: 11, g: 14, b: 20, alpha: 1 };
const canvas = sharp({
create: { width: W, height: H, channels: 3, background: bg },
});
const markX = Math.floor((W - markSize) / 2);
const markY = Math.floor((H - markSize) / 2) - Math.floor(H * 0.08); // slight nudge up
const markPng = await sharp(SVG).resize(markSize, markSize).png().toBuffer();
// "Theseus Navigator" wordmark rendered as SVG text so we don't need a font file.
const wordmarkSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W - 20}" height="60">
<text x="50%" y="24" text-anchor="middle" fill="#e7eaf1"
font-family="Segoe UI, Arial, sans-serif" font-weight="600" font-size="18">Theseus</text>
<text x="50%" y="48" text-anchor="middle" fill="#d6ff3d"
font-family="Segoe UI, Arial, sans-serif" font-weight="600" font-size="14" letter-spacing="1">NAVIGATOR</text>
</svg>`;
const wordmarkPng = await sharp(Buffer.from(wordmarkSvg)).png().toBuffer();
await canvas
.composite([
{ input: markPng, top: markY, left: markX },
{ input: wordmarkPng, top: markY + markSize + 14, left: 10 },
])
.toColorspace("srgb")
.raw({ depth: "uchar" })
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => writeBmp24(outFile, info.width, info.height, data));
console.log("wrote", outFile);
}
// NSIS MUI2 sidebar: 164x314. Uninstaller reuses the same asset.
await makeSidebar(INSTALLER_SIDEBAR, 164, 314, 96);
fs.copyFileSync(INSTALLER_SIDEBAR, UNINSTALLER_SIDEBAR);
console.log("wrote", UNINSTALLER_SIDEBAR);
// NSIS header (top strip, 150x57) — small compass on dark, right-aligned so
// it doesn't compete with the page title on the left.
async function makeHeader(outFile) {
const W = 150, H = 57;
const bg = { r: 11, g: 14, b: 20, alpha: 1 };
const mark = await sharp(SVG).resize(40, 40).png().toBuffer();
await sharp({ create: { width: W, height: H, channels: 3, background: bg } })
.composite([{ input: mark, top: 8, left: W - 48 }])
.toColorspace("srgb")
.raw({ depth: "uchar" })
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => writeBmp24(outFile, info.width, info.height, data));
console.log("wrote", outFile);
}
await makeHeader(INSTALLER_HEADER);