diff --git a/aesel/bin/easel b/aesel/bin/easel index 8c531581fd..b286a64e6c 100755 --- a/aesel/bin/easel +++ b/aesel/bin/easel @@ -39,6 +39,7 @@ Usage: ac [directory] [--runtime mjs|lisp|processing] aesthetic doctor aesthetic login | logout | whoami aesthetic publish [slug] + aesthetic colors Open the aesel terminal interface in a workspace, or manage the shared Aesthetic Computer sign-in (~/.ac-token) and publish a piece under @@ -136,7 +137,7 @@ case "${1:-}" in shift exec node "$PROJECT_DIR/src/history-cli.mjs" "$@" ;; - login|logout|whoami|publish) + login|logout|whoami|publish|colors) has_command node || fail "Node.js is required" exec node "$PROJECT_DIR/src/cli.mjs" "$@" ;; diff --git a/aesel/src/cli.mjs b/aesel/src/cli.mjs index 13940cee7a..63eaef510f 100644 --- a/aesel/src/cli.mjs +++ b/aesel/src/cli.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node -// cli.mjs — account and publish subcommands behind `aesthetic`. +// cli.mjs — account, publish and handle-colour subcommands behind `aesthetic`. import "./env.mjs"; import process from "node:process"; import { ACSession } from "./ac-session.mjs"; import { planPublish, publishPiece } from "./publish.mjs"; +import { handleColorPlan } from "./handle-colors.mjs"; +import { SITE } from "./ac-session.mjs"; const [command = "", ...rest] = process.argv.slice(2); const session = new ACSession(); @@ -37,6 +39,26 @@ try { out(result.route); if (!result.verified) note("published, but the live file did not read back yet"); } + } else if (command === "colors") { + // Your @handle's letters, painted: one colour repeats, several cycle. + // Names (orange, teal, …) or hex. The site keeps them; everything that + // draws your handle — the prompt, Aesel, Slab's rocks — reads them back. + const handle = session.handle; + if (!handle) fail("sign in first: ac login"); + if (!rest.length) fail("usage: ac colors e.g. ac colors orange teal"); + const colors = handleColorPlan(`@${handle}`, rest); + if (process.env.AESEL_DRY_RUN === "1") out(JSON.stringify(colors)); + else { + const token = await session.token(); + const response = await fetch(`${SITE}/api/handle-colors`, { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ handle, colors }), + }); + const body = await response.json().catch(() => ({})); + if (!response.ok) fail(body.message || `the site answered ${response.status}`); + out(`@${handle} · ${rest.join(" ")}`); + } } else { fail(`unknown command: ${command || "(none)"}`); } diff --git a/aesel/src/handle-colors.mjs b/aesel/src/handle-colors.mjs index 79b0ef7e67..61cf05d9b7 100644 --- a/aesel/src/handle-colors.mjs +++ b/aesel/src/handle-colors.mjs @@ -2,3 +2,24 @@ const colors=[[255,105,97],[255,170,80],[255,225,90],[150,225,90],[90,220,140],[90,215,210],[95,180,255],[150,150,255],[200,140,255],[255,140,210]]; export function handleCharacterColors(handle,saved){return Array.from(handle).map((character,index)=>{const custom=saved?.[index];if(custom&&[custom.r,custom.g,custom.b].every(n=>Number.isFinite(n)&&n>=0&&n<=255))return[custom.r,custom.g,custom.b].map(Math.round);const ch=character.toLowerCase();return ch>='a'&&ch<='z'?colors[(ch.charCodeAt(0)-97)%10]:ch>='0'&&ch<='9'?colors[Number(ch)]:[220,180,255];});} export async function fetchHandleColors(handle,{fetch=globalThis.fetch}={}){const r=await fetch('https://aesthetic.computer/api/handle-colors?handle='+encodeURIComponent(handle.replace(/^@/,'')),{signal:AbortSignal.timeout(5000)});if(!r.ok)throw new Error('Handle colors unavailable');const data=await r.json();return handleCharacterColors(handle,data.colors);} + +// Colour words a person would type, as the site stores them. +const NAMED = { + red: [230, 50, 50], orange: [255, 140, 0], yellow: [250, 210, 40], lime: [150, 220, 50], + green: [40, 180, 80], teal: [0, 160, 160], cyan: [40, 200, 230], blue: [50, 110, 230], + purple: [140, 80, 210], magenta: [220, 60, 200], pink: [255, 120, 180], white: [245, 245, 245], + gray: [150, 150, 150], grey: [150, 150, 150], black: [20, 20, 20], brown: [140, 90, 50], gold: [230, 180, 40], +}; +function parseColor(word) { + const w = String(word).trim().toLowerCase(); + if (NAMED[w]) return NAMED[w]; + const hex = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/.exec(w)?.[1]; + if (!hex) throw new Error(`not a colour: ${word} (try a name like orange or a hex like #ff8800)`); + const full = hex.length === 3 ? [...hex].map((c) => c + c).join("") : hex; + return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16)); +} +// One colour per character of `@handle`, cycling through the words given. +export function handleColorPlan(handle, words) { + const palette = words.map(parseColor); + return Array.from(handle).map((_, i) => { const [r, g, b] = palette[i % palette.length]; return { r, g, b }; }); +} diff --git a/aesel/src/workspace-tools.mjs b/aesel/src/workspace-tools.mjs index 76715e5814..84ca94572a 100644 --- a/aesel/src/workspace-tools.mjs +++ b/aesel/src/workspace-tools.mjs @@ -93,7 +93,11 @@ const TOOLBOX = [ ["puppet", "act on a Mac or browser: click, type, keys (`puppet --help`)"], ["slab-ledger", "the fleet's live sessions"], ["ac-os", "AC Native OS builds"], - ["gh", "GitHub"], + ["ac", "the person's AC account: `ac whoami`, `ac publish [slug]`, `ac colors orange teal` (their @handle's letter colours, names or hex, cycling)"], + ["rg", "ripgrep, for searching code — never grep -r"], + ["fd", "find files by name, fast"], + ["jq", "JSON"], + ["gh", "GitHub"], ]; export function toolboxInstructions(env = process.env) {