Rename project to trailhead-path-bookmarker

- npm name: trailhead-path-bookmarker (package.json + lockfile)
- electron-builder appId: com.dbeuttel.trailhead-path-bookmarker
- AppUserModelId in main.js matches the new appId
- README title updated; userData path now %APPDATA%\Trailhead Path Bookmarker

Icon generator now emits both sizes:
- assets/tray-icon.png (32x32) for the system tray
- assets/app-icon.png (256x256) for electron-builder's win.icon, which
  rejects PNGs smaller than 256
This commit is contained in:
David
2026-04-29 10:27:25 -04:00
parent 77c06d664b
commit 704e172da9
7 changed files with 91 additions and 65 deletions
+3 -3
View File
@@ -1,4 +1,4 @@
# Folder Bookmark Tray
# Trailhead Path Bookmarker
A Windows system-tray app for bookmarking folder paths with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).
@@ -41,7 +41,7 @@ npm run icon # regenerate assets/tray-icon.png
Persisted to `app.getPath('userData')/config.json`. On Windows that's:
```
%APPDATA%\Folder Bookmark Tray\config.json
%APPDATA%\Trailhead Path Bookmarker\config.json
```
Schema:
@@ -78,7 +78,7 @@ assets/tray-icon.png orange folder pictogram, 32×32
## Stack
Electron 33, React 18, Vite 5. NSIS installer via `electron-builder` (`appId: com.dbeuttel.folder-bookmark-tray`). No native deps.
Electron 33, React 18, Vite 5. NSIS installer via `electron-builder` (`appId: com.dbeuttel.trailhead-path-bookmarker`). No native deps.
## Non-goals
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 159 B

