9e37213cd4
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
214 lines
6.6 KiB
HTML
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>
|