Files
Edward Langley 9e37213cd4 fix(docs): re-record the four casts from a script, with no shell to leak
improvise-2oxt. All four casts under docs/casts/ opened and closed on
`(0) Mac:<hostname> ~gf/u/edwlan/improvise` — a real hostname and home directory
on what improvise-kh8 made the site root — and `formulas.cast` and `import.cast`
showed that prompt as their visible OPENING frame. drill.cast also predated the
records columns entirely, showing a single `Value` column where the current
build draws a column per measure plus `+cat` and `+`.

The repair is structural, not careful. `docs/record-casts.py` drives `improvise`
on a pty it owns and writes asciicast v2 itself, so `improvise` is the pty's ONLY
child and there is no shell for a prompt to leak from — nothing to remember to
clean, and nothing that comes back when somebody re-records on another machine.
Three other things stopped being accidents with it: the geometry is an input
(120x37, the numbers docs/index.html already spelled, so they did not change),
the pacing is a per-step table, and the header carries no `env` (which held
$SHELL) and no `timestamp` (the recording machine's clock).

Two decisions that each cost a re-record. No `:q` is typed at the end: the player
loops, so the last frame is also the frame before the first, and a minibuffer
reading `:q` is what it would show — the process is signalled instead. And each
scenario had to be ARRANGED so the thing it names is visible: the drill cast
drilled into a cell with no records behind it, and the pivot cast pressed `c` on
a category already on the column axis. Both recorded cleanly and demonstrated
nothing — true frames about the wrong thing — and neither was visible from the
keystroke list.

That is the method worth keeping: a cast can be replayed through a small ANSI
reconstruction and READ AS TEXT, so "what does frame N show" has an answer rather
than a guess. It is how the `+cat`/`+` columns in the new drill.cast were
confirmed present, and how both dud scenarios were caught.

The guard is `landing.spec.js`, asserted over the cast FILES: no header carries
`env` or `timestamp`, the geometry is the one the page configures, and no chunk
contains an OSC window-title sequence — the leak's signature, emitted by a shell
once per prompt with the cwd in it and by `improvise` never. Asserted over the
file rather than the rendering because a player draws a prompt as ordinary text,
so a screenshot assertion would have to know what the hostname had been. BOTH
clauses were injected separately, because the `env` one would otherwise mask the
OSC one: the old cast fails on `env`, and the old cast with `env` stripped fails
on the title sequence.

docs/index.html loses the `p.dated` note admitting the drill cast was out of
date, which is now false.

Tests: 33 harness + 3 skipped (was 28 + 3). Rust and node unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 20:52:24 -07:00

214 lines
6.6 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>improvise — terminal pivot-table modeling</title>
<meta
name="description"
content="Terminal pivot-table modeling in the spirit of Lotus Improv: multidimensional cells, formulas over dimensions instead of cell addresses, vim-style keybindings."
/>
<!-- improvise-e61: the site root, linking to the app under ./app/.
120x37 is an INPUT now, not whatever the recorder had: the casts are
made by docs/record-casts.py, which sets it (improvise-2oxt). -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/asciinema-player@3.10.0/dist/bundle/asciinema-player.min.css"
/>
<style>
:root {
--bg: #14161a;
--panel: #1b1e24;
--fg: #d7dae0;
--dim: #8b93a1;
--accent: #8ec07c;
--rule: #2b2f37;
--mono: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
}
main {
max-width: 1000px;
margin: 0 auto;
padding: 3rem 1rem 5rem;
}
h1,
h2 {
font-family: var(--mono);
font-weight: 600;
letter-spacing: -0.01em;
}
h1 {
font-size: 2.4rem;
margin: 0 0 0.4rem;
color: var(--accent);
}
h2 {
font-size: 1.15rem;
margin: 3rem 0 0.3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
}
.tagline {
color: var(--dim);
font-size: 1.1rem;
max-width: 62ch;
margin: 0 0 1.5rem;
}
p.note {
color: var(--dim);
margin: 0 0 1rem;
max-width: 72ch;
}
a {
color: var(--accent);
}
.cta {
display: inline-block;
margin: 0 0.6rem 0.6rem 0;
padding: 0.55rem 1.1rem;
border: 1px solid var(--rule);
border-radius: 6px;
background: var(--panel);
font-family: var(--mono);
text-decoration: none;
}
.cta:hover {
border-color: var(--accent);
}
.cta.primary {
border-color: var(--accent);
color: var(--bg);
background: var(--accent);
font-weight: 600;
}
.player {
background: var(--panel);
border: 1px solid var(--rule);
border-radius: 6px;
overflow: hidden;
}
/* A 120-column cast cannot fit a phone: the player DERIVES its font size
from the width it is given, so 390px gave 4.83px (improvise-o31u). */
@media (max-width: 900px) {
.player {
overflow-x: auto;
}
/* The overflow must stay INSIDE .player. A page that scrolls sideways
breaks the whole document rather than one section of it. */
.player > * {
min-width: 880px;
}
}
pre {
background: var(--panel);
border: 1px solid var(--rule);
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
font-family: var(--mono);
font-size: 0.9rem;
}
footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--rule);
color: var(--dim);
font-size: 0.9rem;
}
</style>
</head>
<body>
<main>
<h1>improvise</h1>
<p class="tagline">
Terminal pivot-table modeling in the spirit of Lotus Improv —
multidimensional cells, formulas over dimensions instead of cell
addresses, and vim-style keybindings for reassigning axes on the fly.
</p>
<p>
<a class="cta primary" href="./app/"
>▶ Run improvise in your browser</a
>
<a class="cta" href="./docs/">Documentation</a>
<a class="cta" href="https://github.com/fiddlerwoaroof/improvise"
>Source on GitHub</a
>
</p>
<p class="note">
No install, no server, no account: the whole application runs in a Web
Worker on this machine and the document lives in your browser's private
storage.
</p>
<h2>Reassign an axis</h2>
<p class="note">
A category lives on exactly one axis. Moving it rewrites the grid;
nothing in the data or the formulas changes.
</p>
<div class="player" id="cast-pivot"></div>
<h2>Drill into an aggregate</h2>
<p class="note">
An aggregated cell opens onto the records behind it, editable in place.
</p>
<div class="player" id="cast-drill"></div>
<h2>Formulas over dimensions</h2>
<p class="note">
<code>Profit = Revenue - Cost</code> names categories and items, not
<code>B3</code>, so it survives every rearrangement of the view.
</p>
<div class="player" id="cast-formulas"></div>
<h2>Import a CSV</h2>
<p class="note">
The wizard classifies each column as a category, a measure or a time
field, and builds the model from what you confirm.
</p>
<div class="player" id="cast-import"></div>
<h2>Install</h2>
<pre>
# Nix
nix profile install github:fiddlerwoaroof/improvise
# Prebuilt binaries (Linux x86_64/aarch64, macOS Intel and Apple Silicon)
# https://github.com/fiddlerwoaroof/improvise/releases</pre
>
<p class="note">
Not yet on crates.io; see the releases page for the current prerelease.
</p>
<footer>
Apache-2.0 ·
<a href="https://github.com/fiddlerwoaroof/improvise">GitHub</a> ·
<a href="./app/">Live browser demo</a> ·
<a href="./docs/">Documentation</a>
</footer>
</main>
<script src="https://cdn.jsdelivr.net/npm/asciinema-player@3.10.0/dist/bundle/asciinema-player.min.js"></script>
<script>
// One loop rather than four copies of the same call: the config is the
// same for every cast and a second spelling of it is a second answer.
for (const name of ["pivot", "drill", "formulas", "import"]) {
AsciinemaPlayer.create(
`./casts/${name}.cast`,
document.getElementById(`cast-${name}`),
// No idleTimeLimit: every cast header already carries 2.0, and a
// second spelling of it here is a second answer to one question.
{ cols: 120, rows: 37, theme: "monokai", autoPlay: false, loop: true },
);
}
</script>
</body>
</html>