+1 -1
View File
@@ -750,7 +750,7 @@ ipcMain.handle('set-popup-width', (_event, w) => {
app.whenReady().then(() => {
if (process.platform === 'win32') {
app.setAppUserModelId('com.dbeuttel.folder-bookmark-tray');
app.setAppUserModelId('com.dbeuttel.trailhead-path-bookmarker');
}
migrateConfigIfNeeded();
pinned = !!readConfig().pinned;
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "folder-bookmark-tray",
"name": "trailhead-path-bookmarker",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "folder-bookmark-tray",
"name": "trailhead-path-bookmarker",
"version": "0.1.0",
"hasInstallScript": true,
"license": "MIT",
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "folder-bookmark-tray",
"name": "trailhead-path-bookmarker",
"version": "0.1.0",
"description": "Windows system tray app for bookmarking folders with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).",
"main": "electron/main.js",
@@ -28,7 +28,7 @@
"wait-on": "^8.0.1"
},
"build": {
"appId": "com.dbeuttel.folder-bookmark-tray",
"appId": "com.dbeuttel.trailhead-path-bookmarker",
"productName": "Trailhead Path Bookmarker",
"files": [
"electron/**/*",
@@ -37,7 +37,7 @@
],
"win": {
"target": "nsis",
"icon": "assets/tray-icon.png"
"icon": "assets/app-icon.png"
},
"nsis": {
"oneClick": false,
+76 -50
View File
@@ -1,66 +1,85 @@
// Generates assets/tray-icon.png — Trailhead branding: a bookmark ribbon
// (vertical rectangle with a V-cut at the bottom — the universal "save"
// glyph) carrying a mountain peak motif inside it. The ribbon silhouette
// communicates the app's *function* (bookmarking); the inner peak ties to
// the Trailhead theme.
// Strokes are kept >=2 source pixels wide so the silhouette survives the
// downscale to 16-24px Windows tray sizes.
// Generates two PNGs from the same procedural Trailhead glyph:
// assets/tray-icon.png — 32x32 (Windows tray)
// assets/app-icon.png — 256x256 (electron-builder requires >=256 for the
// win.icon used in the installer + .exe resource)
//
// Glyph: a vertical bookmark ribbon (rectangle with a V-cut bottom — the
// universal "save" mark) carrying a cream mountain peak inside it.
// Geometry is expressed as fractions of the canvas so a single set of rules
// renders cleanly at both sizes.
// No external image deps; raw PNG bytes via zlib.
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const SIZE = 32;
const OUT = path.join(__dirname, '..', 'assets', 'tray-icon.png');
const ORANGE = [217, 119, 87, 255]; // #d97757 — ribbon fill
const ORANGE_DARK = [165, 82, 56, 255]; // shadow on right edge
const CREAM = [250, 230, 215, 255]; // mountain motif inside the ribbon
const TRANSPARENT = [0, 0, 0, 0];
// Ribbon spans 14px wide (x=9..22) and 25px tall (y=3..27). The bottom 5
// rows form a V-cut, narrowing the ribbon into two prongs.
function inRibbon(x, y) {
if (x < 9 || x > 22) return false;
if (y < 3 || y > 27) return false;
if (y >= 23) {
const depth = y - 23; // 0..4
const cutLeft = 15 - depth;
const cutRight = 16 + depth;
if (x >= cutLeft && x <= cutRight) return false;
}
return true;
// Geometry, in 0..1 fractions of canvas.
const G = {
ribbonLeft: 9 / 32,
ribbonRight: 23 / 32, // exclusive
ribbonTop: 3 / 32,
ribbonBottom: 28 / 32, // exclusive
vCutTop: 23 / 32, // V notch starts here, points up into the ribbon
mountainTop: 7 / 32,
mountainBottom: 15 / 32, // exclusive
mountainBaseHalfW: 4.5 / 32,
shadowWidth: 1 / 32,
};
function makeColorAt(size) {
const rL = G.ribbonLeft * size;
const rR = G.ribbonRight * size;
const rT = G.ribbonTop * size;
const rB = G.ribbonBottom * size;
const vT = G.vCutTop * size;
const mT = G.mountainTop * size;
const mB = G.mountainBottom * size;
const sw = G.shadowWidth * size;
const cx = (rL + rR) / 2;
const baseHalf = G.mountainBaseHalfW * size;
return function colorAt(x, y) {
// Sample at pixel center so geometry edges land cleanly.
const px = x + 0.5;
const py = y + 0.5;
if (px < rL || px >= rR) return TRANSPARENT;
if (py < rT || py >= rB) return TRANSPARENT;
// V-cut: triangular notch carved from the bottom of the ribbon.
if (py >= vT) {
const depth = (py - vT) / (rB - vT); // 0..1
const cutHalfW = depth * ((rR - rL) / 2);
if (Math.abs(px - cx) < cutHalfW) return TRANSPARENT;
}
// Mountain peak motif inside the upper half of the ribbon. Peak at y=7,
// base at y=14, max half-width 4 (so base spans 8 source pixels).
function inMountain(x, y) {
if (y < 7 || y > 14) return false;
const halfW = Math.floor((y - 7) / 2) + 1;
return x >= 15 - halfW + 1 && x <= 15 + halfW;
// Mountain peak silhouette inside the upper half of the ribbon.
if (py >= mT && py < mB) {
const t = (py - mT) / (mB - mT); // 0..1 (top..base)
const halfW = t * baseHalf;
if (Math.abs(px - cx) <= halfW) return CREAM;
}
function colorAt(x, y) {
if (!inRibbon(x, y)) return TRANSPARENT;
// Inner mountain silhouette painted in cream so it pops against the
// orange ribbon at any size.
if (inMountain(x, y)) return CREAM;
// Right-edge shadow on the ribbon body (skip the V-cut zone so the cut
// edges look clean rather than half-shaded).
if (x === 22 && y >= 3 && y < 23) return ORANGE_DARK;
// Right-edge shadow on the ribbon body, stopping before the V-cut so the
// notch edges read clean rather than half-shaded.
if (px >= rR - sw && py < vT) return ORANGE_DARK;
return ORANGE;
};
}
function buildRawImage() {
const rowLen = SIZE * 4 + 1;
const raw = Buffer.alloc(rowLen * SIZE);
for (let y = 0; y < SIZE; y++) {
function buildRawImage(size) {
const colorAt = makeColorAt(size);
const rowLen = size * 4 + 1;
const raw = Buffer.alloc(rowLen * size);
for (let y = 0; y < size; y++) {
raw[y * rowLen] = 0;
for (let x = 0; x < SIZE; x++) {
for (let x = 0; x < size; x++) {
const [r, g, b, a] = colorAt(x, y);
const off = y * rowLen + 1 + x * 4;
raw[off] = r;
@@ -97,17 +116,17 @@ function chunk(type, data) {
return Buffer.concat([len, typeBuf, data, crcBuf]);
}
function buildPng() {
function buildPng(size) {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(SIZE, 0);
ihdr.writeUInt32BE(SIZE, 4);
ihdr.writeUInt32BE(size, 0);
ihdr.writeUInt32BE(size, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
const idat = zlib.deflateSync(buildRawImage());
const idat = zlib.deflateSync(buildRawImage(size));
return Buffer.concat([
sig,
chunk('IHDR', ihdr),
@@ -116,6 +135,13 @@ function buildPng() {
]);
}
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, buildPng());
console.log(`Wrote ${OUT}`);
const targets = [
{ size: 32, file: path.join(__dirname, '..', 'assets', 'tray-icon.png') },
{ size: 256, file: path.join(__dirname, '..', 'assets', 'app-icon.png') },
];
for (const t of targets) {
fs.mkdirSync(path.dirname(t.file), { recursive: true });
fs.writeFileSync(t.file, buildPng(t.size));
console.log(`Wrote ${t.file} (${t.size}x${t.size})`);
}