diff --git a/CLAUDE.md b/CLAUDE.md index df446e5b40..72188d8b47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,7 @@ ac-os flash+upload ### Notation - **compush** - commit & push +- **sticky the X** - on a macOS host, run `node toolchain/macos/sticky.mjs` on X — drops it into a translucent, larger-text Stickies note sized to fit the content and centered on screen. See `toolchain/macos/README.md`. ## Architecture diff --git a/toolchain/macos/README.md b/toolchain/macos/README.md new file mode 100644 index 0000000000..c306f8f51c --- /dev/null +++ b/toolchain/macos/README.md @@ -0,0 +1,26 @@ +# toolchain/macos + +Host-specific helpers that only make sense on a macOS machine. + +## sticky.mjs — the "sticky the X" workflow + +Drops text into a macOS **Stickies** note, styled the way @jeffrey +likes it: **translucent**, **larger text**, **sized to fit the content +closely**, and **centered on screen**. + +When @jeffrey says *"sticky the <thing>"* on a macOS host, run this +on that thing. + +```bash +node toolchain/macos/sticky.mjs path/to/file.txt # note from a file +node toolchain/macos/sticky.mjs --text "a note" # note from a string +echo "a note" | node toolchain/macos/sticky.mjs # note from stdin +``` + +Flags: `--bigger N` (font bumps over the Stickies default, default 3), +`--opaque` (skip translucency), `--no-center` (leave it where it lands). + +Needs **Accessibility** permission for the host terminal — the script +drives Stickies with `osascript` / System Events keystrokes (⌘N, ⌘V, +font bumps, ⌥⌘T). If the paste lands empty, the content is still on the +clipboard; just ⌘V into the note by hand. diff --git a/toolchain/macos/sticky.mjs b/toolchain/macos/sticky.mjs new file mode 100755 index 0000000000..b0800079da --- /dev/null +++ b/toolchain/macos/sticky.mjs @@ -0,0 +1,129 @@ +#!/usr/bin/env node +// sticky.mjs — drop text into a macOS Stickies note: translucent, +// larger text, sized to fit the content closely, centered on screen. +// +// This is the canonical "sticky the X" workflow. When @jeffrey says +// "sticky the " on a macOS host, run this on that thing. +// +// Usage: +// node toolchain/macos/sticky.mjs # note from a file +// node toolchain/macos/sticky.mjs --text "hello" # note from a string +// echo "hello" | node toolchain/macos/sticky.mjs # note from stdin +// +// Flags: +// --bigger N font-size bumps above the Stickies default (default 3) +// --opaque skip the translucency toggle (leave it solid) +// --no-center leave the window where Stickies dropped it +// +// macOS only. Needs Accessibility permission for the host terminal — +// System Events drives the ⌘N / ⌘V / font / translucency keystrokes. +// If the paste lands empty, the content is still on the clipboard +// (this script set it) — just ⌘V into the note by hand. + +import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; + +if (process.platform !== "darwin") { + console.error("✗ sticky.mjs is macOS-only (needs the Stickies app)."); + process.exit(1); +} + +// ── args ────────────────────────────────────────────────────────────── +const argv = process.argv.slice(2); +let bumps = 3, opaque = false, center = true, text = null, file = null; +for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--bigger") bumps = Math.max(0, parseInt(argv[++i], 10) || 0); + else if (a === "--opaque") opaque = true; + else if (a === "--no-center") center = false; + else if (a === "--text") text = argv[++i] ?? ""; + else if (!a.startsWith("--") && !file) file = a; +} + +// ── content: --text, file arg, or stdin ─────────────────────────────── +let content = text; +if (content == null && file) { + try { content = readFileSync(file, "utf8"); } + catch (e) { console.error(`✗ cannot read ${file}: ${e.message}`); process.exit(1); } +} +if (content == null && !process.stdin.isTTY) { + content = readFileSync(0, "utf8"); +} +if (content == null || content.trim() === "") { + console.error("usage: node toolchain/macos/sticky.mjs | --text \"…\" | (stdin)"); + process.exit(1); +} +content = content.replace(/\s+$/, ""); + +// ── size estimate — fit the text content closely ────────────────────── +// Stickies' default note text is ~14 pt; each ⌘+ bump adds ~2 pt. +// Width/height fall out of the rendered metrics; the AppleScript clamps +// the result to the screen. +const lines = content.split("\n"); +const maxLen = lines.reduce((m, l) => Math.max(m, l.length), 0); +const fontPt = 14 + 2 * bumps; +const charW = fontPt * 0.58; // proportional-font average advance +const lineH = fontPt * 1.42; // line box height +const desiredW = Math.round(maxLen * charW + 36); // + L/R padding +const desiredH = Math.round(lines.length * lineH + 56); // + titlebar/pad + +// ── set the clipboard (pbcopy handles unicode cleanly) ──────────────── +const cp = spawnSync("pbcopy", { input: content }); +if (cp.status !== 0) { console.error("✗ pbcopy failed"); process.exit(1); } + +// ── drive Stickies via osascript ────────────────────────────────────── +const osa = ` +on run argv + set desiredW to (item 1 of argv) as integer + set desiredH to (item 2 of argv) as integer + set bumps to (item 3 of argv) as integer + set doOpaque to ((item 4 of argv) is "1") + set doCenter to ((item 5 of argv) is "1") + + tell application "Finder" to set sb to bounds of window of desktop + set scrW to (item 3 of sb) + set scrH to (item 4 of sb) + + set w to desiredW + if w > scrW - 40 then set w to (scrW - 40) + if w < 260 then set w to 260 + set h to desiredH + if h > scrH - 60 then set h to (scrH - 60) + if h < 160 then set h to 160 + + tell application "Stickies" to activate + delay 0.5 + tell application "System Events" to tell process "Stickies" + keystroke "n" using command down + delay 0.4 + keystroke "v" using command down + delay 0.3 + keystroke "a" using command down + delay 0.15 + repeat bumps times + keystroke "+" using command down + delay 0.12 + end repeat + key code 123 -- collapse the selection (caret to start) + if not doOpaque then keystroke "t" using {command down, option down} + delay 0.2 + set size of window 1 to {w, h} + if doCenter then + set position of window 1 to {(scrW - w) / 2, (scrH - h) / 2} + end if + end tell + return ("sticky " & w & "x" & h & " on " & scrW & "x" & scrH) +end run +`; + +const r = spawnSync("osascript", [ + "-", String(desiredW), String(desiredH), String(bumps), + opaque ? "1" : "0", center ? "1" : "0", +], { input: osa, encoding: "utf8" }); + +if (r.status !== 0) { + console.error("✗ osascript failed:", (r.stderr || "").trim()); + console.error(" (the content is on your clipboard — ⌘V into a note by hand)"); + process.exit(1); +} +console.log(`✓ ${(r.stdout || "").trim()}`);