cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Notebooks: cells run as streams, rows arriving as the pipeline prints them

Dami committed 0806076
f (Data > Shell > Run as stream) runs a cell until it's stopped: nu
prints each value the pipeline yields as NUON on a line of its own
(nushell.Stream), a live.Stream reads them as they arrive through the
same Tail followed files use, and each poll's rows reach the region
the output was sent to as live operations and the cell's output, its
last 10,000 values. The box says ● live with the rows printed; Stop
ends every stream, keeping what it printed.
commit
0806076645ebc8789845d971f765c49a26af6654
parent
27054a2ee38b35ce6f7c7a1f63103a2acef9f76c
author
Dami <dami.etoile@gmail.com>
kind
feat risk score 1.9 of 3, Jev's estimate
34 files changed, +985 -28
MROADMAP.md2+-
Mdocs/contributing/architecture.md12+-
Mdocs/contributing/ux.md2+-
Mdocs/nushell/notebooks.md28++++-
Mdocs/reference/keys.md3+-
Me2e/screennotebook_test.go9+-
Ae2e/stream_test.go61+++++++++
Me2e/testdata/nu/nu10+-
Me2e/testdata/screens/help-light.html2+-
Me2e/testdata/screens/help-narrow.html2+-
Me2e/testdata/screens/help-wide.html2+-
Me2e/testdata/screens/help.html2+-
Ae2e/testdata/screens/notebook-stream-high-contrast.html30++++
Ae2e/testdata/screens/notebook-stream-light.html30++++
Ae2e/testdata/screens/notebook-stream.html30++++
Me2e/testdata/screens/palette-narrow.html2+-
Me2e/testdata/screens/palette-search-light.html2+-
Me2e/testdata/screens/palette-search.html2+-
Me2e/testdata/screens/palette-wide.html2+-
Me2e/testdata/screens/palette.html2+-
Ainternal/live/stream.go214++++++++++++++++++++++++++++++
Ainternal/live/stream_test.go93+++++++++++++
Minternal/nushell/nushell_test.go22+++
Ainternal/nushell/stream.go37+++++
Minternal/ui/crash.go1+
Minternal/ui/model_test.go4+
Minternal/ui/nbrun.go13++-
Minternal/ui/nbscreen.go9+
Ainternal/ui/nbstream.go212++++++++++++++++++++++++++++++
Ainternal/ui/nbstream_test.go139+++++++++++++++++++
Minternal/ui/nbview/draw.go12++
Minternal/ui/nbview/toolbar.go4+
Minternal/ui/nbview/view.go7+-
Minternal/ui/notebook.go11+-
M ROADMAP.md +1 -1
@@ -46,7 +46,6 @@ log macros record. Each step is useful on its own.
4646 4747 | Step | Result | Size |4848 |---|---|---|49-| A notebook cell's output follows a streaming pipeline through the linked regions' live sources (`live.Source`), rows arriving on its sheet as the pipeline writes them | Pipelines as live sheets | S |5049 | Shared viewing over SSH: several `012 serve` sessions open the same workbook; one edits, the others follow live with their own cursor, scroll and theme; presence shows who is where | Watch-along and review, one writer | M |5150 | Shared editing over SSH: every session edits, the server orders operations (no CRDT needed while one server holds the workbook), per-user undo, presence and edit ownership shown in the grid, saves by the server | Multiplayer 012 over SSH | L |5251
@@ -149,6 +148,7 @@ stream above rather than a design of their own.
149148 - 012 as a stage in a pipeline: `012 -` reads a table from standard input, `012 --pipe` sends the sheet or selection on, with nushell's types kept through NUON: [Pipelines](docs/nushell/pipelines.md)150149 - The `sheet` command: a nushell module shipped in the binary (`012 nu --install-module`), so nu calls 012 without `^012` or NUON on either side: [Pipelines](docs/nushell/pipelines.md#the-sheet-command)151150 - Notebook tabs, a TUI Jupyter for nushell: code and note cells with Jupyter's keys, outputs drawn in place and opened full-screen, `$name` between cells, stale marks and a reactive mode, outputs sent to sheets as live regions (`nu.name`), saved up to a cap, and earlier notebook sheets converted on open: [Notebooks](docs/nushell/notebooks.md)151+- Cells run as streams: a pipeline that never ends (`tail -f`, `watch`) followed live, its rows reaching the output and its sheet as nu prints them: [Notebooks](docs/nushell/notebooks.md#streams)152152 - Code cells highlighted, checked and completed as they're written by nu itself (`--ide-ast`, `--ide-check`, `--ide-complete`), in the background, falling back to 012's own when nu is missing, old, slow or not trusted: [Notebooks](docs/nushell/notebooks.md#writing-a-cell)153153 154154 **Files**
M docs/contributing/architecture.md +8 -4
@@ -283,12 +283,14 @@ style.
283283 284284 Rows reach a region as `sheet.LiveOp`s, each applied at once by285285 `Workbook.ApplyLive` through the regions' one write path, `writeTable`:286-what follows a linked file sends them, and the UI sends a notebook287-cell's output to the region it was sent to whenever the cell runs.286+what follows a linked file sends them, the UI sends a notebook cell's287+output to the region it was sent to whenever the cell runs, and a cell288+run as a stream sends each batch of rows its pipeline prints.288289 289290 ```mermaid290291 flowchart TD291292 file[a linked file] -->|polled| source[live.Source]293+ stream[a cell run as a stream: nu printing a value a line] -->|live.Stream| source292294 source -->|live.Update| op[sheet.LiveOp]293295 cell[a notebook cell's run] -->|its output, fileio.NUONRows| op294296 op --> apply[Workbook.ApplyLive]
@@ -605,8 +607,10 @@ file followed as it grows: the importers' readers run on a goroutine of
605607 its own over a pipe that waits for the next piece, so a record cut off606608 at the end of one waits for the rest, and rows come out typed as an607609 import types them (`tail.go`). A `live.Source` is anything that yields a608-table's rows over time, which a notebook cell following a pipeline609-would be too.610+table's rows over time: a followed file (`live.File`), or a notebook611+cell run as a stream (`live.Stream`), whose pipeline nu runs printing612+each value as NUON on a line of its own (`nushell.Stream`), read by the613+same `Tail` as it arrives.610614 611615 XLSX is read by 012's own SpreadsheetML reader on `archive/zip` and612616 `encoding/xml`: the workbook, shared strings (kept end to end in one
M docs/contributing/ux.md +1 -1
@@ -99,7 +99,7 @@ there.
9999 | Entries failing validation | A dotted underline, and `Invalid:` with the rule on the context line |100100 | Values an array spilled | Italic, and `Spilled from B2` on the context line |101101 | Linked files' rows, following, paused or failing | Italic; `●`, `‖` or `!` beside the tab's name and on the context line, with the state in words (`Following`, `Paused`, the error) |102-| A notebook's cells and outputs | `❯` before the tab's name; `▌` left of the active cell and `▎` left of the others selected; a heavy box around the cell being edited; the prompt's and the box's words for its state (`[*]:` and `running`, `waiting`, `failed`, `stale`); `■` for `▶` while it runs; `output hidden` for a hidden output, and which rows show for one scrolled; `×` before an error; an output table's header bold and underlined; an output sent to a sheet in italic, with `Output of files` on the context line; a problem nu finds in the cell being written curly-underlined, in words on the context line with the caret on it |102+| A notebook's cells and outputs | `❯` before the tab's name; `▌` left of the active cell and `▎` left of the others selected; a heavy box around the cell being edited; the prompt's and the box's words for its state (`[*]:` and `running`, `waiting`, `failed`, `stale`); `■` for `▶` while it runs; `● live` and the rows it printed for a cell running as a stream; `output hidden` for a hidden output, and which rows show for one scrolled; `×` before an error; an output table's header bold and underlined; an output sent to a sheet in italic, with `Output of files` on the context line; a problem nu finds in the cell being written curly-underlined, in words on the context line with the caret on it |103103 | A table's header row | Bold and underlined, when the table styles its header; `Table Sales, column Amount` on the context line |104104 | Pivot table results | Their headings (`SUM of Units`, `Grand Total`), and a note in words when an edit is refused |105105 | Protected ranges | A question in words before an edit (`A1:B2 is protected.`) |
M docs/nushell/notebooks.md +27 -1
@@ -170,7 +170,8 @@ delete, copy, cut, move, make notes or code, hide or clear outputs.
170170 | `o`, `O` | Hide the output, or show it again; show every row, or the window again |171171 | `G` | Send the output to a sheet |172172 | Ctrl+G | Go to a cell by its number, name, code or heading |173-| `ii` | Stop: kill the cell running, and forget those waiting |173+| `f` | Run the cell as a stream, until it's stopped ([Streams](#streams)) |174+| `ii` | Stop: kill the cell running, forget those waiting, and stop every stream |174175 | `00` | Restart: stop, clear every output, count runs from 1 |175176 176177 | Key | In edit mode |
@@ -272,6 +273,31 @@ commands aren't there unless `nu-config` is on
272273 A cell stops after `nu-timeout` (30 seconds unless set); `ii` stops it273274 sooner.274275 276+### Streams277+278+A pipeline that never ends, such as following a log or `watch`ing a279+folder, runs as a stream: `f` (**Data > Shell > Run as stream**) runs280+the selected cell until you stop it, and every value it yields reaches281+the cell's output as soon as nu prints it, and the sheet the output was282+[sent to](#send-to-a-sheet) as rows under its header:283+284+```nu285+log = tail -f app.log | lines | parse '{time} {level} {msg}'286+changes = watch . --glob=*.csv --quiet287+```288+289+While it runs, the cell's box says `● live` with how many rows it has290+printed, and the toolbar how many cells stream; streams run beside the291+cells run once, each on its own, without `nu-timeout`. `ii` (or `■`)292+stops them all, keeping what they printed; running the cell once (`r`)293+stops its stream first. A stream that ends on its own keeps its output294+as a run does, and one that fails says why.295+296+The output keeps a stream's last 10,000 values; a sheet it was sent to297+keeps every row, up to `max-cells`. The rows arrive as the change298+stream does for a [followed file](../files/following.md), outside the299+undo history.300+275301 ## Names and $name276302 277303 A cell that starts `name =` gives its output that name; `n` names a
M docs/reference/keys.md +2 -1
M e2e/screennotebook_test.go +7 -2
A e2e/stream_test.go +61 -0
@@ -0,0 +1,61 @@
1+package e2e2+3+import (4+ "os"5+ "path/filepath"6+ "strings"7+ "testing"8+ "time"9+)10+11+// A cell run as a stream with the real nu follows a log as it's12+// written: its rows reach the output and the sheet it was sent to as13+// lines are appended, and Stop ends it, keeping them.14+func TestNotebookStreamWithNu(t *testing.T) {15+ needNu(t)16+ dir := t.TempDir()17+ logFile := filepath.Join(dir, "app.log")18+ if err := os.WriteFile(logFile, []byte("info started\n"), 0o644); err != nil {19+ t.Fatal(err)20+ }21+ s := startWith(t, options{dir: dir, startsOn: "EDIT"}, "nu")22+ s.keys("log = tail -n +1 -f app.log | lines | parse '{level} {msg}'", "<esc>", "f")23+ s.waitFor("● live, 1 row")24+ s.waitFor("started")25+26+ s.keys("G", "<enter>")27+ s.waitFor("Sent log to")28+ appendFile(t, logFile, "warn slow-query\nerror disk-full\n")29+ s.waitFor("● live, 3 rows")30+ s.waitFor("disk-full")31+32+ s.keys("<ctrl+pgdown>", "<ctrl+home>", "<right>", "<right>", "<right>", "=ROWS(nu.log)", "<enter>")33+ s.waitFor(" 4")34+ appendFile(t, logFile, "info recovered\n")35+ s.waitFor(" 5")36+37+ s.keys("<ctrl+pgup>", "i", "i")38+ s.waitFor("✓")39+ s.eventually("the stream stopped", func() bool { return !strings.Contains(s.screen(), "● live") })40+ appendFile(t, logFile, "info after-stop\n")41+ s.keys("<ctrl+pgdown>")42+ s.waitFor(" 5")43+ time.Sleep(time.Second)44+ if strings.Contains(s.screen(), " 6") {45+ t.Errorf("rows arrived after Stop:\n%s", s.screen())46+ }47+ s.keys("<ctrl+q>", "d")48+ s.waitExit()49+}50+51+func appendFile(t *testing.T, name, text string) {52+ t.Helper()53+ f, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0)54+ if err != nil {55+ t.Fatal(err)56+ }57+ defer f.Close()58+ if _, err := f.WriteString(text); err != nil {59+ t.Fatal(err)60+ }61+}
M e2e/testdata/nu/nu +8 -2
M e2e/testdata/screens/help-light.html +1 -1
@@ -25,6 +25,6 @@
2525 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">21</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Deselect <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>2626 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">22</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select all <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+A</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>2727 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">23</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select columns <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+Space</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>28-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-25</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">130</span> <span style="color:var(--vt-palette-8)">─┘</span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-25</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">131</span> <span style="color:var(--vt-palette-8)">─┘</span>2929 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">25</span><span style="background:var(--vt-palette-7)"> </span>3030 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">scroll</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file
M e2e/testdata/screens/help-narrow.html +1 -1
@@ -11,6 +11,6 @@
1111 <span style="color:var(--vt-palette-8)">│</span> Last used cell <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+End</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>1212 <span style="color:var(--vt-palette-8)">│</span> Go to <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+G</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F5</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>1313 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>14-<span style="color:var(--vt-palette-8)">└───────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-11</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">130</span> <span style="color:var(--vt-palette-8)">─┘</span>14+<span style="color:var(--vt-palette-8)">└───────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-11</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">131</span> <span style="color:var(--vt-palette-8)">─┘</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
M e2e/testdata/screens/help-wide.html +1 -1
@@ -40,6 +40,6 @@
4040 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">36</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Time <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Shift+2</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>4141 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">37</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Notebooks</span> Bold <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+B</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>4242 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">38</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move between cells and outputs <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">j</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">k</span><span style="background:var(--vt-palette-8)"> </span> Italic <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+I</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>43-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">39</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-40</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">65</span> <span style="color:var(--vt-palette-8)">─┘</span>43+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">39</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-40</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">66</span> <span style="color:var(--vt-palette-8)">─┘</span>4444 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">40</span><span style="background:var(--vt-palette-8)"> </span>4545 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
M e2e/testdata/screens/help.html +1 -1
@@ -25,6 +25,6 @@
2525 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Deselect <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>2626 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select all <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+A</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>2727 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select columns <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Space</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>28-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-25</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">130</span> <span style="color:var(--vt-palette-8)">─┘</span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-25</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">131</span> <span style="color:var(--vt-palette-8)">─┘</span>2929 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">25</span><span style="background:var(--vt-palette-8)"> </span>3030 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/notebook-stream-high-contrast.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">F</span><span style="color:#ffffff;background:#1f1f1f">ile</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">E</span><span style="color:#ffffff;background:#1f1f1f">dit</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">V</span><span style="color:#ffffff;background:#1f1f1f">iew</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">I</span><span style="color:#ffffff;background:#1f1f1f">nsert</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">F</span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">o</span><span style="color:#ffffff;background:#1f1f1f">rmat</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">D</span><span style="color:#ffffff;background:#1f1f1f">ata</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">H</span><span style="color:#ffffff;background:#1f1f1f">elp</span><span style="background:#1f1f1f"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">NOTEBOOK</span><span style="background:#40e0ff"> </span>2+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▶</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Run</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">■</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">Stop</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">↻</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Restart</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▶▶</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Run</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">all</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">+</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Add</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">✂</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Cut</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">⧉</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Copy</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">⎘</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">Paste</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Code</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▾</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">nu</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">●</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">1</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">live</span><span style="background:#000000"> </span>3+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">Following</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">cell</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">4</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">as</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">a</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">stream:</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Stop</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">(i</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">i)</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">ends</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">it</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">──────────────────────────────────────────────────</span>4+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">Files</span><span style="background:#000000"> </span>5+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">What&#39;s</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">big</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">in</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">the</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">repo,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">from</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">ls</span><span style="color:#ffffff;background:#000000">.</span><span style="background:#000000"> </span>6+<span style="background:#000000"> </span>7+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╭─</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">files</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">──────────────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">✓</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">&lt;1s</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">─╮</span><span style="background:#000000"> </span>8+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">[1]:</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ff80ff;background:#000000">files</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">=</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">ls</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">▶</span>9+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╰──────────────────────────────────────────────────────────────────────────────────────╯</span><span style="background:#000000"> </span>10+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">Out[1]:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">name</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">type</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">size</span><span style="background:#000000"> </span>11+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">012</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4.2</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>12+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">CLAUDE.md</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1.0</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>13+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">README.md</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">9.8</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>14+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">demos</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">dir</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">3.1</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>15+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">go.mod</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">812</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">B</span><span style="background:#000000"> </span>16+<span style="background:#000000"> </span>17+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╭──────────────────────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">✓</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">&lt;1s</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">─╮</span><span style="background:#000000"> </span>18+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">[2]:</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ff80ff;background:#000000">$files</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">|</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">where</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">size</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">&gt;</span><span style="background:#000000"> </span><span style="color:#ffe040;background:#000000">1kb</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">▶</span>19+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╰──────────────────────────────────────────────────────────────────────────────────────╯</span><span style="background:#000000"> </span>20+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">Out[2]:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">name</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">type</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">size</span><span style="background:#000000"> </span>21+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">012</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4.2</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>22+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">README.md</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">9.8</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>23+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">demos</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">dir</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">3.1</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>24+<span style="background:#000000"> </span>25+<span style="color:#80a8ff;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">╭─</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">log</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">───────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">●</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">live,</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">3</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">rows</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">─╮</span><span style="background:#000000"> </span>26+<span style="color:#80a8ff;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">[*]:</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ff80ff;background:#000000">log</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">=</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">tail</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">-f</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">app.log</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">|</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">lines</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">|</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">parse</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">&#39;{time}</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">{level}</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">{msg}&#39;</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">■</span>27+<span style="color:#80a8ff;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#80a8ff;background:#000000">╰──────────────────────────────────────────────────────────────────────────────────────╯</span><span style="background:#000000"> </span>28+<span style="color:#80a8ff;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Out[3]:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">time</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">level</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">msg</span><span style="background:#000000"> </span>29+<span style="color:#80a8ff;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">09:00:01</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">info</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">server</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">started</span><span style="background:#000000"> </span>30+<span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">❯</span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">Notebook</span><span style="background:#40e0ff"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">+</span><span style="background:#1f1f1f"> </span><span style="color:#757575;background:#1f1f1f">│</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">untitled</span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">modified</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Ctrl+K</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">search</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Ctrl+/</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">shortcuts</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">F10</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/notebook-stream-light.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">NOTEBOOK</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶ Run <span style="color:var(--vt-palette-8)">■</span> <span style="color:var(--vt-palette-8)">Stop</span> ↻ Restart ▶▶ Run all <span style="color:var(--vt-palette-8)">│</span> + Add ✂ Cut ⧉ Copy <span style="color:var(--vt-palette-8)">⎘</span> <span style="color:var(--vt-palette-8)">Paste</span> <span style="color:var(--vt-palette-8)">│</span> Code ▾ <span style="color:var(--vt-palette-0)">nu</span> <span style="color:var(--vt-palette-0)">●</span> <span style="color:var(--vt-palette-0)">1</span> <span style="color:var(--vt-palette-0)">live</span>3+ <span style="color:var(--vt-palette-0)">Following</span> <span style="color:var(--vt-palette-0)">cell</span> <span style="color:var(--vt-palette-0)">4</span> <span style="color:var(--vt-palette-0)">as</span> <span style="color:var(--vt-palette-0)">a</span> <span style="color:var(--vt-palette-0)">stream:</span> <span style="color:var(--vt-palette-0)">Stop</span> <span style="color:var(--vt-palette-0)">(i</span> <span style="color:var(--vt-palette-0)">i)</span> <span style="color:var(--vt-palette-0)">ends</span> <span style="color:var(--vt-palette-0)">it</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────</span>4+ <span style="font-weight:bold;text-decoration:underline">Files</span>5+ What&#39;s big in the repo, from <span style="color:var(--vt-palette-4)">ls</span>.6+7+ <span style="color:var(--vt-palette-8)">╭─</span> <span style="color:var(--vt-palette-0)">files</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">✓</span> <span style="color:var(--vt-palette-0)">&lt;1s</span> <span style="color:var(--vt-palette-8)">─╮</span>8+ <span style="color:var(--vt-palette-0)">[1]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">files</span> <span style="font-weight:bold">=</span> <span style="color:var(--vt-palette-4)">ls</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>9+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>10+ <span style="color:var(--vt-palette-0)">Out[1]:</span> <span style="font-weight:bold;text-decoration:underline">name</span> <span style="font-weight:bold;text-decoration:underline">type</span> <span style="font-weight:bold;text-decoration:underline">size</span>11+ 012 file 4.2 kB12+ CLAUDE.md file 1.0 kB13+ README.md file 9.8 kB14+ demos dir 3.1 kB15+ go.mod file 812 B16+17+ <span style="color:var(--vt-palette-8)">╭──────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">✓</span> <span style="color:var(--vt-palette-0)">&lt;1s</span> <span style="color:var(--vt-palette-8)">─╮</span>18+ <span style="color:var(--vt-palette-0)">[2]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">where</span> size <span style="font-weight:bold">&gt;</span> <span style="color:var(--vt-palette-1)">1kb</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>19+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>20+ <span style="color:var(--vt-palette-0)">Out[2]:</span> <span style="font-weight:bold;text-decoration:underline">name</span> <span style="font-weight:bold;text-decoration:underline">type</span> <span style="font-weight:bold;text-decoration:underline">size</span>21+ 012 file 4.2 kB22+ README.md file 9.8 kB23+ demos dir 3.1 kB24+25+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-4)">╭─</span> <span style="color:var(--vt-palette-0)">log</span> <span style="color:var(--vt-palette-4)">───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">●</span> <span style="color:var(--vt-palette-0)">live,</span> <span style="color:var(--vt-palette-0)">3</span> <span style="color:var(--vt-palette-0)">rows</span> <span style="color:var(--vt-palette-4)">─╮</span>26+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-0)">[*]:</span> <span style="color:var(--vt-palette-4)">│</span> <span style="color:var(--vt-palette-5)">log</span> <span style="font-weight:bold">=</span> <span style="color:var(--vt-palette-4)">tail</span> -f app.log <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">lines</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">parse</span> <span style="color:var(--vt-palette-2)">&#39;{time}</span> <span style="color:var(--vt-palette-2)">{level}</span> <span style="color:var(--vt-palette-2)">{msg}&#39;</span> <span style="color:var(--vt-palette-4)">│</span> <span style="color:var(--vt-palette-4)">■</span>27+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-4)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>28+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-0)">Out[3]:</span> <span style="font-weight:bold;text-decoration:underline">time</span> <span style="font-weight:bold;text-decoration:underline">level</span> <span style="font-weight:bold;text-decoration:underline">msg</span>29+<span style="color:var(--vt-palette-4)">▌</span> 09:00:01 info server started30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">❯</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Notebook</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/notebook-stream.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">NOTEBOOK</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶ Run <span style="color:var(--vt-palette-8)">■</span> <span style="color:var(--vt-palette-8)">Stop</span> ↻ Restart ▶▶ Run all <span style="color:var(--vt-palette-8)">│</span> + Add ✂ Cut ⧉ Copy <span style="color:var(--vt-palette-8)">⎘</span> <span style="color:var(--vt-palette-8)">Paste</span> <span style="color:var(--vt-palette-8)">│</span> Code ▾ <span style="color:var(--vt-palette-8)">nu</span> <span style="color:var(--vt-palette-8)">●</span> <span style="color:var(--vt-palette-8)">1</span> <span style="color:var(--vt-palette-8)">live</span>3+ <span style="color:var(--vt-palette-8)">Following</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="color:var(--vt-palette-8)">4</span> <span style="color:var(--vt-palette-8)">as</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">stream:</span> <span style="color:var(--vt-palette-8)">Stop</span> <span style="color:var(--vt-palette-8)">(i</span> <span style="color:var(--vt-palette-8)">i)</span> <span style="color:var(--vt-palette-8)">ends</span> <span style="color:var(--vt-palette-8)">it</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────</span>4+ <span style="font-weight:bold;text-decoration:underline">Files</span>5+ What&#39;s big in the repo, from <span style="color:var(--vt-palette-6)">ls</span>.6+7+ <span style="color:var(--vt-palette-8)">╭─</span> <span style="color:var(--vt-palette-8)">files</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">✓</span> <span style="color:var(--vt-palette-8)">&lt;1s</span> <span style="color:var(--vt-palette-8)">─╮</span>8+ <span style="color:var(--vt-palette-8)">[1]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">files</span> <span style="font-weight:bold">=</span> <span style="color:var(--vt-palette-6)">ls</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>9+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>10+ <span style="color:var(--vt-palette-8)">Out[1]:</span> <span style="font-weight:bold;text-decoration:underline">name</span> <span style="font-weight:bold;text-decoration:underline">type</span> <span style="font-weight:bold;text-decoration:underline">size</span>11+ 012 file 4.2 kB12+ CLAUDE.md file 1.0 kB13+ README.md file 9.8 kB14+ demos dir 3.1 kB15+ go.mod file 812 B16+17+ <span style="color:var(--vt-palette-8)">╭──────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">✓</span> <span style="color:var(--vt-palette-8)">&lt;1s</span> <span style="color:var(--vt-palette-8)">─╮</span>18+ <span style="color:var(--vt-palette-8)">[2]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">where</span> size <span style="font-weight:bold">&gt;</span> <span style="color:var(--vt-palette-3)">1kb</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>19+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>20+ <span style="color:var(--vt-palette-8)">Out[2]:</span> <span style="font-weight:bold;text-decoration:underline">name</span> <span style="font-weight:bold;text-decoration:underline">type</span> <span style="font-weight:bold;text-decoration:underline">size</span>21+ 012 file 4.2 kB22+ README.md file 9.8 kB23+ demos dir 3.1 kB24+25+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-4)">╭─</span> <span style="color:var(--vt-palette-8)">log</span> <span style="color:var(--vt-palette-4)">───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">●</span> <span style="color:var(--vt-palette-8)">live,</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color:var(--vt-palette-8)">rows</span> <span style="color:var(--vt-palette-4)">─╮</span>26+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-8)">[*]:</span> <span style="color:var(--vt-palette-4)">│</span> <span style="color:var(--vt-palette-5)">log</span> <span style="font-weight:bold">=</span> <span style="color:var(--vt-palette-6)">tail</span> -f app.log <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">lines</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">parse</span> <span style="color:var(--vt-palette-2)">&#39;{time}</span> <span style="color:var(--vt-palette-2)">{level}</span> <span style="color:var(--vt-palette-2)">{msg}&#39;</span> <span style="color:var(--vt-palette-4)">│</span> <span style="color:var(--vt-palette-4)">■</span>27+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-4)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>28+<span style="color:var(--vt-palette-4)">▌</span> <span style="color:var(--vt-palette-8)">Out[3]:</span> <span style="font-weight:bold;text-decoration:underline">time</span> <span style="font-weight:bold;text-decoration:underline">level</span> <span style="font-weight:bold;text-decoration:underline">msg</span>29+<span style="color:var(--vt-palette-4)">▌</span> 09:00:01 info server started30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">❯</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Notebook</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file
M e2e/testdata/screens/palette-narrow.html +1 -1
@@ -12,5 +12,5 @@
1212 <span style="color:var(--vt-palette-8)">│</span> Sort <span style="color:var(--vt-palette-3);font-weight:bold">s</span>heet Z to <span style="color:var(--vt-palette-3);font-weight:bold">A</span> <span style="color:var(--vt-palette-8)">│</span>1313 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Ask</span> <span style="color:var(--vt-palette-8)">JEV</span> <span style="color:var(--vt-palette-8)">again</span> <span style="color:var(--vt-palette-8)">│</span>1414 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-3);font-weight:bold">S</span>ort r<span style="color:var(--vt-palette-3);font-weight:bold">a</span>nge <span style="color:var(--vt-palette-8)">│</span>15-<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">64</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>15+<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">65</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">226</span> <span style="color:var(--vt-palette-8)">─┘</span>1616 Save the sheet u… <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">move</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">run</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
M e2e/testdata/screens/palette-search-light.html +1 -1
@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Reset <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umn width <span style="color:var(--vt-palette-0)">Format</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">11</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Insert <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umns right <span style="color:var(--vt-palette-0)">Insert</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">12</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Delete rows or <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umns <span style="color:var(--vt-palette-0)">Edit</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+Alt+-</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">48</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">48</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">226</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span>1919 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span>2020 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span>
M e2e/testdata/screens/palette-search.html +1 -1
@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Reset <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umn width <span style="color:var(--vt-palette-8)">Format</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Insert <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umns right <span style="color:var(--vt-palette-8)">Insert</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Delete rows or <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umns <span style="color:var(--vt-palette-8)">Edit</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Alt+-</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">48</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">48</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">226</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>
M e2e/testdata/screens/palette-wide.html +1 -1
@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-3);font-weight:bold">S</span>av<span style="color:var(--vt-palette-3);font-weight:bold">e</span> as <span style="color:var(--vt-palette-8)">Fi</span><span style="color:var(--vt-palette-3);font-weight:bold">l</span><span style="color:var(--vt-palette-8)">e</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Code</span> <span style="color:var(--vt-palette-8)">Data</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Shell</span> <span style="color:var(--vt-palette-8)">y</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> In<span style="color:var(--vt-palette-3);font-weight:bold">se</span>rt rows be<span style="color:var(--vt-palette-3);font-weight:bold">l</span>ow <span style="color:var(--vt-palette-8)">Insert</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">59</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">60</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">226</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>
M e2e/testdata/screens/palette.html +1 -1
@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Decimal arithmetic <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Settings</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">226</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">226</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>
A internal/live/stream.go +214 -0
@@ -0,0 +1,214 @@
1+package live2+3+import (4+ "bytes"5+ "context"6+ "errors"7+ "io"8+ "sync"9+ "time"10+11+ "github.com/FelineStateMachine/012/internal/fileio"12+ "github.com/FelineStateMachine/012/internal/sheet"13+)14+15+// Stream follows a program that prints a table as it makes it: NUON16+// values one after another, one to a line, as a notebook cell run as a17+// stream prints them (nushell.Stream). The program starts with the18+// stream and runs on a goroutine of its own until it ends or the stream19+// is closed. A poll waits up to Wait for something to arrive, so rows20+// reach the region as soon as the program prints them, and the owner21+// polls again at once rather than after Interval.22+//23+// It keeps the last Keep values printed, for the cell's output (NUON)24+// and for reading the table whole again (Reload).25+type Stream struct {26+ mu sync.Mutex27+ pending []byte // printed since the last poll28+ ended bool29+ err error // why the program ended, nil when it ended well30+ arrived chan struct{}31+ cancel context.CancelFunc32+33+ // The rest is the poller's: polls never overlap. values is guarded34+ // by mu too, since the owner reads it (NUON).35+ partial []byte // a line not yet ended36+ tail *fileio.Tail37+ values [][]byte38+ header sheet.LiveRow39+ rows []sheet.LiveRow40+ total int // rows printed in all; guarded by mu41+ reload bool42+ done bool // the end was reported; guarded by mu43+44+ // Wait is how long a poll waits for the program to print.45+ Wait time.Duration46+ // Keep is how many of the last values are kept.47+ Keep int48+ // Now is the clock; tests replace it.49+ Now func() time.Time50+}51+52+// StreamKeep is how many of the last values a stream keeps.53+const StreamKeep = 1000054+55+// NewStream starts run, which prints the table to w until ctx is done,56+// and follows what it prints.57+func NewStream(run func(ctx context.Context, w io.Writer) error) *Stream {58+ ctx, cancel := context.WithCancel(context.Background())59+ s := &Stream{arrived: make(chan struct{}, 1), cancel: cancel, Wait: Interval, Keep: StreamKeep, Now: time.Now}60+ s.tail, _ = fileio.NewTail(fileio.NUON, nil, nil)61+ go func() {62+ err := run(ctx, streamWriter{s})63+ s.mu.Lock()64+ s.ended, s.err = true, err65+ s.mu.Unlock()66+ s.kick()67+ }()68+ return s69+}70+71+// streamWriter takes what the program prints.72+type streamWriter struct{ s *Stream }73+74+func (w streamWriter) Write(p []byte) (int, error) {75+ w.s.mu.Lock()76+ w.s.pending = append(w.s.pending, p...)77+ w.s.mu.Unlock()78+ w.s.kick()79+ return len(p), nil80+}81+82+// kick tells a poll waiting that something arrived.83+func (s *Stream) kick() {84+ select {85+ case s.arrived <- struct{}{}:86+ default:87+ }88+}89+90+// Poll waits up to Wait for the program to print, and returns the rows91+// it completed: all of those kept again after Reload, and the reason92+// it failed once it ends with an error.93+func (s *Stream) Poll(ctx context.Context) (Update, bool) {94+ if done, _ := s.Ended(); done {95+ return Update{}, false96+ }97+ t := time.NewTimer(s.Wait)98+ select {99+ case <-s.arrived:100+ case <-t.C:101+ case <-ctx.Done():102+ }103+ t.Stop()104+ s.mu.Lock()105+ got, ended, err := s.pending, s.ended, s.err106+ s.pending = nil107+ s.mu.Unlock()108+109+ u := Update{At: s.Now()}110+ s.read(got, ended, &u)111+ if s.reload {112+ s.reload = false113+ u.Reset, u.Header, u.Rows = true, s.header, s.rows114+ }115+ if ended && !s.done {116+ s.mu.Lock()117+ s.done = true118+ s.mu.Unlock()119+ if err != nil && !isStop(err) {120+ u.Err = err.Error()121+ }122+ return u, true123+ }124+ return u, u.Reset || len(u.Rows) > 0 || u.Header != nil125+}126+127+// read parses the complete lines of what arrived into u, keeping them.128+func (s *Stream) read(got []byte, ended bool, u *Update) {129+ s.partial = append(s.partial, got...)130+ end := bytes.LastIndexByte(s.partial, '\n') + 1131+ if ended {132+ end = len(s.partial)133+ }134+ if end == 0 {135+ return136+ }137+ text := s.partial[:end]138+ s.partial = append([]byte(nil), s.partial[end:]...)139+ var lines [][]byte140+ for line := range bytes.SplitSeq(text, []byte("\n")) {141+ if line = bytes.TrimSpace(line); len(line) > 0 {142+ lines = append(lines, append([]byte(nil), line...))143+ }144+ }145+ if len(lines) == 0 {146+ return147+ }148+ s.mu.Lock()149+ s.values = keepLast(append(s.values, lines...), s.Keep)150+ s.mu.Unlock()151+ res, err := s.tail.Feed(append(bytes.Join(lines, []byte("\n")), '\n'))152+ if err != nil {153+ u.Err = err.Error()154+ }155+ if res.Header != nil {156+ s.header = res.Header157+ u.Header = res.Header158+ }159+ s.mu.Lock()160+ s.total += len(res.Rows)161+ s.mu.Unlock()162+ s.rows = keepLast(append(s.rows, res.Rows...), s.Keep)163+ u.Rows = res.Rows164+}165+166+// keepLast is the last n of xs.167+func keepLast[T any](xs []T, n int) []T {168+ if n > 0 && len(xs) > n {169+ return append(xs[:0:0], xs[len(xs)-n:]...)170+ }171+ return xs172+}173+174+// isStop reports whether err is the stream being stopped rather than175+// failing.176+func isStop(err error) bool { return errors.Is(err, context.Canceled) }177+178+// Reload has the next poll give every row kept again.179+func (s *Stream) Reload() { s.reload = true }180+181+// Close stops the program; the stream is polled no more.182+func (s *Stream) Close() {183+ s.cancel()184+ s.tail.Close()185+}186+187+// Ended reports whether the program has ended and the last poll gave188+// what it printed, and why it failed if it did (nil when it ended well189+// or was stopped).190+func (s *Stream) Ended() (bool, error) {191+ s.mu.Lock()192+ defer s.mu.Unlock()193+ if !s.done {194+ return false, nil195+ }196+ if isStop(s.err) {197+ return true, nil198+ }199+ return true, s.err200+}201+202+// Rows is how many rows the program has printed so far.203+func (s *Stream) Rows() int {204+ s.mu.Lock()205+ defer s.mu.Unlock()206+ return s.total207+}208+209+// NUON is the values kept, as a NUON list: the cell's output so far.210+func (s *Stream) NUON() []byte {211+ s.mu.Lock()212+ defer s.mu.Unlock()213+ return append(append([]byte("["), bytes.Join(s.values, []byte(", "))...), ']')214+}
A internal/live/stream_test.go +93 -0
@@ -0,0 +1,93 @@
1+package live2+3+import (4+ "context"5+ "errors"6+ "io"7+ "testing"8+ "time"9+)10+11+// A stream's rows come as the program prints them, a line cut in two12+// waiting for its end; Reload gives them all again, and the end is13+// reported once, with the error it ended on.14+func TestStreamRowsAsPrinted(t *testing.T) {15+ next := make(chan string)16+ s := NewStream(func(ctx context.Context, w io.Writer) error {17+ for text := range next {18+ io.WriteString(w, text)19+ }20+ return errors.New("tail: app.log: no such file")21+ })22+ defer s.Close()23+ s.Wait = time.Second24+ ctx := context.Background()25+26+ next <- "{x: a, y: 1}\n{x: b"27+ u, ok := s.Poll(ctx)28+ if !ok || len(u.Rows) != 1 || u.Header == nil || u.Header[0].V.Str != "x" || u.Rows[0][1].V.Num != 1 {29+ t.Fatalf("first poll: %v %+v", ok, u)30+ }31+ next <- ", y: 2}\n"32+ if u, ok = s.Poll(ctx); !ok || len(u.Rows) != 1 || u.Rows[0][0].V.Str != "b" || u.Header != nil {33+ t.Fatalf("second poll: %v %+v", ok, u)34+ }35+ if got := string(s.NUON()); got != "[{x: a, y: 1}, {x: b, y: 2}]" {36+ t.Errorf("NUON %s", got)37+ }38+ s.Reload()39+ next <- "{x: c, y: 3}\n"40+ if u, ok = s.Poll(ctx); !ok || !u.Reset || len(u.Rows) != 3 || u.Header == nil {41+ t.Fatalf("after Reload: %v %+v", ok, u)42+ }43+ if done, _ := s.Ended(); done {44+ t.Fatal("ended too soon")45+ }46+ close(next)47+ u, ok = s.Poll(ctx)48+ if !ok || u.Err != "tail: app.log: no such file" {49+ t.Fatalf("the end: %v %+v", ok, u)50+ }51+ if done, err := s.Ended(); !done || err == nil || s.Rows() != 3 {52+ t.Errorf("ended %v %v, %d rows", done, err, s.Rows())53+ }54+ if _, ok := s.Poll(ctx); ok {55+ t.Error("polled after the end")56+ }57+}58+59+// Closing a stream stops its program, which ends without an error.60+func TestStreamClose(t *testing.T) {61+ s := NewStream(func(ctx context.Context, w io.Writer) error {62+ io.WriteString(w, "1\n")63+ <-ctx.Done()64+ return ctx.Err()65+ })66+ s.Wait = time.Second67+ if u, ok := s.Poll(context.Background()); !ok || len(u.Rows) != 1 {68+ t.Fatalf("%v %+v", ok, u)69+ }70+ s.Close()71+ u, ok := s.Poll(context.Background())72+ if !ok || u.Err != "" {73+ t.Fatalf("after Close: %v %+v", ok, u)74+ }75+ if done, err := s.Ended(); !done || err != nil {76+ t.Errorf("ended %v %v", done, err)77+ }78+}79+80+// Only the last Keep values are kept.81+func TestStreamKeepsTheLast(t *testing.T) {82+ s := NewStream(func(ctx context.Context, w io.Writer) error {83+ io.WriteString(w, "1\n2\n3\n")84+ return nil85+ })86+ s.Keep = 287+ for done := false; !done; done, _ = s.Ended() {88+ s.Poll(context.Background())89+ }90+ if got := string(s.NUON()); got != "[2, 3]" {91+ t.Errorf("NUON %s", got)92+ }93+}
M internal/nushell/nushell_test.go +22 -0
A internal/nushell/stream.go +37 -0
M internal/ui/crash.go +1 -0
M internal/ui/model_test.go +4 -0
M internal/ui/nbrun.go +11 -2
@@ -64,6 +64,10 @@ type nbState struct {
6464 // follow scrolls the view to each cell as it starts, while a run of6565 // several cells goes on and the user hasn't scrolled away.6666 follow bool67+ // streams are the cells running as streams, by cell ID, and68+ // streamTick is set while their heads are ticking: nbstream.go.69+ streams map[int]*nbStream70+ streamTick bool6771 }6872 6973 // nbQueued is a cell waiting to run.
@@ -118,6 +122,9 @@ func (m *Model) cellState(s *sheet.Sheet, id int) nbview.State {
118122 if r := m.nb.running; r != nil && r.s == s && r.id == id {119123 st.Running, st.Started = true, r.start120124 }125+ if r := m.nb.streams[id]; r != nil && r.s == s {126+ st.Running, st.Started, st.Live, st.Rows = true, r.start, true, r.rows127+ }121128 st.Waiting = slices.Contains(m.nb.queue, nbQueued{s, id})122129 st.Stale = m.staleCells(s)[id]123130 cells := s.NotebookCells()
@@ -270,6 +277,7 @@ func (m *Model) nextCell() tea.Cmd {
270277 m.failCell(q, c, why)271278 continue272279 }280+ m.stopStream(q.id) // run once, it no longer follows273281 run := &nbRun{nbQueued: q, source: c.Source, start: time.Now()}274282 job, err := m.jobFor(q.s, c, run)275283 if err != nil {
@@ -400,13 +408,14 @@ func runError(err error) string {
400408 return err.Error()401409 }402410 403-// stopCells stops the cell running, killing its process, and forgets404-// those waiting.411+// stopCells stops the cell running, killing its process, forgets412+// those waiting, and stops every stream.405413 func (m *Model) stopCells() {406414 if r := m.nb.running; r != nil {407415 r.cancel()408416 }409417 m.nb.queue = nil418+ m.stopStreams()410419 }411420 412421 // clearOutputs clears every output of the notebook shown; restart also
M internal/ui/nbscreen.go +9 -0
A internal/ui/nbstream.go +212 -0
@@ -0,0 +1,212 @@
1+package ui2+3+import (4+ "context"5+ "io"6+ "slices"7+ "strconv"8+ "time"9+10+ tea "charm.land/bubbletea/v2"11+12+ "github.com/FelineStateMachine/012/internal/live"13+ "github.com/FelineStateMachine/012/internal/notebook"14+ "github.com/FelineStateMachine/012/internal/nushell"15+)16+17+// Cells run as streams (Run as stream): the pipeline runs until it ends18+// or is stopped, and each value it yields reaches the cell's output, and19+// the sheet the output was sent to, as soon as nu prints it, so20+// `tail -f app.log | lines | parse ...` or `watch` is followed live. The21+// run is a live.Stream, the linked regions' source: it is polled one22+// poll at a time, each poll waiting for what the pipeline prints, and23+// what it gives is applied as a live operation to the output's region24+// (sheet.LiveOp, outside the undo history) and kept as the cell's25+// output, its last live.StreamKeep values. Streams run beside the queue26+// of cells run once, each on its own; Stop ends them all.27+28+// nbStream is a cell running as a stream.29+type nbStream struct {30+ *nbRun31+ src *live.Stream32+ count int // the run's number, [n]33+ rows int // rows printed so far34+ // shown is when the output was last updated, so a fast stream35+ // updates it a few times a second rather than on every poll, and36+ // behind is set when rows came since.37+ shown time.Time38+ behind bool39+}40+41+// nbStreamMsg is what a stream's poll found.42+type nbStreamMsg struct {43+ st *nbStream44+ u live.Update45+ ok bool46+}47+48+// streamOutputEvery is how often a stream's output is rebuilt, at most:49+// rows reach a sheet the output was sent to on every poll.50+const streamOutputEvery = 100 * time.Millisecond51+52+// streamSelected runs the selected code cell as a stream.53+func (m *Model) streamSelected() tea.Cmd {54+ v := m.nbView()55+ v.StopEdit()56+ c, ok := v.Cell()57+ if !ok || c.Kind != notebook.Code {58+ return nil59+ }60+ s := m.sheet61+ return m.trustNotebook(func(m *Model) tea.Cmd { return m.startStream(nbQueued{s, c.ID}) })62+}63+64+// startStream starts cell q as a stream, in place of any stream or65+// queued run of it.66+func (m *Model) startStream(q nbQueued) tea.Cmd {67+ c, ok := cellOf(q)68+ if !ok {69+ return nil70+ }71+ m.stopStream(q.id)72+ m.nb.queue = slices.DeleteFunc(m.nb.queue, func(x nbQueued) bool { return x == q })73+ cells := q.s.NotebookCells()74+ i := slices.IndexFunc(cells, func(x notebook.Cell) bool { return x.ID == c.ID })75+ if why := notebook.Taken(cells, i); why != "" {76+ m.failCell(q, c, why)77+ return nil78+ }79+ run := &nbRun{nbQueued: q, source: c.Source, start: time.Now()}80+ job, err := m.jobFor(q.s, c, run)81+ if err != nil {82+ m.failCell(q, c, err.Error())83+ return nil84+ }85+ runner, parent := m.runner(), m.spans.Parent()86+ src := live.NewStream(func(ctx context.Context, w io.Writer) error {87+ span := parent.Start("nu.stream")88+ err := nushell.Stream(ctx, runner, job, w)89+ span.End()90+ return err91+ })92+ src.Wait = streamOutputEvery // an idle poll brings the output up to date93+ m.nb.count++94+ st := &nbStream{nbRun: run, src: src, count: m.nb.count}95+ if m.nb.streams == nil {96+ m.nb.streams = map[int]*nbStream{}97+ }98+ m.nb.streams[q.id] = st99+ m.showStream(st)100+ st.shown = time.Time{} // the first rows show at once101+ m.feedOutput(c.Name())102+ m.note = "Following cell " + strconv.Itoa(i+1) + " as a stream: Stop (i i) ends it"103+ return tea.Batch(pollStream(st), m.tickStreams())104+}105+106+// pollStream polls st's stream on a goroutine of its own.107+func pollStream(st *nbStream) tea.Cmd {108+ return func() tea.Msg {109+ u, ok := st.src.Poll(context.Background())110+ return nbStreamMsg{st, u, ok}111+ }112+}113+114+// streamPolled applies what a stream's poll found: its rows to the region115+// the output was sent to, and to the output, until the stream ends.116+func (m *Model) streamPolled(msg nbStreamMsg) tea.Cmd {117+ st := msg.st118+ if m.nb.streams[st.id] != st {119+ st.src.Close() // stopped, or started again since120+ return nil121+ }122+ c, ok := cellOf(st.nbQueued)123+ if !ok {124+ m.stopStream(st.id)125+ return nil126+ }127+ st.rows = st.src.Rows()128+ st.behind = st.behind || msg.ok129+ if msg.ok {130+ if _, r, ok := m.book().Region(c.Name()); ok && r.Output {131+ if err := m.book().ApplyLive(msg.u.Op(r.Name)); err != nil {132+ m.warn = err.Error()133+ }134+ }135+ }136+ if done, err := st.src.Ended(); done {137+ delete(m.nb.streams, st.id)138+ m.endStream(st, err)139+ return nil140+ }141+ if st.behind && time.Since(st.shown) >= streamOutputEvery {142+ m.showStream(st)143+ }144+ return pollStream(st)145+}146+147+// showStream keeps what the stream has printed so far as the cell's148+// output.149+func (m *Model) showStream(st *nbStream) {150+ st.shown, st.behind = time.Now(), false151+ m.book().SetOutput(st.id, &notebook.Output{NUON: st.src.NUON(), Count: st.count, Took: time.Since(st.start),152+ Source: st.source, Reads: st.reads, Selection: st.selection})153+}154+155+// endStream keeps a stream's output as it ended: its rows, and why it156+// failed if it did.157+func (m *Model) endStream(st *nbStream, err error) {158+ m.showStream(st)159+ if err != nil {160+ o := *m.book().Output(st.id)161+ o.Err = runError(err)162+ m.book().SetOutput(st.id, &o)163+ }164+}165+166+// stopStream stops cell id's stream, keeping what it printed.167+func (m *Model) stopStream(id int) {168+ st := m.nb.streams[id]169+ if st == nil {170+ return171+ }172+ delete(m.nb.streams, id)173+ st.src.Close()174+ m.showStream(st)175+}176+177+// stopStreams stops every stream.178+func (m *Model) stopStreams() {179+ for id := range m.nb.streams {180+ m.stopStream(id)181+ }182+}183+184+// closeStreams stops every stream's process as the program ends,185+// touching nothing else: the model may be broken by a crash.186+func (m *Model) closeStreams() {187+ for _, st := range m.nb.streams {188+ st.src.Close()189+ }190+}191+192+// tickStreams redraws streaming cells' heads while any runs, so the193+// toolbar and the output's run time keep up.194+func (m *Model) tickStreams() tea.Cmd {195+ if m.nb.still || m.nb.streamTick {196+ return nil197+ }198+ m.nb.streamTick = true199+ return tea.Tick(time.Second, func(time.Time) tea.Msg { return nbStreamTickMsg{} })200+}201+202+// nbStreamTickMsg redraws streaming cells.203+type nbStreamTickMsg struct{}204+205+// streamTicked keeps the ticks going while a stream runs.206+func (m *Model) streamTicked() tea.Cmd {207+ m.nb.streamTick = false208+ if len(m.nb.streams) == 0 {209+ return nil210+ }211+ return m.tickStreams()212+}
A internal/ui/nbstream_test.go +139 -0
@@ -0,0 +1,139 @@
1+package ui2+3+import (4+ "context"5+ "io"6+ "strings"7+ "testing"8+9+ "github.com/FelineStateMachine/012/internal/nushell"10+)11+12+// streamPolls are the stream polls commands returned in tests, which13+// run doesn't follow on its own (see pumpStreams).14+var streamPolls []nbStreamMsg15+16+// pumpStreams hands the model the stream polls waiting, and those they17+// start, until a poll finds nothing new after one found rows.18+func pumpStreams(m *Model) {19+ rows := false20+ for i := 0; i < 50 && len(streamPolls) > 0; i++ {21+ msg := streamPolls[0]22+ streamPolls = streamPolls[1:]23+ send(m, msg)24+ if msg.ok {25+ rows = true26+ } else if rows {27+ return28+ }29+ }30+}31+32+// streamNu prints each line sent on lines, as a stream's pipeline33+// would, until lines is closed or the run is stopped.34+type streamNu struct {35+ lines chan string36+ scripts chan string37+}38+39+func newStreamNu() *streamNu {40+ return &streamNu{lines: make(chan string), scripts: make(chan string, 8)}41+}42+43+func (f *streamNu) Run(ctx context.Context, job nushell.Job, script string, stdout io.Writer) error {44+ if len(job.IDE) > 0 || job.Command == "help commands | get name" {45+ return nushell.ErrMissing46+ }47+ f.scripts <- script48+ for {49+ select {50+ case <-ctx.Done():51+ return ctx.Err()52+ case l, ok := <-f.lines:53+ if !ok {54+ return nil55+ }56+ io.WriteString(stdout, l+"\n")57+ }58+ }59+}60+61+// A cell run as a stream shows as live, its rows reaching its output62+// and the sheet it was sent to as the pipeline prints them, and Stop63+// ends it keeping them.64+func TestNotebookStream(t *testing.T) {65+ streamPolls = nil66+ m, _ := notebookModel(t, nil)67+ nu := newStreamNu()68+ m.SetShellRunner(nu)69+ write(t, m, "log = tail -f app.log | lines | parse '{level} {msg}'")70+ press(t, m, "f")71+ if len(m.nb.streams) != 1 || !strings.Contains(screen(m), "● live, 0 rows") {72+ t.Fatalf("not live:\n%s", screen(m))73+ }74+ if script := <-nu.scripts; !strings.Contains(script, "to nuon --raw | print") {75+ t.Errorf("not run as a stream: %s", script)76+ }77+ // Send it to a new sheet while it runs.78+ press(t, m, "G", "<enter>")79+ logs := m.book().Lookup("log")80+ if logs == nil {81+ t.Fatalf("no sheet log: %q", m.note)82+ }83+ nb := m.sheet84+ m.showSheet(m.book().Sheets()[0])85+86+ nu.lines <- "{level: info, msg: started}"87+ pumpStreams(m)88+ nu.lines <- "{level: warn, msg: slow}"89+ pumpStreams(m)90+ if got := logs.Value(addr("B3")).String(); got != "slow" {91+ t.Errorf("B3 = %q", got)92+ }93+ if got := logs.Value(addr("A1")).String(); got != "level" {94+ t.Errorf("A1 = %q", got)95+ }96+ st := m.cellState(nb, firstStream(m))97+ if !st.Live || !st.Running || st.Rows != 2 {98+ t.Errorf("state %+v", st)99+ }100+ run(m, m.runCommand("nb.stop"))101+ if len(m.nb.streams) != 0 {102+ t.Fatal("still streaming after Stop")103+ }104+ m.showSheet(nb)105+ c, _ := m.nbCell()106+ o := m.book().Output(c.ID)107+ if o == nil || o.Failed() || !strings.Contains(string(o.NUON), "slow") {108+ t.Errorf("output after Stop: %+v", o)109+ }110+ if got := logs.Value(addr("B3")).String(); got != "slow" {111+ t.Errorf("after Stop B3 = %q", got)112+ }113+ if strings.Contains(screen(m), "live") {114+ t.Errorf("still shown live:\n%s", screen(m))115+ }116+}117+118+func firstStream(m *Model) int {119+ for id := range m.nb.streams {120+ return id121+ }122+ return -1123+}124+125+// A cell running as a stream reads without color: ● live and its rows126+// in words, and ■ for ▶.127+func TestMonochromeStream(t *testing.T) {128+ streamPolls = nil129+ m, _ := notebookModel(t, nil)130+ nu := newStreamNu()131+ m.SetShellRunner(nu)132+ write(t, m, "tail -f app.log | lines")133+ press(t, m, "f")134+ defer m.stopCells()135+ nu.lines <- `"started"`136+ pumpStreams(m)137+ findLine(t, m, "● live, 1 row")138+ findLine(t, m, "■")139+}
M internal/ui/nbview/draw.go +12 -0
M internal/ui/nbview/toolbar.go +4 -0
M internal/ui/nbview/view.go +6 -1
M internal/ui/notebook.go +7 -4
@@ -43,6 +43,7 @@ var nbKeys = map[string]string{
4343 "n": "nb.name",4444 "G": "nb.send",4545 "i i": "nb.stop",46+ "f": "nb.stream", // follow4647 "0 0": "nb.restart",4748 "f9": "nb.run_all",4849 "V": "nb.paste_above",
@@ -110,7 +111,7 @@ func nbHelpRows(listed map[string]bool) []helpRow {
110111 // notebookItems are Data > Shell's items.111112 var notebookItems = []menuItem{112113 {cmd: "nb.open"}, sep,113- {cmd: "nb.run"}, {cmd: "nb.run_next"}, {cmd: "nb.run_all"}, {cmd: "nb.run_above"}, {cmd: "nb.run_below"}, {cmd: "nb.stop"}, sep,114+ {cmd: "nb.run"}, {cmd: "nb.run_next"}, {cmd: "nb.run_all"}, {cmd: "nb.run_above"}, {cmd: "nb.run_below"}, {cmd: "nb.stream"}, {cmd: "nb.stop"}, sep,114115 {cmd: "nb.insert_above"}, {cmd: "nb.insert_below"}, {cmd: "nb.delete"}, {cmd: "nb.move_up"}, {cmd: "nb.move_down"}, sep,115116 {cmd: "nb.to_code"}, {cmd: "nb.to_note"}, {cmd: "nb.name"}, sep,116117 {cmd: "nb.copy"}, {cmd: "nb.cut"}, {cmd: "nb.paste"}, {cmd: "nb.paste_above"}, sep,
@@ -122,7 +123,7 @@ var notebookItems = []menuItem{
122123 123124 // nbCellMenu is a cell's context menu, on a right click.124125 var nbCellMenu = []menuItem{125- {cmd: "nb.run"}, {cmd: "nb.run_below"}, sep,126+ {cmd: "nb.run"}, {cmd: "nb.run_below"}, {cmd: "nb.stream"}, sep,126127 {cmd: "nb.cut"}, {cmd: "nb.copy"}, {cmd: "nb.paste"}, {cmd: "nb.paste_above"}, {cmd: "nb.delete"}, sep,127128 {cmd: "nb.move_up"}, {cmd: "nb.move_down"}, {cmd: "nb.to_code"}, {cmd: "nb.to_note"}, {cmd: "nb.name"}, sep,128129 {cmd: "nb.toggle_output"}, {cmd: "nb.toggle_whole"}, {cmd: "nb.clear_output"}, {cmd: "nb.open_output"}, {cmd: "nb.send"},
@@ -152,8 +153,10 @@ func init() {
152153 enabled: onCell, run: func(m *Model) tea.Cmd { i, _ := m.nbView().Selected(); return m.runRange(0, i) }},153154 &command{id: "nb.run_below", macro: macroNever, title: "Run cell and below", desc: "Run the selected cell and every code cell under it",154155 enabled: onCell, run: func(m *Model) tea.Cmd { i, _ := m.nbView().Selected(); return m.runRange(i, -1) }},155- &command{id: "nb.stop", macro: macroNever, title: "Stop running", desc: "Stop the cell running, killing its process, and those waiting",156- enabled: func(m *Model) bool { return m.nb.running != nil || len(m.nb.queue) > 0 },156+ &command{id: "nb.stream", macro: macroNever, title: "Run as stream", desc: "Run the selected cell until it's stopped, its rows arriving as the pipeline prints them (tail -f, watch)",157+ enabled: onCode, run: (*Model).streamSelected},158+ &command{id: "nb.stop", macro: macroNever, title: "Stop running", desc: "Stop the cell running, killing its process, those waiting and the cells running as streams",159+ enabled: func(m *Model) bool { return m.nb.running != nil || len(m.nb.queue) > 0 || len(m.nb.streams) > 0 },157160 run: func(m *Model) tea.Cmd { m.stopCells(); return nil }},158161 &command{id: "nb.clear_outputs", title: "Clear outputs", desc: "Clear every cell's output",159162 enabled: onTab, run: func(m *Model) tea.Cmd { m.clearOutputs(false); return nil }},