{"_id":"@benjamin-small/browser-terminal","_rev":"5-464d66d0e492395f027ae9c8793939e7","name":"@benjamin-small/browser-terminal","dist-tags":{"latest":"0.5.0"},"versions":{"0.1.0":{"name":"@benjamin-small/browser-terminal","version":"0.1.0","keywords":["terminal","tmux","shell","webassembly","wasm","xterm","repl","structured-data","pipeline","rust"],"author":{"name":"Benjamin Small"},"license":"Apache-2.0","_id":"@benjamin-small/browser-terminal@0.1.0","maintainers":[{"name":"ben-small","email":"benjamin.small83@gmail.com"}],"homepage":"https://benjamin-small.github.io/browser-terminal/","bugs":{"url":"https://github.com/benjamin-small/browser-terminal/issues"},"dist":{"shasum":"f231785e1556649f4969b77134c8483d4dd96e58","tarball":"https://registry.npmjs.org/@benjamin-small/browser-terminal/-/browser-terminal-0.1.0.tgz","fileCount":21,"integrity":"sha512-0XOmw6Rn/yHkvjHiTI/mQgo5kSj9cr2GiQc3ZhelPltjenDVakusrDmr16i19xZGyTi3l1JjX31ZOM1Go78M5A==","signatures":[{"sig":"MEUCIQCJNtkjliZSN/wm+J9/qKbwOOsB9P2trbkTt9Y8GfhBAQIgVvQQIXmMCOXD4FyI7Gcfi/aEg9pSrkjlTXFlBw7+8EU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjamin-small%2fbrowser-terminal@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":568600},"main":"./dist/index.js","type":"module","_from":"file:/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.1.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"node scripts/gen-xterm-css.mjs && tsc -p tsconfig.json && rm -rf dist/wasm && cp -R src/wasm dist/wasm && cp ../../LICENSE ./LICENSE","prepublishOnly":"node scripts/check-publishable.mjs"},"_npmUser":{"name":"ben-small","email":"benjamin.small83@gmail.com"},"_resolved":"/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.1.0.tgz","_integrity":"sha512-0XOmw6Rn/yHkvjHiTI/mQgo5kSj9cr2GiQc3ZhelPltjenDVakusrDmr16i19xZGyTi3l1JjX31ZOM1Go78M5A==","repository":{"url":"git+https://github.com/benjamin-small/browser-terminal.git","type":"git","directory":"packages/browser-terminal"},"_npmVersion":"10.8.2","description":"A terminal/tmux experience for any web page, powered by a Rust/WASM core with structured-value pipes.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","dependencies":{"@xterm/xterm":"^6.0.0","@xterm/addon-fit":"^0.11.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/browser-terminal_0.1.0_1785104888397_0.5492146137970648","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@benjamin-small/browser-terminal","version":"0.2.0","keywords":["terminal","tmux","shell","webassembly","wasm","xterm","repl","structured-data","pipeline","rust"],"author":{"name":"Benjamin Small"},"license":"Apache-2.0","_id":"@benjamin-small/browser-terminal@0.2.0","maintainers":[{"name":"ben-small","email":"benjamin.small83@gmail.com"}],"homepage":"https://benjamin-small.github.io/browser-terminal/","bugs":{"url":"https://github.com/benjamin-small/browser-terminal/issues"},"dist":{"shasum":"8fb7328514cf01b4e84bc7fce5c74a731951d7e0","tarball":"https://registry.npmjs.org/@benjamin-small/browser-terminal/-/browser-terminal-0.2.0.tgz","fileCount":21,"integrity":"sha512-10+SBAPVcehmiR8JJOfTIAn3cWEwLzkQVpvbRGifQ1oOVClbMcoAhcA+UyFLEw/b6ZEchKDf5kMHg7vx3jDmYQ==","signatures":[{"sig":"MEUCIQC/U5Z6fC/gAfC6a/fcCrGRSiDuU29CnCb15nN//nxO8AIgcy/ohbfES/K0UxVWWicurLrpjdPkDiD4rPwY+IJNF/I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjamin-small%2fbrowser-terminal@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":608403},"main":"./dist/index.js","type":"module","_from":"file:/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.2.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"node scripts/gen-xterm-css.mjs && tsc -p tsconfig.json && rm -rf dist/wasm && cp -R src/wasm dist/wasm && cp ../../LICENSE ./LICENSE","prepublishOnly":"node scripts/check-publishable.mjs"},"_npmUser":{"name":"ben-small","email":"benjamin.small83@gmail.com"},"_resolved":"/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.2.0.tgz","_integrity":"sha512-10+SBAPVcehmiR8JJOfTIAn3cWEwLzkQVpvbRGifQ1oOVClbMcoAhcA+UyFLEw/b6ZEchKDf5kMHg7vx3jDmYQ==","repository":{"url":"git+https://github.com/benjamin-small/browser-terminal.git","type":"git","directory":"packages/browser-terminal"},"_npmVersion":"10.8.2","description":"A terminal/tmux experience for any web page, powered by a Rust/WASM core with structured-value pipes.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","dependencies":{"@xterm/xterm":"^6.0.0","@xterm/addon-fit":"^0.11.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/browser-terminal_0.2.0_1786408557023_0.9760044186172172","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@benjamin-small/browser-terminal","version":"0.3.0","keywords":["terminal","tmux","shell","webassembly","wasm","xterm","repl","structured-data","pipeline","rust"],"author":{"name":"Benjamin Small"},"license":"Apache-2.0","_id":"@benjamin-small/browser-terminal@0.3.0","maintainers":[{"name":"ben-small","email":"benjamin.small83@gmail.com"}],"homepage":"https://benjamin-small.github.io/browser-terminal/","bugs":{"url":"https://github.com/benjamin-small/browser-terminal/issues"},"dist":{"shasum":"a666709502160461f9b746c9985758a462b4bee6","tarball":"https://registry.npmjs.org/@benjamin-small/browser-terminal/-/browser-terminal-0.3.0.tgz","fileCount":21,"integrity":"sha512-/qorG7B1STk0DgZPo9Aef9MChpHlZZi8SHt3dXuLHux/nh4KsaRbr6BWZ+MX14QKN8JcqxUhbTK7vMXHF0Wwcw==","signatures":[{"sig":"MEQCIHMw0g7sTC7/ECW7T+M/EW32TzNT1x113HTr44wkAKVAAiAMrIve9PIGVI6mkyW1Yja2DhQuayA0ybC1heCMks6Utw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCT8W2WmkQqyAEguMLpxj17NIH6cK5XCmo3LzII7hM1HAIgDsskNKyy9/CosDOs27M2/xDkIvlqWRYP9G7T+8KELQI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjamin-small%2fbrowser-terminal@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":640072},"main":"./dist/index.js","type":"module","_from":"file:/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.3.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"node scripts/gen-xterm-css.mjs && tsc -p tsconfig.json && rm -rf dist/wasm && cp -R src/wasm dist/wasm && cp ../../LICENSE ./LICENSE","prepublishOnly":"node scripts/check-publishable.mjs"},"_npmUser":{"name":"ben-small","email":"benjamin.small83@gmail.com"},"_resolved":"/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.3.0.tgz","_integrity":"sha512-/qorG7B1STk0DgZPo9Aef9MChpHlZZi8SHt3dXuLHux/nh4KsaRbr6BWZ+MX14QKN8JcqxUhbTK7vMXHF0Wwcw==","repository":{"url":"git+https://github.com/benjamin-small/browser-terminal.git","type":"git","directory":"packages/browser-terminal"},"_npmVersion":"10.8.2","description":"A terminal/tmux experience for any web page, powered by a Rust/WASM core with structured-value pipes.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","dependencies":{"@xterm/xterm":"^6.0.0","@xterm/addon-fit":"^0.11.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/browser-terminal_0.3.0_1790127009699_0.39500229668818565","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@benjamin-small/browser-terminal","version":"0.4.0","keywords":["terminal","tmux","shell","webassembly","wasm","xterm","repl","structured-data","pipeline","rust"],"author":{"name":"Benjamin Small"},"license":"Apache-2.0","_id":"@benjamin-small/browser-terminal@0.4.0","maintainers":[{"name":"ben-small","email":"benjamin.small83@gmail.com"}],"homepage":"https://benjamin-small.github.io/browser-terminal/","bugs":{"url":"https://github.com/benjamin-small/browser-terminal/issues"},"dist":{"shasum":"c9953bab61b0650afaa9476a0fe2046f63765962","tarball":"https://registry.npmjs.org/@benjamin-small/browser-terminal/-/browser-terminal-0.4.0.tgz","fileCount":35,"integrity":"sha512-17RuTejxd8RNaU9ujQ1mmt1vcZ/OpwzYLuEEHa1M4nc2nCrKEHjMVUkHlxVWs+ljHendE5ljjEfAF5dGdYlfjA==","signatures":[{"sig":"MEYCIQCfBY6amdBxnOE5n3ZhlWXjsGgLYqW1V3835TX30x9DTwIhANUs8Mh5Vhno4j9EhRQ1ZJfALTE1yfOWIFsNFoIz9BU7","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDKKzKvJJGJ8BJwRKxuru9AI4MoQlpN5u9OfEMDkZF2rgIhAOBi10NX0D2WBsYRp4e3sGA92zmT8OzktFpBl/TEAwJL","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjamin-small%2fbrowser-terminal@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":727714},"main":"./dist/index.js","type":"module","_from":"file:/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.4.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./filesystem":{"types":"./dist/filesystem/index.d.ts","import":"./dist/filesystem/index.js"},"./filesystem/editor":{"types":"./dist/filesystem/editor.d.ts","import":"./dist/filesystem/editor.js"}},"scripts":{"build":"node scripts/gen-xterm-css.mjs && tsc -p tsconfig.json && rm -rf dist/wasm && cp -R src/wasm dist/wasm && cp ../../LICENSE ./LICENSE","prepublishOnly":"node scripts/check-publishable.mjs","test:filesystem":"node --test tests/filesystem.test.mjs"},"_npmUser":{"name":"ben-small","email":"benjamin.small83@gmail.com"},"_resolved":"/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.4.0.tgz","_integrity":"sha512-17RuTejxd8RNaU9ujQ1mmt1vcZ/OpwzYLuEEHa1M4nc2nCrKEHjMVUkHlxVWs+ljHendE5ljjEfAF5dGdYlfjA==","repository":{"url":"git+https://github.com/benjamin-small/browser-terminal.git","type":"git","directory":"packages/browser-terminal"},"_npmVersion":"10.8.2","description":"A terminal/tmux experience for any web page, powered by a Rust/WASM core with structured-value pipes.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","dependencies":{"@xterm/xterm":"^6.0.0","@xterm/addon-fit":"^0.11.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.0"},"_npmOperationalInternal":{"tmp":"tmp/browser-terminal_0.4.0_1790649808695_0.9719615882874353","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"_id":"@benjamin-small/browser-terminal@0.5.0","bugs":{"url":"https://github.com/benjamin-small/browser-terminal/issues"},"dist":{"shasum":"280b1e4e95291ad32696a4130c2dd9ad01bcece4","tarball":"https://registry.npmjs.org/@benjamin-small/browser-terminal/-/browser-terminal-0.5.0.tgz","fileCount":35,"integrity":"sha512-x4AnCDGIQaFGq6M/iE2bxGwjaJ4gXXr/k2u+reQX9CNnfM+BAS1Wz9ZpeHre8OqrTBEPtoxYe2bDRUHqB86TKA==","signatures":[{"sig":"MEYCIQCiAFcd4637ORKjCEKUPwT7V/j501Px6wMzwEtHD2kv/wIhALU93THNDlPQyLSQATvCU0Ue3ehXolWUcto++IdhSZ7L","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIH+TiXI2HAP7FMO5V8rRgOTnQ1IvGSVWF18riEyJFEr4AiEAx3csi3LCXOxCVnsgfzulIsoOqRxTL1Jt+yBK81R/srw="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@benjamin-small%2fbrowser-terminal@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":736296},"main":"./dist/index.js","name":"@benjamin-small/browser-terminal","type":"module","_from":"file:/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.5.0.tgz","types":"./dist/index.d.ts","author":{"name":"Benjamin Small"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./filesystem":{"types":"./dist/filesystem/index.d.ts","import":"./dist/filesystem/index.js"},"./filesystem/editor":{"types":"./dist/filesystem/editor.d.ts","import":"./dist/filesystem/editor.js"}},"license":"Apache-2.0","scripts":{"build":"node scripts/gen-xterm-css.mjs && tsc -p tsconfig.json && rm -rf dist/wasm && cp -R src/wasm dist/wasm && cp ../../LICENSE ./LICENSE","prepublishOnly":"node scripts/check-publishable.mjs","test:filesystem":"node --test tests/filesystem.test.mjs"},"version":"0.5.0","_npmUser":{"name":"ben-small","email":"benjamin.small83@gmail.com"},"homepage":"https://benjamin-small.github.io/browser-terminal/","keywords":["terminal","tmux","shell","webassembly","wasm","xterm","repl","structured-data","pipeline","rust"],"_resolved":"/home/runner/work/browser-terminal/browser-terminal/packages/browser-terminal/benjamin-small-browser-terminal-0.5.0.tgz","_integrity":"sha512-x4AnCDGIQaFGq6M/iE2bxGwjaJ4gXXr/k2u+reQX9CNnfM+BAS1Wz9ZpeHre8OqrTBEPtoxYe2bDRUHqB86TKA==","repository":{"url":"git+https://github.com/benjamin-small/browser-terminal.git","type":"git","directory":"packages/browser-terminal"},"_npmVersion":"10.8.2","description":"A terminal/tmux experience for any web page, powered by a Rust/WASM core with structured-value pipes.","directories":{},"maintainers":[{"name":"ben-small","email":"benjamin.small83@gmail.com"}],"sideEffects":false,"_nodeVersion":"20.20.2","dependencies":{"@xterm/xterm":"^6.0.0","@xterm/addon-fit":"^0.11.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.6.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/browser-terminal_0.5.0_1790711500681_0.994071661895624"}}},"time":{"created":"2026-07-26T22:28:08.289Z","modified":"2026-09-29T19:51:41.202Z","0.1.0":"2026-07-26T22:28:08.546Z","0.2.0":"2026-08-11T00:35:57.223Z","0.3.0":"2026-09-23T01:30:09.800Z","0.4.0":"2026-09-29T02:43:28.793Z","0.5.0":"2026-09-29T19:51:40.796Z"},"bugs":{"url":"https://github.com/benjamin-small/browser-terminal/issues"},"author":{"name":"Benjamin Small"},"license":"Apache-2.0","homepage":"https://benjamin-small.github.io/browser-terminal/","keywords":["terminal","tmux","shell","webassembly","wasm","xterm","repl","structured-data","pipeline","rust"],"repository":{"url":"git+https://github.com/benjamin-small/browser-terminal.git","type":"git","directory":"packages/browser-terminal"},"description":"A terminal/tmux experience for any web page, powered by a Rust/WASM core with structured-value pipes.","maintainers":[{"name":"ben-small","email":"benjamin.small83@gmail.com"}],"readme":"# @benjamin-small/browser-terminal\n\nA terminal/tmux experience for any web page, powered by a Rust core compiled to\nWebAssembly.\n\nA terminal panel docks to the edge of your page and runs a shell-like language\nwith **structured-value pipes** (nushell/PowerShell style). Commands are built\ninto the Rust core or registered from TypeScript in a few lines — so a pane can\nquery the DOM, call your app's APIs, and pipe the results through filters into\nbox-drawn tables. `Ctrl-B %` splits panes, `session new` forks shells.\n\n**[Try the live demos →](https://benjamin-small.github.io/browser-terminal/)**\n\n## Install\n\n```sh\nnpm install @benjamin-small/browser-terminal\n```\n\nWorks out of the box with Vite and webpack 5 — the `.wasm` loads via\n`new URL(..., import.meta.url)`.\n\n## Quickstart\n\n`BrowserTerminal.create()` automatically mounts the origin's OPFS at writable\n`/scratch` and starts there. It loads `pwd`, `cd`, `ls`, `cat`, `read-bytes`,\nand `edit`, the text editor, path completion, and file redirection. No custom\ncommands or folder picker are needed: `echo hello > hello.txt; cat hello.txt`\nworks immediately. The prompt follows the working directory.\n\nIf OPFS is missing or access fails, creation still succeeds with the core\nshell. A warning appears in the terminal and through `console.warn`; filesystem\ncommands are not installed. `bt.filesystem` exposes the default adapter, or\n`null` when unavailable or disabled. Files belong to this origin and browser\nprofile and may be removed by clearing site data or browser eviction.\n\nUse `create({ filesystem: false })` to disable automatic storage access and\nwarnings when installing your own adapter. Importing alone does not initialize\nthe terminal or access storage.\n\n```ts\nimport { BrowserTerminal } from '@benjamin-small/browser-terminal';\n\nconst bt = await BrowserTerminal.create();\n\nbt.registerCommand(\n  {\n    name: 'links',\n    summary: 'List links on the host page',\n    flags: [{ long: 'limit', shape: 'int' }],\n  },\n  ({ flags }) =>\n    [...document.querySelectorAll('a')]\n      .slice(0, Number(flags.limit ?? 100))\n      .map((a) => ({ text: a.textContent?.trim() ?? '', href: a.href })),\n);\n```\n\nThen, in the panel:\n\n```\n❯ links --limit 20 | filter {|o| $o.text != ''} | head 5\n┌───────────────┬──────────────────────────────┐\n│ text          │ href                         │\n├───────────────┼──────────────────────────────┤\n│ Rust language │ https://www.rust-lang.org/   │\n│ …             │ …                            │\n└───────────────┴──────────────────────────────┘\n```\n\nA registered command is indistinguishable from a builtin: same `--help`, same\npiping, same did-you-mean on typos.\n\nUnquoted `key=value` operands are positional strings: a host-registered\n`dd if=/dev/hda count=1` receives `['if=/dev/hda', 'count=1']` in\n`args.positionals`. They do not set variables or bind flags; declared flags\nstill use `--name=value`. Quote the entire operand for spaces or an empty\nvalue (`'label=hello world'`, `'label='`). The language is built around\nstructured values; POSIX shell compatibility is not a goal.\n\nThe terminal also works as a scripting engine for the host page:\n\n```ts\nconst { value } = await bt.run(\"links | filter {|o| $o.text != ''} | length\");\n```\n\n`run()` resolves `{ value, log, err }` — diagnostics come back as arrays\ninstead of printing, so a background call never writes on whatever pane the\nuser is looking at. On failure or Ctrl-C it rejects with an `Error` carrying\nthe same two arrays, because a failed run is when its log matters most.\n\n## Options\n\n```ts\nawait BrowserTerminal.create({\n  filesystem,    // boolean — automatic writable OPFS at /scratch, default true\n  mount,          // HTMLElement — your own container; skips the panel chrome\n  wasmUrl,        // string | URL — custom .wasm location (CDN, no bundler)\n  wasmBinary,     // BufferSource — pre-loaded bytes; beats wasmUrl, enables\n                  //   single-file builds that run from file://\n  globalToggle,   // boolean — opt into a window-level Ctrl+` toggle\n  dock,           // 'right' (default) | 'left' | 'float'\n  dockWidth,      // number — docked width in px, default 480\n  dockTarget,     // HTMLElement — what gets padded, default document.body\n});\n```\n\nOther surface: `registerCommand` / `unregisterCommand`, `registerFn` /\n`unregisterFn` (a named function usable as `@name` in any selector position —\nthe CSP-safe alternative to inline closures), `run`, `snapshot`,\n`setPanelMode`, `panelMode`, `show` / `hide` / `toggle`, `dispose`, and the\nvariable API below (`setVariable` / `setVariables`, `getVariable`,\n`unsetVariable`, `variables`).\n\n## Writing commands\n\nA command receives `(args, input, ctx)` and may return a value or a promise;\narrays of objects render as tables automatically. Async generators stream.\n\n```ts\nbt.registerCommand({ name: 'fetch-users' }, async (_args, _input, ctx) => {\n  ctx.log('fetching…');                       // channel 3: plain\n  const res = await fetch('/api/users', { signal: ctx.signal });\n  if (!res.ok) ctx.err(`HTTP ${res.status}`); // channel 2: red\n  return res.json();                          // channel 1: the pipe\n});\n```\n\n`ctx.signal` is an `AbortSignal` — Ctrl-C cancels in-flight work. The three\nchannels are separate by construction: **nothing written to `ctx.log` or\n`ctx.err` can enter the pipe**, so a downstream `| length` is unaffected by\nanything a command logs.\n\n`ctx.session` and `ctx.pane` are read-only numeric IDs matching\n`bt.snapshot.sessions[].id` and `bt.snapshot.panes[].pane`. They identify the\npane and session that started the pipeline and remain stable even if the\nuser switches sessions while a command awaits input. Use them to keep host\nstate scoped to a session or pane:\n\n```ts\nconst directories = new Map<number, string>();\nbt.registerCommand({ name: 'cwd' }, (_args, _input, ctx) =>\n  directories.get(ctx.session) ?? '/',\n);\nbt.registerCommand(\n  { name: 'chdir', required: [{ name: 'path' }] },\n  ({ positionals }, _input, ctx) => {\n    directories.set(ctx.session, String(positionals[0]));\n  },\n);\n```\n\nSplit panes in one session share its ID; a new session gets a different ID.\nThe host owns this state, including initialization and cleanup.\n\n### Binary values\n\nReturn a `Uint8Array` to pass binary data through the pipeline. Commands,\nregistered functions, variables, and `run().value` preserve the type, also\ninside records and lists. A byte buffer is one value; it is never split\ninto individual numbers. Sliced views preserve their selected bytes, and\ncrossing the host boundary copies the data so later mutations cannot change\nstored shell values.\n\n```ts\nbt.registerCommand({ name: 'read-bytes' }, () => new Uint8Array([0, 128, 255]));\nbt.registerCommand({ name: 'consume-bytes' }, (_args, input) => {\n  if (!(input instanceof Uint8Array)) throw new Error('Expected bytes');\n  return input; // process bytes without text decoding\n});\nconst result = await bt.run('read-bytes | consume-bytes'); // Uint8Array in result.value\nawait bt.run('read-bytes | length');  // 3\nawait bt.run('read-bytes | to-json'); // JSON string: \"0080ff\"\n```\n\nAt the terminal boundary bytes display as `<3 bytes>` (including in table\ncells), without interpreting their contents as text or terminal escapes.\n`length` counts bytes; closures can also read `$value.length`. `to-json`\nencodes byte values as lowercase hex strings, including nested values.\nThe spaced names `to json`, `from json`, `str upcase`, and `str downcase`\nremain compatibility aliases. Help and command-name completion use `to-json`,\n`from-json`, `str-upcase`, and `str-downcase`.\n\nJSON has no binary type: `from-json` keeps these as strings, and ordinary\narrays of numbers remain lists. Convert an `ArrayBuffer` with\n`new Uint8Array(buffer)`. For another typed-array view, use\n`new Uint8Array(view.buffer, view.byteOffset, view.byteLength)` to preserve\nits raw bytes.\n\n### Partial output\n\nFor partial output — progress bars, in-place redraws — `ctx.log` and `ctx.err`\nare also writer objects:\n\n```ts\nctx.log.mode('byte');            // 'byte' | 'line' (default) | 'block'\nctx.log.write(`\\r${bar} ${pct}%`);\nctx.log.flush();\n```\n\nRaw writes pass through a control-character allowlist: `\\r`, `\\b`, `\\t`,\ncursor moves within the line, erase-to-end-of-line, and colors survive;\nanything that could clear the screen, position absolutely, or open an OS\ncommand sequence is stripped.\n\nNote that `ctx.log('line')` and `ctx.log.write(bytes)` are different APIs\nrather than two spellings of one. The first passes a *message* — the shell\nframes and sanitizes it. The second passes *terminal bytes* — you own the\nframing. Mixing both on one channel can interleave out of order, since the\nline call bypasses the buffer.\n\n## Structured redirection\n\nThe default OPFS setup installs filesystem hooks for `<`, `>`, and `>>`.\nFor host-managed storage, use `create({ filesystem: false })` and install both\n`read` and `write` hooks. The host decides what targets mean and how values are\nstored; the core does not convert values to text or bytes. Hooks may return promises.\n\n```ts\nconst values = new Map<string, Value>(); // import type { Value } from the package\nbt.setRedirectHandler({\n  read(target) {\n    if (!values.has(target)) throw new Error(`Unknown target: ${target}`);\n    return values.get(target)!;\n  },\n  write(target, value, { append }) {\n    if (append && values.has(target)) {\n      const previous = values.get(target);\n      if (typeof previous !== 'string' || typeof value !== 'string') {\n        throw new Error('This store only appends strings');\n      }\n      value = previous + value;\n    }\n    values.set(target, value);\n  },\n});\nawait bt.run('echo hello > greeting; echo world >> greeting');\nawait bt.run('str-upcase < greeting'); // HELLOWORLD\nbt.setRedirectHandler(null); // future lines no longer accept redirect syntax\n```\n\nInput redirection follows the first command's arguments:\n`filter {|row| $row.active} < source | length`. Output redirection follows\nthe final command: `links | head 2 > target`. A pipeline may have one of\neach, including `map {|x| $x} < source > target`. Duplicate redirects,\nmissing targets, or command arguments after a redirect are errors. Targets\ncan be barewords, quoted strings, `$variables`, or interpolated strings;\nthey must resolve to strings. Quote names containing spaces or operators.\nComparison operators inside closures retain their meaning.\n\nA read follows the same pipeline rules as a command result: lists supply\nitems, and records, strings, and byte buffers are individual values. A write\ncollects the entire output in memory using the same rules as a consuming host\ncommand: list shape is preserved even for one item, an unbatched scalar stays\nscalar, and an empty stream becomes `[]`. Bytes stay `Uint8Array`. Successful\nwrites consume the value, so `run()` returns `value: null` and the terminal\ndoes not print it. `ctx.log` and `ctx.err` still reach their usual destinations.\nA failed pipeline never calls its writer, including after partial output.\n\nBoth hooks receive a context with read-only `session`, `pane`, and `signal`;\nthe writer also receives read-only `append`. IDs identify the originating\npane and session. The handler and IDs are captured for a submitted line,\nso replacing/removing the handler or switching sessions does not retarget\nwork already running. Invalid registrations leave the previous handler intact.\nThrown errors and rejected promises reject the run; `{ message, help }`\nerrors retain their help text.\n\nCtrl-C, pane closure, and disposal abort `signal` and settle the run. Pass\nthe signal to host I/O that supports cancellation. A read that resolves\nafter cancellation cannot start commands or writes. The host remains\nresponsible for cancelling its own work; completed external writes are not\nrolled back. Without a handler, redirection retains its existing parse errors.\n\n## Keyboard focus\n\nCall `bt.focus()` to focus the active pane's terminal input and `bt.blur()`\nto release its keyboard focus. Both work with the built-in panel and a custom\n`create({ mount })` container, including after switching or splitting panes.\nThey do not change panel visibility; use `bt.show()` first when needed.\nBoth methods throw after `bt.dispose()`.\n\n## Terminal appearance\n\nSet colors and fonts when creating the terminal. Settings apply to every pane,\nincluding new splits, windows, and sessions, with either the built-in panel or\na custom mount:\n\n```ts\nconst bt = await BrowserTerminal.create({\n  terminal: {\n    theme: { background: '#ffffff', foreground: '#222222', cursor: '#0066cc' },\n    fontFamily: 'Menlo, monospace',\n    fontSize: 14,\n  },\n});\n\nbt.setTheme({ background: '#181825', foreground: '#eeeeee', cursor: '#ffffff' });\n```\n\n`setTheme()` replaces the theme for all existing and future panes, leaving fonts\nunchanged. Omitted colors use xterm defaults, except the background defaults to\n`#181825`; `bt.setTheme({})` restores those defaults. The default font size is\n13px, with xterm's default font family. `ITheme` and `TerminalOptions` are exported\ntypes. `setTheme()` throws after disposal. These settings style the terminal\npanes; the surrounding panel chrome keeps its existing styling.\n\n## Host state as shell variables\n\nFor state that belongs in the prompt, call `bt.setPrompt('/mnt ')` to show\n`/mnt ❯`. Include any desired spacing, and pass `''` to clear the prefix.\nIt applies to every existing and future pane. Idle panes redraw immediately\nwhile preserving typed input; panes with running tasks use it at their next\nprompt. This includes programmatic `run()` calls, which do not themselves\nprint a prompt. The prefix is plain text: escape sequences and controls are\nremoved, and newlines, tabs, and carriage returns become spaces. `setPrompt()`\nthrows after disposal.\n\nFor a directory-aware prompt, supply a synchronous callback:\n\n```ts\nbt.setPrompt(({ session }) => `${filesystem.pwd(session)} `);\n```\n\nCallbacks refresh when the visible layout changes. Call `bt.refreshPrompt()`\nwhen host state changes; the filesystem's `onDirectoryChange` option can do\nthis after navigation and mount changes. Passing a string restores a static\nprefix for all panes.\n\nTab completes built-in and host-registered command names at the end of the\ninput line, including multiword commands and command positions after `|` or\n`;`. It expands a shared prefix or lists ambiguous candidates without executing\nanything. Command signatures also provide flag names (including `--help`) and\n`true`/`false` suggestions for boolean arguments and flag values. The filesystem\nadapter adds path arguments and redirect targets. Completion does not evaluate\nclosures or interpolated variables.\n\nHosts can add command-specific argument choices:\n\n```ts\nconst removeCompletion = bt.addCompletionProvider(context => {\n  if (context.command !== 'theme' || context.argumentIndex !== 0 || context.flag) return [];\n  return [{ value: 'dark' }, { value: 'light' }];\n});\n```\n\nProviders receive the command, preceding arguments, current prefix, positional\nindex or flag name, session/pane IDs, and an abort signal. They may return a\npromise. Values are filtered by prefix and quoted for insertion; use\n`{ value: 'folder/', directory: true }` for a directory. Call the returned\ncleanup function when unregistering the feature. Delayed results cannot replace\nnewer edits. Unrestricted text and numeric arguments have no automatic choices.\n\nInject application state and reference it as `$name`, instead of pasting it\ninto the command text:\n\n```ts\nbt.setVariables({\n  game: gameDefinition,      // any Value: string, number, record, list, null\n  build: currentBuild,\n});\n\nawait bt.run('simulate --game $game --build $build');\n```\n\nVariables are visible in every pane and session, including ones created\nafterwards, and inside interpolation (`\"run-$game\"`). Values cross as typed\nvalues and are never parsed as shell source, so a string containing `;` or\n`|` stays a string.\n\nA change takes effect from the next command line — a pipeline already\nrunning keeps the values it started with, so a long command cannot see one\nof its arguments change halfway through.\n\n```ts\nbt.getVariable('game');     // the value, or undefined if unset\nbt.variables();             // everything you injected\nbt.unsetVariable('game');   // true if it was set\n```\n\n`undefined` from `getVariable` means \"not set\"; `null` means you injected\n`null`. In the terminal, `vars` lists what is visible and pipes like any\nother table (`vars | grep game`).\n\n### Scoping a variable to one session\n\nBy default a variable is engine-wide. To scope one to a single session,\npass an id from `bt.snapshot.sessions`:\n\n```ts\nconst [first] = bt.snapshot!.sessions;\nbt.setVariable('scratch', 'local', { scope: 'session', session: first.id });\n```\n\nA session value shadows a host value of the same name, in that session\nonly. Reads name a layer rather than merging, so both remain readable:\n\n```ts\nbt.getVariable('scratch');                                          // the host value\nbt.getVariable('scratch', { scope: 'session', session: first.id }); // that session's\n```\n\nFor \"what would `$scratch` actually be here?\", the shell's `vars` shows\nthe resolved view with a `scope` column saying where each value came from:\n`vars | filter {|v| $v.scope == 'session'}`.\n\nSessions are addressed by explicit id rather than \"whichever is active\",\nbecause a user can switch sessions between your read and your write. An\nunknown id throws from `setVariable`, `setVariables`, `getVariable` and\n`variables`. `unsetVariable` is the one exception, returning `false`,\nbecause its `boolean` has nowhere to put an error. So `undefined` from\n`getVariable` means one thing only: the name is not set in that layer.\n\n## Limitations\n\n- **One instance per page.** `create()` throws if one is already live; call\n  `dispose()` first.\n- **Browser only.** No SSR or Jest — guard your imports accordingly.\n- A failing command's buffered `.write()` output currently prints *below* its\n  error message rather than above it. Nothing is lost, only ordered oddly, and\n  only on the error path.\n- `grep` uses JavaScript's native `RegExp`, so full regex works in the browser.\n  The native CLI has no JS engine and falls back to substring matching:\n  patterns that work in the CLI always work in the browser, not the reverse.\n\n## Links\n\n- [Live demos](https://benjamin-small.github.io/browser-terminal/) — vanilla,\n  React, and Svelte\n- [Source and development docs](https://github.com/benjamin-small/browser-terminal)\n- [Issues](https://github.com/benjamin-small/browser-terminal/issues)\n\nApache-2.0\n\n## Browser filesystem and editor\n\nThe default filesystem and editor are ready after `create()` resolves when\nOPFS is available. Use `bt.filesystem` to mount additional directories.\nAdditional mounts remain read-only unless explicitly enabled.\n\nFor a custom setup, first use `create({ filesystem: false })`. Import `installFilesystem` from\n`@benjamin-small/browser-terminal/filesystem` to connect directory handles to\n`pwd`, `cd`, structured `ls`, `cat`, `read-bytes`, and `edit`. The optional\n`@benjamin-small/browser-terminal/filesystem/editor` export supplies a small\ntext editor; hosts can provide their own instead.\n\nMounts are read-only by default. Local folder selection needs browser support\nand a user gesture; writing requires host opt-in and browser permission.\nRedirection is installed separately so the adapter does not replace existing\nhost hooks. OPFS scratch storage and experimental file-backed block access are\nalso available.\n\nSee the [filesystem guide](https://github.com/benjamin-small/browser-terminal/blob/main/docs/filesystem.md)\nfor setup, command examples, byte I/O, cancellation, and save limitations.\n","readmeFilename":"README.md"}