@promptctl/cc-candybar 1.56.1 → 1.56.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -149,7 +149,7 @@ Config tooling:
149
149
  Replace: layout: [["seg1", "seg2"], ["seg3"]]
150
150
  With: root: { v: [{ h: ["seg1", "seg2"] }, "seg3"] }
151
151
  Single-row example: root: { h: ["seg1", "seg2"] }`,line:J(e.source,[`layout`])}),t.root!==void 0&&(n.root=pc(e,`root`,t.root)),t.actions!==void 0&&(n.actions=Tg(e,t.actions)),t.looks!==void 0&&(n.looks=p_(e,t.looks)),t.presets!==void 0&&(n.presets=h_(e,t.presets)),t.editGlobals!==void 0&&(n.editGlobals=wo(e,`editGlobals`,t.editGlobals)),t.helpers!==void 0&&(n.helpers=g_(e,t.helpers)),Sc(e,n),km(e,n),Rm(e,n),Xs(e,n,hh,`the global settings menu`),n}const sv=new Set([`globals`,`variables`,`segments`,`layout`,`root`,`actions`,`looks`,`presets`,`editGlobals`,`helpers`]);function cv(e){let t=fv(e.termCols),n=fv(e.termRows),r=pv(e.ssh),i=uv(e.tmux),a=lv(e.configEnv);return{...t!==void 0&&{termCols:t},...n!==void 0&&{termRows:n},...r!==void 0&&{ssh:r},...i!==void 0&&{tmux:i},...a!==void 0&&{configEnv:a}}}function lv(e){return typeof e==`string`&&e!==``?J_(e):void 0}function uv(e){if(e===null)return null;if(typeof e!=`object`)return;let{socket:t,pane:n,truecolor:r}=e;if(!(typeof t!=`string`||t===``)&&!(typeof n!=`string`||n===``)&&!(r!==null&&(typeof r!=`string`||r===``)))return{socket:t,pane:n,truecolor:r}}const dv=1e4;function fv(e){if(typeof e!=`number`||!Number.isFinite(e))return;let t=Math.floor(e);if(!(t<=0))return t>dv?dv:t}function pv(e){return typeof e==`boolean`?e:void 0}var mv=class extends Error{constructor(e){super(e),this.name=`ProtocolError`}};function hv(e){let t=Buffer.from(JSON.stringify(e),`utf8`),n=Buffer.alloc(4);return n.writeUInt32BE(t.length,0),Buffer.concat([n,t])}function gv(e,t){let n=Buffer.alloc(0);return function(r){for(n=Buffer.concat([n,r]);n.length>=4;){let r=n.readUInt32BE(0);if(r>16777216){t(new mv(`frame too large: ${r}`));return}if(n.length<4+r)return;let i=n.subarray(4,4+r);n=n.subarray(4+r);try{e(JSON.parse(i.toString(`utf8`)))}catch(e){let n=new mv(e instanceof Error?e.message:String(e));e instanceof Error&&(n.cause=e),t(n);return}}}}function _v(e,t,n){return new Promise((r,i)=>{let a=!1,o=t=>{if(!a){a=!0,clearTimeout(s),e.removeAllListeners();try{t()}catch{}}},s=setTimeout(()=>{o(()=>{e.destroy(),i(Error(`TIMEOUT`))})},n),c=gv(e=>o(()=>r(e)),e=>o(()=>i(e)));e.on(`data`,c),e.on(`error`,e=>o(()=>i(e))),e.on(`close`,()=>o(()=>i(Error(`socket closed before response`)))),e.write(hv(t))})}async function vv(e,t,n){let r=null;try{r=await Cv(k(),t.connectMs);let i=await _v(r,{v:3,...e},t.budgetMs);return xv(e.kind,i,n)}catch(e){return Sv(e)}finally{r&&r.destroy()}}function yv(e,t){return typeof e==`string`?e:t}function bv(e){return typeof e==`number`&&Number.isInteger(e)&&e>=0?e:0}function xv(e,t,n){let r=t;if(r.ok===!0){let r=n(t);return r===void 0?{kind:`permanent`,cause:`malformed_response`,message:`ok response without payload for "${e}" request`}:{kind:`ok`,value:r}}let i=yv(r.error,`(no error message)`);switch(r.code){case`VERSION_MISMATCH`:return{kind:`permanent`,cause:`version_mismatch`,clientV:3,daemonV:bv(r.daemonV)};case`TIMEOUT`:return{kind:`transient`,cause:`timeout`,message:i};case`BAD_REQUEST`:return{kind:`permanent`,cause:`bad_request`,message:i};case`RENDER_FAILED`:return{kind:`permanent`,cause:`render_failed`,message:i};default:return{kind:`permanent`,cause:`malformed_response`,message:`unknown error code: ${String(r.code)}`}}}function Sv(e){if(e instanceof mv||e instanceof SyntaxError)return{kind:`permanent`,cause:`malformed_response`,message:e instanceof Error?e.message:String(e)};let t=e instanceof Error?e.message:String(e);return t===`CONNECT_TIMEOUT`||t===`TIMEOUT`?{kind:`transient`,cause:`timeout`,message:t}:t.includes(`ECONNREFUSED`)||t.includes(`ENOENT`)||t.includes(`ENOTSOCK`)?{kind:`transient`,cause:`unreachable`,message:t}:{kind:`transient`,cause:`io_error`,message:t}}function Cv(e,t){return new Promise((n,r)=>{let i=g.createConnection({path:e}),a=setTimeout(()=>{i.destroy(),r(Error(`CONNECT_TIMEOUT`))},t);i.once(`connect`,()=>{clearTimeout(a),n(i)}),i.once(`error`,e=>{clearTimeout(a),r(e)})})}function wv(e){if(e.kind===`transient`)return`daemon unavailable (${e.cause}: ${e.message})\nHint: daemon may not be running. Run \`cc-candybar\` once to spawn it.`;switch(e.cause){case`version_mismatch`:{let t=e.daemonV===0?`unknown`:`v${e.daemonV}`;return`daemon protocol mismatch (client v${e.clientV} ≠ daemon ${t})`}case`bad_request`:case`render_failed`:case`malformed_response`:return`daemon error (${e.cause}): ${e.message}`}}const Tv={connectMs:50,budgetMs:150},Ev={connectMs:50,budgetMs:200};function Dv(e){let t=e.output;return typeof t==`string`?t:void 0}function Ov(e,t,n,r){return vv({kind:`render`,hookData:e,args:t,cwd:n,...r},Tv,Dv)}function kv(e,t){return vv({kind:`click`,verb:e,value:t},Ev,Dv)}let Av=null;function jv(e){if(Av!==null)return Av;r.mkdirSync(c.dirname(e),{recursive:!0});try{Av=r.statSync(e).size}catch{Av=0}return Av}function Mv(e){for(let t=2;t>=1;t--){let n=`${e}.${t}`,i=`${e}.${t+1}`;try{r.renameSync(n,i)}catch{}}try{r.renameSync(e,`${e}.1`)}catch{}Av=0}function Z(e,t){let n=`${new Date().toISOString()} [${e}] ${t}\n`;try{let e=He(),t=jv(e);r.appendFileSync(e,n),Av=t+Buffer.byteLength(n,`utf8`),Av>=5242880&&Mv(e)}catch(e){Av=null;try{process.stderr.write(`${n}cc-candybar: daemon.log unwritable: ${e.message}\n`)}catch{}}}const Nv=`CC_CANDYBAR_RSS_LIMIT_MB`;function Pv(e){let t=e[Nv];if(t===void 0)return 512;let n=/^\d+$/.test(t)?Number(t):NaN;if(!Number.isSafeInteger(n)||n<=0)throw Error(`${Nv} must be a positive integer (MB), got ${JSON.stringify(t)}`);return n}function Fv(e){return Pv(e)*2}const Iv=1024*1024;function Lv(e){return Pv(e)*Iv}function Rv(e){let t=e.rssLimitBytes??512*Iv,n=e.snapshotsKeep??3,r=!1;function i(){if(r)return!0;let i=e.rssBytes();if(i<=t)return!1;r=!0,e.log(`warn`,`RSS ${i} > limit ${t}; writing heap snapshot then shutting down`);try{let t=new Date(e.now()).toISOString().replace(/[:.]/g,`-`),r=c.join(e.snapshotDir,`heap-${t}-${e.pid}.heapsnapshot`),i=e.writeHeapSnapshot(r);e.log(`info`,`heap snapshot written: ${i}`),zv(e.listSnapshots(),n,e.removeFile)}catch(t){e.log(`warn`,`heap snapshot failed: ${t.message}`)}return e.shutdown(0),!0}function a(){let n=e.rssBytes();return n>t*.75?`rss ${n} approaching limit ${t}`:null}function o(e=6e4){let t=setInterval(()=>{i()},e);return t.unref(),{disarm:()=>clearInterval(t)}}return{checkRss:i,describeNextRestart:a,arm:o}}function zv(e,t,n){let r=[...e].sort((e,t)=>{let n=e.slice(e.lastIndexOf(`/`)+1);return t.slice(t.lastIndexOf(`/`)+1).localeCompare(n)});for(let e of r.slice(t))try{n(e)}catch{}}function Bv(e,t,n={}){let i=Ae();return{now:()=>Date.now(),pid:process.pid,snapshotDir:i,log:Z,rssBytes:()=>process.memoryUsage().rss,writeHeapSnapshot:e=>_.writeHeapSnapshot(e),listSnapshots:()=>{try{return r.readdirSync(i).filter(e=>e.startsWith(`heap-`)&&e.endsWith(`.heapsnapshot`)).map(e=>c.join(i,e))}catch{return[]}},removeFile:e=>r.unlinkSync(e),shutdown:t,startedAtMs:e,...n}}function Vv(e={}){if(ry()!==null)return;let n=e.spawn??iy,r=ey();if(r.kind===`contended`){let e=Uv();e!==null&&e>2e3&&(t.stderr.write(`cc-candybar: spawn-lock held ${e}ms (likely crashed holder) — spawning unlocked\n`),Hv(n));return}if(r.kind===`error`){t.stderr.write(`cc-candybar: spawn-lock unavailable (${r.reason}) — spawning unlocked\n`),Hv(n);return}try{Hv(n)}finally{ny()}}function Hv(e){Xv()&&Wv(e)}function Uv(){try{let e=r.statSync(Re());return Date.now()-e.mtimeMs}catch{return null}}function Wv(e){try{e()||t.stderr.write(`cc-candybar: daemon spawn returned false (unable to resolve script path?)
152
- `)}catch(e){t.stderr.write(`cc-candybar: daemon spawn failed: ${e.message}\n`)}}function Gv(e,t){return e===null?{kind:`allow`}:e<-1e4?{kind:`allow-future-garbage`,futureMs:-e}:e<t?{kind:`deny`}:{kind:`allow`}}function Kv(e){return Math.min(3e3*2**Math.min(Math.max(e,0),5),6e4)}function qv(e){try{let t=r.readFileSync(e,`utf8`).trim(),n=Number(t);return!Number.isInteger(n)||n<0?0:Math.min(n,5)}catch{return 0}}function Jv(e,n){try{r.mkdirSync(c.dirname(e),{recursive:!0}),r.writeFileSync(e,String(n),{mode:384})}catch(e){t.stderr.write(`cc-candybar: could not record spawn.backoff: ${e.message}\n`)}}function Yv(){try{r.unlinkSync(Be())}catch(e){e.code!==`ENOENT`&&t.stderr.write(`cc-candybar: could not reset spawn.backoff: ${e.message}\n`)}}function Xv(){let e=ze(),n=Be(),r=qv(n),i=Gv(Zv(e),Kv(r));return i.kind===`deny`?!1:(i.kind===`allow-future-garbage`&&t.stderr.write(`cc-candybar: spawn.cooldown mtime is ${i.futureMs}ms in the future — ignoring and spawning\n`),Qv(e),Jv(n,Math.min(r+1,5)),!0)}function Zv(e){try{return Date.now()-r.statSync(e).mtimeMs}catch{return null}}function Qv(e){try{r.mkdirSync(c.dirname(e),{recursive:!0}),r.writeFileSync(e,`${t.pid} ${Date.now()}\n`,{mode:384})}catch(e){t.stderr.write(`cc-candybar: could not record spawn.cooldown: ${e.message}\n`)}}let $v=null;function ey(){let e=Re();for(let n=0;n<2;n++){try{let n=r.openSync(e,`wx`,384);try{r.writeSync(n,JSON.stringify({pid:t.pid,ts:Date.now()}))}catch{}return $v={fd:n,path:e},{kind:`held`}}catch(t){let n=t.code;if(n!==`EEXIST`)return{kind:`error`,reason:`openSync(${e}): ${n??t.message}`}}if(!ty(e))return{kind:`contended`};try{r.unlinkSync(e)}catch(e){if(e.code!==`ENOENT`)return{kind:`error`,reason:`unlink stale spawn.lock: ${e.message}`}}}return{kind:`contended`}}function ty(e){try{let t=r.statSync(e);return Date.now()-t.mtimeMs>1e4}catch{return!0}}function ny(){if(!$v)return;let{fd:e,path:t}=$v;$v=null;try{r.closeSync(e)}catch{}try{r.unlinkSync(t)}catch{}}function ry(){try{return r.mkdirSync(Ae(),{recursive:!0}),null}catch(e){return`mkdir ${Ae()}: ${e.message}`}}function iy(){let e=t.execPath,n=t.argv[1];return n?we({bin:e,args:[`--max-old-space-size=${Fv(t.env)}`,n,`daemon`],category:`daemon-spawn`}).ok:!1}const ay=`cc-candybar`,oy=`dispatch`,sy=`set-state`,cy=`step-state`,ly=`copy`,uy=`open-vscode`,dy=`toolbar-toggle`,fy=`show-config-error`,py=`show-config-warning`,my=`set-config`,hy=`step-config`,gy=`reset-config`,_y=`apply-layout-op`,vy=`undo`,yy=`redo`,by=`apply-update`,xy=`doctor-run`,Sy=`doctor-fix`;function Cy(e){return e.map(encodeURIComponent).join(`/`)}function wy(e){return e.length===0?[]:e.split(`/`).map(decodeURIComponent)}function Ty(e){return`${ay}://${oy}/${e.map(e=>`e=${encodeURIComponent(`${e.verb}/${Cy(e.args)}`)}`).join(`&`)}`}function Ey(e){return new y(e).getAll(`e`).map(Dy)}function Dy(e){let t=e.indexOf(`/`);return t===-1?{verb:e,value:``}:{verb:e.slice(0,t),value:e.slice(t+1)}}const Oy=`1.56.1`;function ky(){return c.join(f.homedir(),`.claude`,`settings.json`)}function Q(e){return{kind:`ok`,value:e}}const Ay={kind:`absent`};function jy(e){return{kind:`failed`,reason:e}}function My(e,t){return e?.kind===`ok`?e.value:t}const Ny=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;function Py(e){let t=Ny.exec(e);return t?{kind:`ok`,value:[Number(t[1]),Number(t[2]),Number(t[3])]}:{kind:`failed`,reason:`"${e}" is not a release version (expected MAJOR.MINOR.PATCH)`}}function Fy(e){return e.join(`.`)}function Iy([e,t,n],[r,i,a]){return e-r||t-i||n-a}const Ly=`@promptctl/cc-candybar`;async function Ry(e,t,n){let r=`${n}/-/package/${encodeURIComponent(e)}/dist-tags`;try{let e=await t(r,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return jy(`registry responded ${e.status} for ${r}`);let n=await e.json();return typeof n.latest==`string`?Py(n.latest):Ay}catch(e){return jy(e instanceof Error?e.message:String(e))}}function zy(e,t){let n=Py(e);if(n.kind===`failed`)return{kind:`unchecked`,installed:e,reason:n.reason};switch(t.kind){case`failed`:return{kind:`unchecked`,installed:e,reason:t.reason};case`absent`:return{kind:`unchecked`,installed:e,reason:"the registry lists no `latest` dist-tag"};case`ok`:{let e=Iy(n.value,t.value);return e<0?{kind:`stale`,installed:n.value,latest:t.value}:e>0?{kind:`ahead`,installed:n.value,latest:t.value}:{kind:`current`,installed:n.value}}}}function By(e,t){switch(t.kind){case`current`:return{stream:`stdout`,text:`✓ cc-candybar ${Fy(t.installed)} is the latest release.\n`};case`ahead`:return{stream:`stdout`,text:`cc-candybar ${Fy(t.installed)} is newer than the registry's latest release (${Fy(t.latest)}): an unpublished build.\n`};case`stale`:{let n=Fy(t.latest);return{stream:`stderr`,text:`⚠ cc-candybar ${Fy(t.installed)} was staged, but the latest release is ${n}.\n pnpm's release-age gate (minimumReleaseAge) and its dlx cache can both\n resolve \`@latest\` to an older release without saying so.\n To install ${n} now, name it explicitly:\n pnpm dlx ${e}@${n} install\n`}}case`unchecked`:return{stream:`stderr`,text:`⚠ Could not check for a cc-candybar release newer than ${t.installed} (${t.reason}); the registry check was skipped.\n`}}}const Vy=`CCCandybarURLHandler`,Hy=[],Uy={"darwin-arm64":`@promptctl/cc-candybar-darwin-arm64`,"darwin-x64":`@promptctl/cc-candybar-darwin-x64`,"linux-x64":`@promptctl/cc-candybar-linux-x64`,"linux-arm64":`@promptctl/cc-candybar-linux-arm64`};function Wy(e){return/^[A-Za-z0-9_./=,:-]+$/.test(e)?e:`'${e.replace(/'/g,`'\\''`)}'`}function Gy(e,t){return[e,...t].map(Wy).join(` `)}function Ky(){return c.join(f.homedir(),`Applications`,`${Vy}.app`)}function qy(){if(process.platform!==`darwin`)throw Error(`URL handler installation requires macOS (found platform: ${process.platform}).`)}function Jy(){if(process.platform===`darwin`)return c.join(f.homedir(),`Library`,`Application Support`,`CCCandybar`);let e=process.env.XDG_DATA_HOME??c.join(f.homedir(),`.local`,`share`);return c.join(e,`cc-candybar`)}function Yy(){return c.join(Jy(),`bin`,`cc-candybar`)}function Xy(){return c.join(Jy(),`dist`,`index.mjs`)}function Zy(e,t){return[`on open location L`,`\tdo shell script "'${e.replace(/"/g,`\\"`)}' '${t.replace(/"/g,`\\"`)}' url-handle " & quoted form of L`,`end open location`].join(`
152
+ `)}catch(e){t.stderr.write(`cc-candybar: daemon spawn failed: ${e.message}\n`)}}function Gv(e,t){return e===null?{kind:`allow`}:e<-1e4?{kind:`allow-future-garbage`,futureMs:-e}:e<t?{kind:`deny`}:{kind:`allow`}}function Kv(e){return Math.min(3e3*2**Math.min(Math.max(e,0),5),6e4)}function qv(e){try{let t=r.readFileSync(e,`utf8`).trim(),n=Number(t);return!Number.isInteger(n)||n<0?0:Math.min(n,5)}catch{return 0}}function Jv(e,n){try{r.mkdirSync(c.dirname(e),{recursive:!0}),r.writeFileSync(e,String(n),{mode:384})}catch(e){t.stderr.write(`cc-candybar: could not record spawn.backoff: ${e.message}\n`)}}function Yv(){try{r.unlinkSync(Be())}catch(e){e.code!==`ENOENT`&&t.stderr.write(`cc-candybar: could not reset spawn.backoff: ${e.message}\n`)}}function Xv(){let e=ze(),n=Be(),r=qv(n),i=Gv(Zv(e),Kv(r));return i.kind===`deny`?!1:(i.kind===`allow-future-garbage`&&t.stderr.write(`cc-candybar: spawn.cooldown mtime is ${i.futureMs}ms in the future — ignoring and spawning\n`),Qv(e),Jv(n,Math.min(r+1,5)),!0)}function Zv(e){try{return Date.now()-r.statSync(e).mtimeMs}catch{return null}}function Qv(e){try{r.mkdirSync(c.dirname(e),{recursive:!0}),r.writeFileSync(e,`${t.pid} ${Date.now()}\n`,{mode:384})}catch(e){t.stderr.write(`cc-candybar: could not record spawn.cooldown: ${e.message}\n`)}}let $v=null;function ey(){let e=Re();for(let n=0;n<2;n++){try{let n=r.openSync(e,`wx`,384);try{r.writeSync(n,JSON.stringify({pid:t.pid,ts:Date.now()}))}catch{}return $v={fd:n,path:e},{kind:`held`}}catch(t){let n=t.code;if(n!==`EEXIST`)return{kind:`error`,reason:`openSync(${e}): ${n??t.message}`}}if(!ty(e))return{kind:`contended`};try{r.unlinkSync(e)}catch(e){if(e.code!==`ENOENT`)return{kind:`error`,reason:`unlink stale spawn.lock: ${e.message}`}}}return{kind:`contended`}}function ty(e){try{let t=r.statSync(e);return Date.now()-t.mtimeMs>1e4}catch{return!0}}function ny(){if(!$v)return;let{fd:e,path:t}=$v;$v=null;try{r.closeSync(e)}catch{}try{r.unlinkSync(t)}catch{}}function ry(){try{return r.mkdirSync(Ae(),{recursive:!0}),null}catch(e){return`mkdir ${Ae()}: ${e.message}`}}function iy(){let e=t.execPath,n=t.argv[1];return n?we({bin:e,args:[`--max-old-space-size=${Fv(t.env)}`,n,`daemon`],category:`daemon-spawn`}).ok:!1}const ay=`cc-candybar`,oy=`dispatch`,sy=`set-state`,cy=`step-state`,ly=`copy`,uy=`open-vscode`,dy=`toolbar-toggle`,fy=`show-config-error`,py=`show-config-warning`,my=`set-config`,hy=`step-config`,gy=`reset-config`,_y=`apply-layout-op`,vy=`undo`,yy=`redo`,by=`apply-update`,xy=`doctor-run`,Sy=`doctor-fix`;function Cy(e){return e.map(encodeURIComponent).join(`/`)}function wy(e){return e.length===0?[]:e.split(`/`).map(decodeURIComponent)}function Ty(e){return`${ay}://${oy}/${e.map(e=>`e=${encodeURIComponent(`${e.verb}/${Cy(e.args)}`)}`).join(`&`)}`}function Ey(e){return new y(e).getAll(`e`).map(Dy)}function Dy(e){let t=e.indexOf(`/`);return t===-1?{verb:e,value:``}:{verb:e.slice(0,t),value:e.slice(t+1)}}const Oy=`1.56.2`;function ky(){return c.join(f.homedir(),`.claude`,`settings.json`)}function Q(e){return{kind:`ok`,value:e}}const Ay={kind:`absent`};function jy(e){return{kind:`failed`,reason:e}}function My(e,t){return e?.kind===`ok`?e.value:t}const Ny=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;function Py(e){let t=Ny.exec(e);return t?{kind:`ok`,value:[Number(t[1]),Number(t[2]),Number(t[3])]}:{kind:`failed`,reason:`"${e}" is not a release version (expected MAJOR.MINOR.PATCH)`}}function Fy(e){return e.join(`.`)}function Iy([e,t,n],[r,i,a]){return e-r||t-i||n-a}const Ly=`@promptctl/cc-candybar`;async function Ry(e,t,n){let r=`${n}/-/package/${encodeURIComponent(e)}/dist-tags`;try{let e=await t(r,{signal:AbortSignal.timeout(5e3)});if(!e.ok)return jy(`registry responded ${e.status} for ${r}`);let n=await e.json();return typeof n.latest==`string`?Py(n.latest):Ay}catch(e){return jy(e instanceof Error?e.message:String(e))}}function zy(e,t){let n=Py(e);if(n.kind===`failed`)return{kind:`unchecked`,installed:e,reason:n.reason};switch(t.kind){case`failed`:return{kind:`unchecked`,installed:e,reason:t.reason};case`absent`:return{kind:`unchecked`,installed:e,reason:"the registry lists no `latest` dist-tag"};case`ok`:{let e=Iy(n.value,t.value);return e<0?{kind:`stale`,installed:n.value,latest:t.value}:e>0?{kind:`ahead`,installed:n.value,latest:t.value}:{kind:`current`,installed:n.value}}}}function By(e,t){switch(t.kind){case`current`:return{stream:`stdout`,text:`✓ cc-candybar ${Fy(t.installed)} is the latest release.\n`};case`ahead`:return{stream:`stdout`,text:`cc-candybar ${Fy(t.installed)} is newer than the registry's latest release (${Fy(t.latest)}): an unpublished build.\n`};case`stale`:{let n=Fy(t.latest);return{stream:`stderr`,text:`⚠ cc-candybar ${Fy(t.installed)} was staged, but the latest release is ${n}.\n pnpm's release-age gate (minimumReleaseAge) and its dlx cache can both\n resolve \`@latest\` to an older release without saying so.\n To install ${n} now, name it explicitly:\n pnpm dlx ${e}@${n} install\n`}}case`unchecked`:return{stream:`stderr`,text:`⚠ Could not check for a cc-candybar release newer than ${t.installed} (${t.reason}); the registry check was skipped.\n`}}}const Vy=`CCCandybarURLHandler`,Hy=[],Uy={"darwin-arm64":`@promptctl/cc-candybar-darwin-arm64`,"darwin-x64":`@promptctl/cc-candybar-darwin-x64`,"linux-x64":`@promptctl/cc-candybar-linux-x64`,"linux-arm64":`@promptctl/cc-candybar-linux-arm64`};function Wy(e){return/^[A-Za-z0-9_./=,:-]+$/.test(e)?e:`'${e.replace(/'/g,`'\\''`)}'`}function Gy(e,t){return[e,...t].map(Wy).join(` `)}function Ky(){return c.join(f.homedir(),`Applications`,`${Vy}.app`)}function qy(){if(process.platform!==`darwin`)throw Error(`URL handler installation requires macOS (found platform: ${process.platform}).`)}function Jy(){if(process.platform===`darwin`)return c.join(f.homedir(),`Library`,`Application Support`,`CCCandybar`);let e=process.env.XDG_DATA_HOME??c.join(f.homedir(),`.local`,`share`);return c.join(e,`cc-candybar`)}function Yy(){return c.join(Jy(),`bin`,`cc-candybar`)}function Xy(){return c.join(Jy(),`dist`,`index.mjs`)}function Zy(e,t){return[`on open location L`,`\tdo shell script "'${e.replace(/"/g,`\\"`)}' '${t.replace(/"/g,`\\"`)}' url-handle " & quoted form of L`,`end open location`].join(`
153
153
  `)}function Qy(e){if(!e)throw Error(`install: process.argv[1] not set`);return e.endsWith(`.mjs`)||e.endsWith(`.js`)?e:c.resolve(c.dirname(e),`..`,`dist`,`index.mjs`)}function $y(t){let n=Uy[`${process.platform}-${process.arch}`];if(n){let r=e(t);try{return{kind:`native`,sourcePath:r.resolve(`${n}/bin/cc-candybar`)}}catch{}}return{kind:`node-shim`,sourcePath:c.resolve(c.dirname(t),`..`,`bin`,`cc-candybar`)}}function eb(e,t){c.resolve(e)!==c.resolve(t)&&r.copyFileSync(e,t)}function tb(e){let t=r.openSync(e,`r`);try{let n=Buffer.alloc(2),i=r.readSync(t,n,0,2,0);if(i<2)throw Error(`install: staged render entry at ${e} is truncated (${i} byte(s)). Re-run: pnpm dlx ${Ly}@latest install`);return n.toString(`latin1`)===`#!`?`node-shim`:`native`}finally{r.closeSync(t)}}function nb(){let e=Qy(process.argv[1]);if(!r.existsSync(e))throw Error(`install: bundled dist not found at ${e}. Reinstall the package.`);let t=$y(e);if(!r.existsSync(t.sourcePath))throw Error(`install: render entry not found at ${t.sourcePath}. Reinstall the package.`);r.mkdirSync(c.dirname(Xy()),{recursive:!0}),r.mkdirSync(c.dirname(Yy()),{recursive:!0}),eb(e,Xy()),eb(t.sourcePath,Yy()),r.chmodSync(Yy(),493),r.rmSync(c.join(Jy(),`url-handler.mjs`),{force:!0});let n=tb(Yy());return process.stdout.write(`Staged cc-candybar v${Oy} runtime at ${Jy()}\n`+(n===`native`?` render entry: native binary (${process.platform}-${process.arch})\n`:` render entry: node shim (no native binary for ${process.platform}-${process.arch}; renders are correct but pay node startup)\n`)),{binPath:Yy(),distPath:Xy(),entryKind:n}}function rb(){return[{key:`CFBundleIdentifier`,xml:`<string>com.cccandybar.url-handler</string>`},{key:`CFBundleURLTypes`,xml:[`<array>`,` <dict>`,` <key>CFBundleURLName</key>`,` <string>Claude Powerline Click Action</string>`,` <key>CFBundleURLSchemes</key>`,` <array>`,` <string>${ay}</string>`,` </array>`,` </dict>`,`</array>`].join(`
154
154
  `)}]}function ib(e){let t=Ky();r.mkdirSync(c.dirname(t),{recursive:!0}),r.existsSync(t)&&r.rmSync(t,{recursive:!0,force:!0}),process.stdout.write(`Building ${t}\n`);let n=Ce({bin:`/usr/bin/osacompile`,args:[`-o`,t,`-e`,Zy(process.execPath,e)],category:`install.osacompile`});if(!n.ok)throw process.stderr.write(n.stderr),Error(`osacompile failed (${n.reason})`);let i=c.join(t,`Contents`,`Info.plist`);for(let{key:e}of rb())Ce({bin:`/usr/bin/plutil`,args:[`-remove`,e,i],category:`install.plutil`});for(let{key:e,xml:t}of rb()){let n=Ce({bin:`/usr/bin/plutil`,args:[`-insert`,e,`-xml`,t,i],category:`install.plutil`});if(!n.ok)throw process.stderr.write(n.stderr),Error(`plutil -insert ${e} failed (${n.reason})`)}process.stdout.write(`Registering ${ay}:// with Launch Services\n`);let a=Ce({bin:`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister`,args:[`-f`,t],category:`install.lsregister`});if(!a.ok)throw process.stderr.write(a.stderr),Error(`lsregister failed (${a.reason})`);process.stdout.write(`✓ ${Vy}.app installed and registered.\n`),process.stdout.write(` Test: open '${ay}://hello-world' && pbpaste\n`)}function ab(){qy(),ib(nb().distPath)}function ob(e,t=ay){let n=`${t}://`;if(!e.startsWith(n))throw Error(`expected ${n} scheme, got: ${e}`);let r=e.slice(n.length),i=r.indexOf(`/`);return i===-1?{verb:ly,value:r}:{verb:decodeURIComponent(r.slice(0,i)),value:r.slice(i+1)}}async function sb(e){e||(process.stderr.write(`url-handle: missing URL argument.
155
155
  `),process.exit(1));let t;try{t=ob(e)}catch(e){process.stderr.write(`url-handle: ${e instanceof Error?e.message:String(e)}\n`),process.exit(1)}let n=await kv(t.verb,t.value);n.kind===`ok`&&process.exit(0),n.kind===`transient`&&(Vv(),process.stderr.write(`url-handle: daemon unavailable (${n.cause}: ${n.message})\n`),process.exit(1)),process.stderr.write(cb(n)+`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.56.1",
3
+ "version": "1.56.2",
4
4
  "description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -90,9 +90,9 @@
90
90
  "mobx": "^6.15.0"
91
91
  },
92
92
  "optionalDependencies": {
93
- "@promptctl/cc-candybar-darwin-arm64": "1.56.1",
94
- "@promptctl/cc-candybar-darwin-x64": "1.56.1",
95
- "@promptctl/cc-candybar-linux-x64": "1.56.1",
96
- "@promptctl/cc-candybar-linux-arm64": "1.56.1"
93
+ "@promptctl/cc-candybar-darwin-arm64": "1.56.2",
94
+ "@promptctl/cc-candybar-darwin-x64": "1.56.2",
95
+ "@promptctl/cc-candybar-linux-x64": "1.56.2",
96
+ "@promptctl/cc-candybar-linux-arm64": "1.56.2"
97
97
  }
98
98
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-candybar",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "CCCandybar statusline setup wizard",
5
5
  "author": {
6
6
  "name": "promptctl"
@@ -1,305 +1,168 @@
1
1
  #!/usr/bin/env bash
2
+ # Render the wizard's template config against a fixture session so the user can
3
+ # see a real bar before committing to a theme, style, charset, or preset.
4
+ #
5
+ # The config rendered here IS the template the wizard writes — templates/config.json
6
+ # with its four placeholders filled by the same substitution the wizard performs —
7
+ # so the preview cannot drift from what the wizard installs.
8
+ # (test/plugin-templates.test.ts drives every name below through `cc-candybar check`.)
2
9
 
3
10
  set -euo pipefail
4
11
 
5
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
13
  readonly SCRIPT_DIR
7
14
  readonly PLUGIN_ROOT="${SCRIPT_DIR}/.."
8
-
9
- readonly SAMPLE_JSON='{"model":{"id":"claude-sonnet-4-20250514","display_name":"Sonnet 4"},"cost":{"total_cost_usd":0.42,"message_cost_usd":0.03,"duration":"15m"},"context_window":{"context_window_size":200000,"used_percentage":35,"current_usage":{"input_tokens":50000,"cache_creation_input_tokens":10000,"cache_read_input_tokens":5000}},"cwd":"/home/user/my-project","workspace":{"current_dir":"/home/user/my-project"},"session_id":"abc123"}'
10
-
11
- setup_tui_transcript() {
12
- local now_iso user_iso cwd_encoded transcript_dir
13
- now_iso=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
14
- user_iso=$(date -u -d "-13 seconds" +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null \
15
- || date -u -v-13S +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null \
16
- || echo "${now_iso}")
17
-
18
- # Encode the preview CWD the same way Claude Code encodes project dirs
19
- # On Windows: C:\Users\foo\proj -> C--Users-foo-proj
20
- # On Unix: /home/user/proj -> -home-user-proj
21
- local preview_cwd
22
- # Create a temp git repo so previews always show git data
23
- local fake_repo
24
- fake_repo="$(mktemp -d)/my-project"
25
- mkdir -p "${fake_repo}"
26
- git -C "${fake_repo}" init -b main --quiet 2>/dev/null
27
- git -C "${fake_repo}" -c user.name="User" -c user.email="u@e.co" commit --allow-empty -m "init" --quiet 2>/dev/null
28
- git -C "${fake_repo}" checkout -b feat/my-feature --quiet 2>/dev/null
29
- touch "${fake_repo}/newfile.txt"
30
- printf 'change\n' > "${fake_repo}/README.md"
31
- git -C "${fake_repo}" add README.md 2>/dev/null
32
-
33
- # Use native path for Node.js compatibility (Windows needs C:\... not /c/...)
34
- preview_cwd="$(cd "${fake_repo}" && pwd -W 2>/dev/null || pwd)"
35
- cwd_encoded="$(printf '%s' "${preview_cwd}" | sed 's|[/\\]|-|g; s|:||g')"
36
- TUI_PREVIEW_CWD="${preview_cwd}"
37
-
38
- transcript_dir="${HOME}/.claude/projects/${cwd_encoded}"
39
- TUI_TRANSCRIPT="${transcript_dir}/preview-123.jsonl"
40
- mkdir -p "${transcript_dir}"
41
- printf '{"timestamp":"%s","type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}\n' "${user_iso}" > "${TUI_TRANSCRIPT}"
42
- printf '{"timestamp":"%s","type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"done"}]}}\n' "${now_iso}" >> "${TUI_TRANSCRIPT}"
43
- }
44
-
45
- make_tui_sample_json() {
46
- local resets_5h resets_7d
47
- resets_5h=$(( $(date +%s) + 15480 ))
48
- resets_7d=$(( $(date +%s) + 432000 ))
49
-
50
- printf '{"model":{"id":"claude-sonnet-4-20250514","display_name":"Sonnet 4"},"cost":{"total_cost_usd":2.85,"message_cost_usd":0.12,"total_duration_ms":16200000,"total_api_duration_ms":480000,"total_lines_added":342,"total_lines_removed":87},"context_window":{"context_window_size":200000,"used_percentage":42,"total_input_tokens":72000,"total_output_tokens":12000,"current_usage":{"input_tokens":72000,"output_tokens":12000,"cache_creation_input_tokens":15000,"cache_read_input_tokens":8000}},"rate_limits":{"five_hour":{"used_percentage":35,"resets_at":%d},"seven_day":{"used_percentage":28,"resets_at":%d}},"cwd":"%s","workspace":{"current_dir":"%s","project_dir":"%s"},"session_id":"preview-123","version":"1.0.47"}' \
51
- "${resets_5h}" "${resets_7d}" "${TUI_PREVIEW_CWD}" "${TUI_PREVIEW_CWD}" "${TUI_PREVIEW_CWD}"
52
- }
53
-
54
- readonly PREVIEW_CONFIG='{
55
- "theme": "dark",
56
- "display": {
57
- "style": "minimal",
58
- "charset": "unicode",
59
- "colorCompatibility": "auto",
60
- "autoWrap": true,
61
- "padding": 1,
62
- "lines": [
63
- {
64
- "segments": {
65
- "directory": { "enabled": true, "style": "fish" },
66
- "git": { "enabled": true },
67
- "model": { "enabled": true },
68
- "session": { "enabled": true, "type": "cost", "costSource": "calculated" },
69
- "today": { "enabled": true, "type": "cost" },
70
- "context": { "enabled": true, "showPercentageOnly": false, "displayStyle": "text", "autocompactBuffer": 33000 }
71
- }
72
- }
73
- ]
74
- },
75
- "budget": {
76
- "session": { "warningThreshold": 80 },
77
- "today": { "amount": 50, "warningThreshold": 80 }
78
- }
79
- }'
80
-
81
- THEME="dark"
82
- STYLE="minimal"
83
- CHARSET="unicode"
84
- COMPARE_STYLES=false
85
- COMPARE_THEMES=false
86
- COMPARE_TUI_LAYOUTS=false
87
- BIN=""
88
- TEMP_FILES=()
15
+ readonly TEMPLATE="${PLUGIN_ROOT}/templates/config.json"
16
+
17
+ # The options the wizard offers, one list per template placeholder. Each name
18
+ # must be one the daemon accepts; the test pins that against the real domains.
19
+ readonly THEMES=(dark light nord tokyo-night rose-pine gruvbox catppuccin-mocha dracula solarized-dark monokai)
20
+ readonly STYLES=(powerline capsule plain)
21
+ readonly CHARSETS=(unicode ascii)
22
+ readonly PRESETS=(default compact verbose)
23
+
24
+ THEME="${THEMES[0]}"
25
+ STYLE="${STYLES[0]}"
26
+ CHARSET="${CHARSETS[0]}"
27
+ PRESET="${PRESETS[0]}"
28
+ COMPARE=""
29
+
30
+ WORK="$(mktemp -d)"
31
+ readonly WORK
32
+ readonly REPO="${WORK}/my-project"
33
+ readonly TRANSCRIPT="${WORK}/preview.jsonl"
34
+ CLIENT=()
89
35
 
90
36
  # shellcheck disable=SC2329
91
37
  cleanup() {
92
- for f in "${TEMP_FILES[@]}"; do
93
- rm -f "${f}"
94
- done
38
+ rm -rf "${WORK}"
95
39
  }
96
40
  trap cleanup EXIT
97
41
 
98
- test_binary() {
99
- local bin="$1"
100
- printf '{}' | "${bin}" --help >/dev/null 2>&1
101
- }
102
-
103
- find_binary() {
104
- local npm_bin="${PLUGIN_ROOT}/../bin/cc-candybar"
105
- local dist_file="${PLUGIN_ROOT}/../dist/index.mjs"
106
- if [[ -f "${npm_bin}" ]] && [[ -f "${dist_file}" ]] && test_binary "${npm_bin}"; then
107
- printf '%s' "${npm_bin}"
42
+ # The statusline client: this checkout's shim (developing against a checkout),
43
+ # an installed cc-candybar on PATH, else the published package via npx.
44
+ find_client() {
45
+ local checkout_bin="${PLUGIN_ROOT}/../bin/cc-candybar"
46
+ if [[ -x "${checkout_bin}" ]] && [[ -f "${PLUGIN_ROOT}/../dist/index.mjs" ]]; then
47
+ CLIENT=("${checkout_bin}")
108
48
  return 0
109
49
  fi
110
-
111
50
  local path_bin
112
- if path_bin="$(command -v cc-candybar 2>/dev/null)" && test_binary "${path_bin}"; then
113
- printf '%s' "${path_bin}"
51
+ if path_bin="$(command -v cc-candybar)"; then
52
+ CLIENT=("${path_bin}")
114
53
  return 0
115
54
  fi
116
-
117
- printf 'npx'
118
- return 0
55
+ CLIENT=(npx -y @promptctl/cc-candybar@latest)
119
56
  }
120
57
 
121
- make_temp_config() {
122
- local preview_theme="$1"
123
- local preview_style="$2"
124
- local preview_charset="$3"
125
- local tmp
126
-
127
- tmp="$(mktemp)"
128
- TEMP_FILES+=("${tmp}")
129
- printf '%s' "${PREVIEW_CONFIG}" |
130
- sed -e "s/\"theme\": \"dark\"/\"theme\": \"${preview_theme}\"/" \
131
- -e "s/\"style\": \"minimal\"/\"style\": \"${preview_style}\"/" \
132
- -e "s/\"charset\": \"unicode\"/\"charset\": \"${preview_charset}\"/" \
133
- >"${tmp}"
134
- printf '%s' "${tmp}"
58
+ # A git repo on a feature branch with one untracked file, so the git segment has
59
+ # something to show, and a two-turn transcript for the session-derived segments.
60
+ make_fixture() {
61
+ mkdir -p "${REPO}"
62
+ git -C "${REPO}" init -b main --quiet
63
+ git -C "${REPO}" -c user.name="User" -c user.email="u@e.co" commit --allow-empty -m "init" --quiet
64
+ git -C "${REPO}" checkout -b feat/my-feature --quiet
65
+ touch "${REPO}/newfile.txt"
66
+
67
+ local now_iso
68
+ now_iso="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
69
+ printf '{"timestamp":"%s","type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}\n' "${now_iso}" >"${TRANSCRIPT}"
70
+ printf '{"timestamp":"%s","type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"done"}]}}\n' "${now_iso}" >>"${TRANSCRIPT}"
135
71
  }
136
72
 
137
- run_preview() {
138
- local preview_theme="$1"
139
- local preview_style="$2"
140
- local preview_charset="$3"
141
- local tmp_config
142
-
143
- tmp_config="$(make_temp_config "${preview_theme}" "${preview_style}" "${preview_charset}")"
144
-
145
- if [[ "${BIN}" == "npx" ]]; then
146
- printf '%s' "${SAMPLE_JSON}" | FORCE_COLOR=3 npx -y @promptctl/cc-candybar@latest \
147
- --config="${tmp_config}"
148
- else
149
- printf '%s' "${SAMPLE_JSON}" | FORCE_COLOR=3 "${BIN}" \
150
- --config="${tmp_config}"
151
- fi
73
+ # One Claude Code status hook payload, shaped like the daemon's required input.
74
+ sample_json() {
75
+ printf '{"hook_event_name":"Status","session_id":"preview-123","transcript_path":"%s","cwd":"%s","workspace":{"current_dir":"%s","project_dir":"%s"},"model":{"id":"claude-opus-4-8","display_name":"Opus 4.8"},"version":"1.0.47","cost":{"total_cost_usd":2.85,"total_duration_ms":16200000,"total_api_duration_ms":480000,"total_lines_added":342,"total_lines_removed":87},"context_window":{"context_window_size":200000,"used_percentage":42,"total_input_tokens":72000,"total_output_tokens":12000,"current_usage":{"input_tokens":72000,"output_tokens":12000,"cache_creation_input_tokens":15000,"cache_read_input_tokens":8000}},"rate_limits":{"five_hour":{"used_percentage":35,"resets_at":%d},"seven_day":{"used_percentage":28,"resets_at":%d}}}' \
76
+ "${TRANSCRIPT}" "${REPO}" "${REPO}" "${REPO}" \
77
+ "$(($(date +%s) + 15480))" "$(($(date +%s) + 432000))"
152
78
  }
153
79
 
154
- run_preview_config() {
155
- local config_file="$1"
156
- local sample_data="$2"
157
-
158
- if [[ "${BIN}" == "npx" ]]; then
159
- printf '%s' "${sample_data}" | FORCE_COLOR=3 npx -y @promptctl/cc-candybar@latest \
160
- --config="${config_file}"
161
- else
162
- printf '%s' "${sample_data}" | FORCE_COLOR=3 "${BIN}" \
163
- --config="${config_file}"
164
- fi
80
+ # The one render request, sent from inside the fixture repo (the daemon reads
81
+ # git state from the client's working directory).
82
+ render_request() {
83
+ (cd "${REPO}" && sample_json | "${CLIENT[@]}" "$@")
165
84
  }
166
85
 
167
- run_compare_tui_layouts() {
168
- local layouts=(compact standard full)
169
- local label
170
- local template_file tmp tui_json fake_repo
171
-
172
- setup_tui_transcript
173
- tui_json="$(make_tui_sample_json)"
174
- fake_repo="${TUI_PREVIEW_CWD}"
86
+ probe_daemon() {
87
+ { "${CLIENT[@]}" daemon-stats --json >/dev/null; } 2>&1
88
+ }
175
89
 
176
- for label in "${layouts[@]}"; do
177
- template_file="${PLUGIN_ROOT}/templates/config-tui-${label}.json"
178
- if [[ ! -f "${template_file}" ]]; then
179
- printf '%s: template not found\n\n' "${label}"
180
- continue
90
+ # The first request on a cold daemon spawns it in the background and prints an
91
+ # empty line; a preview must not be that line. Kick once, then wait until the
92
+ # daemon answers or the deadline passes. The bound is wall-clock, not a probe
93
+ # count: a probe through npx costs seconds, a native one milliseconds.
94
+ ensure_daemon() {
95
+ local probe deadline=$((SECONDS + 15))
96
+ if probe="$(probe_daemon)"; then
97
+ return 0
98
+ fi
99
+ render_request >/dev/null
100
+ until probe="$(probe_daemon)"; do
101
+ if ((SECONDS >= deadline)); then
102
+ printf 'preview: the cc-candybar daemon did not start within 15s: %s\n' "${probe}" >&2
103
+ exit 1
181
104
  fi
182
-
183
- tmp="$(mktemp)"
184
- TEMP_FILES+=("${tmp}")
185
- sed -e "s/replace:THEME/${THEME}/g" \
186
- -e "s/replace:CHARSET/${CHARSET}/g" \
187
- -e "s/\"replace:TODAY_BUDGET\"/50/g" \
188
- "${template_file}" >"${tmp}"
189
-
190
- printf '%s:\n' "${label}"
191
- run_preview_config "${tmp}" "${tui_json}"
192
- printf '\n\n'
105
+ sleep 0.1
193
106
  done
194
-
195
- # Clean up fake transcript and git repo
196
- rm -f "${TUI_TRANSCRIPT}" 2>/dev/null
197
- rm -rf "${fake_repo%/my-project}" 2>/dev/null
198
107
  }
199
108
 
200
- run_compare_styles() {
201
- local styles=(minimal powerline capsule tui)
202
- local s
203
-
204
- for s in "${styles[@]}"; do
205
- printf '%s:\n' "${s}"
206
- run_preview "${THEME}" "${s}" "${CHARSET}"
207
- printf '\n\n'
208
- done
109
+ # The wizard's substitution, exactly: the template with its four placeholders
110
+ # filled. Prints the written path.
111
+ fill_template() {
112
+ local theme="$1" style="$2" charset="$3" preset="$4"
113
+ local out="${WORK}/config-${theme}-${style}-${charset}-${preset}.json"
114
+ sed -e "s/replace:THEME/${theme}/" \
115
+ -e "s/replace:STYLE/${style}/" \
116
+ -e "s/replace:CHARSET/${charset}/" \
117
+ -e "s/replace:PRESET/${preset}/" \
118
+ "${TEMPLATE}" >"${out}"
119
+ printf '%s' "${out}"
209
120
  }
210
121
 
211
- run_compare_themes() {
212
- local themes=(dark light nord tokyo-night rose-pine gruvbox)
213
- local t
214
-
215
- for t in "${themes[@]}"; do
216
- printf '%s:\n' "${t}"
217
- run_preview "${t}" "${STYLE}" "${CHARSET}"
218
- printf '\n\n'
219
- done
122
+ render() {
123
+ render_request --config="$(fill_template "$@")"
124
+ printf '\n'
220
125
  }
221
126
 
222
127
  parse_args() {
223
128
  while [[ $# -gt 0 ]]; do
224
129
  case "$1" in
225
- --theme=*)
226
- THEME="${1#*=}"
227
- shift
228
- ;;
229
- --theme)
230
- [[ $# -ge 2 ]] || {
231
- printf 'Missing value for --theme\n' >&2
232
- exit 1
233
- }
234
- THEME="$2"
235
- shift 2
236
- ;;
237
- --style=*)
238
- STYLE="${1#*=}"
239
- shift
240
- ;;
241
- --style)
242
- [[ $# -ge 2 ]] || {
243
- printf 'Missing value for --style\n' >&2
244
- exit 1
245
- }
246
- STYLE="$2"
247
- shift 2
248
- ;;
249
- --charset=*)
250
- CHARSET="${1#*=}"
251
- shift
252
- ;;
253
- --charset)
254
- [[ $# -ge 2 ]] || {
255
- printf 'Missing value for --charset\n' >&2
256
- exit 1
257
- }
258
- CHARSET="$2"
259
- shift 2
260
- ;;
261
- --compare-styles)
262
- COMPARE_STYLES=true
263
- shift
264
- ;;
265
- --compare-themes)
266
- COMPARE_THEMES=true
267
- shift
268
- ;;
269
- --compare-tui-layouts)
270
- COMPARE_TUI_LAYOUTS=true
271
- shift
272
- ;;
130
+ --theme=*) THEME="${1#*=}" ;;
131
+ --style=*) STYLE="${1#*=}" ;;
132
+ --charset=*) CHARSET="${1#*=}" ;;
133
+ --preset=*) PRESET="${1#*=}" ;;
134
+ --compare-themes) COMPARE="themes" ;;
135
+ --compare-styles) COMPARE="styles" ;;
136
+ --compare-charsets) COMPARE="charsets" ;;
137
+ --compare-presets) COMPARE="presets" ;;
273
138
  *)
274
139
  printf 'Unknown option: %s\n' "$1" >&2
275
- exit 1
140
+ printf 'Usage: preview.sh [--theme=NAME] [--style=NAME] [--charset=NAME] [--preset=NAME] [--compare-themes|--compare-styles|--compare-charsets|--compare-presets]\n' >&2
141
+ exit 2
276
142
  ;;
277
143
  esac
144
+ shift
278
145
  done
279
146
  }
280
147
 
281
148
  main() {
282
149
  parse_args "$@"
283
- BIN="$(find_binary)"
284
-
285
- if [[ "${COMPARE_STYLES}" == "true" ]]; then
286
- run_compare_styles
287
- exit 0
288
- fi
289
-
290
- if [[ "${COMPARE_THEMES}" == "true" ]]; then
291
- run_compare_themes
292
- exit 0
293
- fi
294
-
295
- if [[ "${COMPARE_TUI_LAYOUTS}" == "true" ]]; then
296
- run_compare_tui_layouts
297
- exit 0
298
- fi
299
-
300
- run_preview "${THEME}" "${STYLE}" "${CHARSET}"
301
- printf '\n'
302
- exit 0
150
+ find_client
151
+ make_fixture
152
+ ensure_daemon
153
+ # The flag values are checked once, as the wizard's user would see them
154
+ # (exit 1 names the error). A compare varies one axis over its array, whose
155
+ # members test/plugin-templates.test.ts pins to the loader's own domain.
156
+ "${CLIENT[@]}" check "$(fill_template "${THEME}" "${STYLE}" "${CHARSET}" "${PRESET}")" >/dev/null
157
+
158
+ local name
159
+ case "${COMPARE}" in
160
+ "") render "${THEME}" "${STYLE}" "${CHARSET}" "${PRESET}" ;;
161
+ themes) for name in "${THEMES[@]}"; do printf '%s:\n' "${name}"; render "${name}" "${STYLE}" "${CHARSET}" "${PRESET}"; printf '\n'; done ;;
162
+ styles) for name in "${STYLES[@]}"; do printf '%s:\n' "${name}"; render "${THEME}" "${name}" "${CHARSET}" "${PRESET}"; printf '\n'; done ;;
163
+ charsets) for name in "${CHARSETS[@]}"; do printf '%s:\n' "${name}"; render "${THEME}" "${STYLE}" "${name}" "${PRESET}"; printf '\n'; done ;;
164
+ presets) for name in "${PRESETS[@]}"; do printf '%s:\n' "${name}"; render "${THEME}" "${STYLE}" "${CHARSET}" "${name}"; printf '\n'; done ;;
165
+ esac
303
166
  }
304
167
 
305
168
  main "$@"