The notebook section of docs/terminal/nushell.md, the keys, the .012 fields, serve-shell in the SSH guide, the architecture of regions and nushell, a reading-without-color row, demos/notebook.tape and its GIF, and the roadmap row moved to Shipped. The prompt's package has tests of its own against a fake host; region.go is split to stay under 500 lines.
- commit
- 7a46ce145239957c847e4203b67cb961c47340ff
- parent
- abbe6bc8b11156bda223d2f7a95e7e29a38fa860
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.0 of 3, Jev's estimate
| M | README.md | 6 | + |
| M | ROADMAP.md | 10 | +-- |
| A | demos/ | 32 | ++++++++ |
| M | docs/ | 42 | ++++++++++- |
| M | docs/ | 1 | + |
| A | docs/ | bin | |
| M | docs/ | 122 | ++++++++++++++++++++++++++++++ |
| M | docs/ | 16 | +++- |
| M | internal/ | 63 | --------------- |
| A | internal/ | 66 | ++++++++++++++++ |
| A | internal/ | 100 | +++++++++++++++++++++++++ |
@@ -45,6 +45,10 @@ rest: building from a clone, importing files, `012 serve` and settings.
4545 - **A stage in a pipeline**: `ls | to nuon | 012 --pipe | from nuon`4646 edits a table on the terminal and sends it on with its types4747 ([nushell and pipelines](docs/terminal/nushell.md)).48+- **Nushell notebooks**: `012 nu` (or `!` in any workbook) runs nushell49+ pipelines whose tables become live, named regions of the sheet;50+ `$r1` reads one in the next command, and refreshing it runs what reads51+ it ([notebooks](docs/terminal/nushell.md#notebooks)).4852 - **Macros**, recorded or written as Starlark scripts saved with the sheet4953 ([macros](docs/sheets/macros.md)).5054 - **Made for terminals**: the mouse, hyperlinks, the system clipboard over@@ -64,6 +68,8 @@ rest: building from a clone, importing files, `012 serve` and settings.
6468 | Arrays that spill | Wrapped text, borders and merged cells |6569 |  |  |6670 | JEV functions in formulas | Macros |71+|  | |72+| Nushell notebooks | |6773 6874 Each guide shows its own recordings too. They are6975 [VHS](https://github.com/charmbracelet/vhs) tapes in [`demos/`](demos),@@ -26,16 +26,11 @@ rises in measured steps; see [Bounds of support](docs/contributing/limits.md#wha
2626 2727 ### 4. Nushell notebook2828 29-One binary: `012 nu`, `012 -` and `012 --pipe` are modes of 012, and a30-notebook sheet lives in a workbook beside ordinary sheets. `012 nu` runs31-`nu` as a separate process; without it, it says so and the rest of 01232-is unchanged. Written like a REPL, kept like a reactive notebook: each33-command's result is a named, live region (`r1`) that later commands read34-as `$r1`, and refreshing a region re-runs what depends on it.29+What's left of making 012 and nushell one tool, on top of the notebook30+([Notebooks](docs/terminal/nushell.md#notebooks)).3531 3632 | Item | Result | Size |3733 |---|---|---|38-| `012 nu` and the notebook sheet: a nushell prompt in the formula bar (history, Tab completion), results stacking as named live regions built on spills, `$r1` references with a dependency graph so refresh cascades, Freeze to values, other sheets reading `Shell!r1`, commands saved and never run on open, trust for files from elsewhere, timeouts and an output cap, off in `012 serve` unless configured | A reactive notebook of pipelines inside a spreadsheet | L |3934 | Nushell's own completions in the prompt; `nu_plugin_012` only if `--pipe` proves clumsy from inside nu | | S to M |4035 4136 ### 5. Toward multiplayer@@ -111,6 +106,7 @@ stream above rather than a design of their own.
111106 - Shift+Enter, Ctrl+I and keys held to preview, with the kitty keyboard protocol: [Keys and mouse](docs/reference/keys.md#keys-the-terminal-has-to-tell-apart)112107 - Sixel chart images on terminals without kitty graphics, drawn after the frame and redrawn as the screen moves: [Charts](docs/sheets/charts.md)113108 - 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: [Nushell and pipelines](docs/terminal/nushell.md)109+- `012 nu` and notebook sheets: nushell pipelines at a prompt on the formula bar become live, named regions that read each other and refresh in dependency order, saved as commands and never run on open: [Notebooks](docs/terminal/nushell.md#notebooks)114110 115111 **Files**116112 @@ -0,0 +1,32 @@
1+# Notebooks: 012 nu opens a notebook at the nu❯ prompt. Each pipeline's2+# table becomes a named region; a second reads the first as $sales, and3+# giving sales a new command runs it again and then the region reading4+# it. Needs nu on the PATH.5+Output out/notebook.gif6+Source lib/setup.tape7+8+Hide9+Type "../bin/012 nu" Enter10+Sleep 2s11+Show12+13+Sleep 800ms14+Type "sales = open data/sales.csv | select Region Quarter Units"15+Sleep 500ms16+Enter17+Sleep 1.5s18+Type "$sales | where Units > 250 | sort-by Units --reverse"19+Sleep 500ms20+Enter21+Sleep 2s22+Screenshot out/stills/notebook-regions.png23+Type "sales = open data/sales.csv | where Quarter == Q4 | select Region Quarter Units"24+Sleep 500ms25+Enter26+Sleep 2.5s27+Escape28+Sleep 500ms29+Down Down Down30+Sleep 1.5s31+Screenshot out/stills/notebook-refreshed.png32+Sleep 1.5s@@ -23,6 +23,7 @@ internal/chart chart layout, text rendering and kitty image encoding
2323 internal/jev the API key's resolution, answer cache and TypeSafe client2424 internal/keyring the OS credential store the API key lives in2525 internal/macro macros: the Starlark scripting API, recorded actions as scripts, step limits26+internal/nushell a notebook's commands: nu run as a process, tables in as NUON files, a table back2627 internal/telemetry opt-in JSON log and OTLP export of spans, events and frame stats2728 internal/serve the SSH server: auth, host key, a Model per session (charm.land/wish/v2)2829 internal/confine resolving typed file names, confined to a directory when served@@ -35,6 +36,7 @@ internal/ui the Bubble Tea model: modes, menus, overlays, rendering
3536 lineedit the one-line editor every text field shares3637 picker the searchable list behind the palette and every picker3738 cmdline the : command line and its completions39+ nuprompt the nushell prompt on the formula bar, its history and completions3840 findbar find and replace, a bar on the context line3941 themepicker File > Settings > Theme, previewing as it moves4042 rules the conditional formatting and data validation panel@@ -212,6 +214,26 @@ style.
212214 format) keeps only its formatting and note. A blocked anchor shows213215 `#REF!` with the reason. `Set` refuses spilled cells; files keep only214216 the anchor.217+- **Regions.** A region (`region.go`) is a block of cells whose values218+ come from outside the engine: an anchor, its label line, and a219+ definition naming what fills it (a notebook's nushell command, opaque220+ to the engine), with the data it shows (`RegionData`) handed over by221+ the UI (`ShowRegion`). Definitions and data are the sheet's222+ `regionState`, replaced whole on every change so undo steps keep them;223+ what isn't undone (a label's status, the cells written) is kept224+ beside it. After a change touching regions, their cells are written225+ again (`regionwrite.go`) as spilled cells are, derived and outside the226+ undo history, only those that differ, and cells no region covers any227+ more are cleared; a table that would overwrite other contents shows228+ only its label, saying where. On a notebook sheet a table that grows229+ or shrinks inserts or deletes rows under it inside the same step, so230+ the rows below move as they would by hand. Formulas name a region's231+ table as `nu.name`, resolved when evaluated as named ranges are; the232+ regions' `$name` references make a dependency graph233+ (`regiongraph.go`) giving refresh and run orders, and cycles are234+ refused. The file keeps the definitions (`regionfile.go`). Anything235+ that fills cells from outside (a file followed as it grows) is a236+ region with another kind of command.215237 - **Rules.** A sheet's conditional formats and data validation216238 (`rules.go`, `condfmt.go`, `validation.go`) are lists of rules on217239 ranges, replaced whole on every change so undo steps keep them as they@@ -376,6 +398,7 @@ draw. The components:
376398 | sheet tabs | `tabstrip.Strip` | where each sheet was left, the tab strip's scroll and layout (package `tabstrip`); what clicks on it do (`tabstrip.go`) |377399 | mouse | `mouseState` | drags, hover, double clicks, the fill handle (`mouse.go`, `fill.go`) |378400 | import | `transfer.Transfer`, `pipeState` | the import in progress, its progress display and cancelling (package `transfer`); choosing and placing imports (`transfer.go`, `importplace.go`); standard input read as a sheet, and what a pipeline gets on quitting (`pipe.go`) |401+| notebooks | `nuprompt.Prompt`, `shellState` | the prompt (package `nuprompt`); the commands running one at a time in the background, their queue and what they said (`nurun.go`); the notebook's commands, keys, lines and trust (`notebook.go`) |379402 | macros | `recorder`, `macroState` | a recording in progress (`macrorec.go`); a macro running, trust in the file's macros (`macrorun.go`); what scripts act on (`macrohost.go`, `macrohostnav.go`); Data > Macros and the manager (`macro.go`, `macromanage.go`) |380403 | others | `clipboard`, `trace`, `chartState`, `jevRunner`, `terminal`, `session` | what Ctrl+V pastes, a trace being shown, chart commands' target, JEV questions in flight, what the terminal supports and the chart images sent to it, what outlasts the file open (the `:` history, whether keys can be held: `keyboard.go`) |381404 @@ -402,6 +425,7 @@ methods off `ui.Model`'s exported API:
402425 | `sortbar` | `sortbar.Host` | theme, size, the sheet, close, sort (recorded as the bar's command) (5) |403426 | `filterpick` | `filterpick.Host` | theme, size, the edit line, close, the locale (5); what applying and cancelling do are callbacks, as the sheet's filter and a pivot's differ |404427 | `cmdline` | `cmdline.Host` | theme, size, the edit line, close, the commands to complete, run a line, fail, the session's history (8) |428+| `nuprompt` | `nuprompt.Host` | theme, size, the edit line, close, run a line, the words to complete, the workbook's history, what's running, stop it, what the last run said (10) |405429 | `suggest` | `suggest.Host` | theme, size, the edit line, whether an entry is being typed, the entry's sheet, the formula as parsed, where the formula bar's text starts (7) |406430 | `themepicker` | `themepicker.Host` | a picker's host, and the current theme, the themes directory, preview, keep, whether keys can be held (10) |407431 | `rules` | `rules.Host` | theme, size, the edit line, close, the sheet and selection, save a conditional format or a validation rule, follow a rule removed or moved (each recorded as the commands that do it), the terminal's palette colors (10) |@@ -447,7 +471,7 @@ hints) are what every overlay is drawn with.
447471 448472 **Packages.** `theme`, `rowtext`, `formula`, `overlay` and `lineedit`449473 depend on nothing in `ui`, so they can be tested and measured alone. The450-components in `picker`, `cmdline`, `themepicker`, `findbar`, `rules`,474+components in `picker`, `cmdline`, `nuprompt`, `themepicker`, `findbar`, `rules`,451475 `sortbar`, `filterpick`, `choicebar`, `shortcuts`, `suggest`, `tabstrip`452476 and `transfer` build on them and reach the model only through their453477 hosts, with unit tests of their own against fake hosts. A component moves out of package@@ -525,6 +549,22 @@ were made or trusted on (an id from `cmd/012`); a macro from elsewhere
525549 asks once before it runs. Only the local app lets scripts be edited in526550 the user's editor (`AllowEditor`), since that starts a program.527551 552+## Notebooks553+554+`internal/nushell` runs a notebook's command: a `Job` holds the command555+and the NUON of the tables it reads, and the script `nu -c` runs binds556+each table from a file named by an environment variable557+(`let r1 = (open --raw $env.NU012_TABLE_0 | from nuon)`), feeds the558+selection as `$in` on standard input, and ends in `to nuon`, so no data559+is ever spliced into the command. What nu prints is read as it's560+written by the NUON importer, capped at max-cells. `Runner` is the561+seam: tests hand the UI a fake, the e2e screens a stand-in `nu`562+script. The UI runs one command at a time as a `tea.Cmd` with a563+context Esc cancels and a timeout, and shows each table as an undo564+step when it arrives; the queue a refresh makes comes from the565+engine's graph. Trust is the macros': a file's commands run only once566+they were made or trusted on this computer, or the user agrees.567+528568 ## Charts529569 530570 `internal/chart` draws a chart in two layers that share one layout: text@@ -92,6 +92,7 @@ there.
9292 | Warnings | Text on the context line or status line (`Invalid: ...`, `Circular reference`) |9393 | Entries failing validation | A dotted underline, and `Invalid:` with the rule on the context line |9494 | Values an array spilled | Italic, and `Spilled from B2` on the context line |95+| A notebook's regions | The label line's text (`r1 ls`, and `Running…` or `failed`) underlined as wide as the table, and `Region r1 of ls` on the context line |9596 | Pivot table results | Their headings (`SUM of Units`, `Grand Total`), and a note in words when an edit is refused |9697 | Protected ranges | A question in words before an edit (`A1:B2 is protected.`) |9798 | Notes | A `▝` in the cell's top-right corner |Binary file.
@@ -21,6 +21,128 @@ Windows), not standard input or output, so the pipeline's data never
2121 mixes with what 012 draws, and nothing but the table reaches standard2222 output.2323 24+The other way round, a [notebook](#notebooks) runs nushell from inside25+012: each pipeline's table becomes a live part of the sheet.26+27+## Notebooks28+29+30+31+`012 nu` opens a notebook: a sheet where you type nushell pipelines and32+each one's table lands in the grid, named, typed and kept live. Written33+like a REPL, kept like a spreadsheet.34+35+```sh36+012 nu # a new notebook37+012 nu work.012 # open one, or make it38+```39+40+In any workbook, `!` (or **Data > Shell**, or the palette) opens the41+same prompt, writing into the workbook's notebook sheet, or a new one42+named Shell 1 at the first command. Commands run `nu` as a separate43+process, so nushell must be installed; without it, the prompt says so44+and the rest of 012 works as before.45+46+### The prompt47+48+The formula bar becomes `nu❯`, and the mode indicator says `NU`:49+50+| Key | Does |51+|---|---|52+| Enter | Run the line; the prompt stays open for the next |53+| Tab, Shift+Tab | Complete the word at the caret: a region (`$r1`) or one of nu's commands, from the box under the context line; again for the next |54+| Up, Down | The workbook's earlier lines, those starting with what's typed; they're saved with the file |55+| Esc | Stop the command running; otherwise, back to the grid |56+57+A name and `=` before the pipeline name the region; without one it's58+`r1`, `r2` and so on:59+60+```nu61+sizes = ls | select name size62+```63+64+Typing a name that exists gives that region a new command and runs it65+again, as does F2 on its label.66+67+### Regions68+69+Each command's table is a region: a label line with its name and70+command, dimmed and underlined as wide as the table, then the table,71+its first row the column names. Regions stack down the sheet in the72+order they ran, a row between them; when a table grows or shrinks, the73+rows below it move, as if you'd inserted or deleted rows.74+75+A region is still cells of the grid. Its values are76+[typed](#types) (file sizes in the Size format, durations, dates),77+formulas read them, and you can format them, filter them78+(**Data > Create a filter**), sort them (the sort is kept, so the table79+stays sorted when it runs again) and chart them. Like an80+[array's spill](../formulas/arrays.md#spilled-cells), they can't be81+typed over: change the command, or **Data > Shell regions > Freeze82+region** turns the table into plain values and stops running it. Copies83+paste values; downloads and XLSX hold the values; undo takes back a run84+(the table it replaced comes back) or the region itself.85+86+The context line says what's under the pointer: on a label, the keys87+that refresh and edit it, and why it failed if it did; on its table,88+which region the cell is part of. A label's end says what it's doing:89+`Running…`, `waiting` for a region it reads, `failed`, `stopped`,90+`not run`, or `no rows`.91+92+### Tables between regions93+94+`$r1` in a command is region r1's table, handed to nu as NUON in a file95+nu reads, never as text spliced into the command. `$in` is the range96+selected when the prompt was opened, on a sheet that isn't the97+notebook, read again each time the region runs:98+99+```nu100+big = $r1 | where size > 1kb | sort-by size --reverse101+$in | group-by region | transpose region rows102+```103+104+A region reading another depends on it. Refreshing a region (Enter on105+its label, **Data > Refresh region**) runs it again and then every region106+that reads it, directly or through others, each after what it reads.107+F9 (**Data > Shell regions > Run all regions**) runs them all. A command108+reading itself, directly or through others, is refused.109+110+Other sheets read a region by name with `nu.` in front, as a named range111+of its table, header included: `=SUM(nu.big)`, `=VLOOKUP("go.mod",112+nu.r1, 3, FALSE)`. Formulas follow the table as it grows, shrinks or113+moves, and show `#REF!` while it has none.114+115+### Running116+117+Commands run in the background, one at a time: the screen stays live,118+and Esc stops the one running and those waiting after it. Each runs as119+`nu --no-config-file -c` (with your own config files if `nu-config` is120+on), stops after `nu-timeout` (30 seconds unless set), and keeps at121+most `max-cells` cells, whole rows, saying how many it left out. When a122+command fails, its label says so and the context line gives nushell's123+message.124+125+### Saving and trust126+127+The file keeps each region's command, name, place and what it reads,128+not its table ([format](../files/format.md#regions)). Opening a notebook129+never runs anything: its regions say `not run`, and F9 runs them all,130+each after what it reads.131+132+Like [macros](../sheets/macros.md#macros-from-other-computers), a133+file's commands run as you, with your files, so a notebook saved on134+another computer asks once before they run:135+136+```137+Run this file's shell commands? Enter Run Esc Cancel138+```139+140+What you type at the prompt runs without asking. The `shell` option141+decides: `ask` (the default) as above, `on` never asks, and `off` runs142+nothing. [012 serve](ssh.md#notebooks) runs no commands unless the143+server's `serve-shell` is on. See [Configuration](../reference/config.md#nushell-notebooks)144+for the options.145+24146 ## Reading standard input25147 26148 `012 -` reads a table from standard input into a new sheet named@@ -86,8 +86,9 @@ the authorized keys. Everything else is closed.
8686 - **Host key.** An ed25519 key generated on first run, written `0600`8787 in a `0700` directory. 012 refuses to start if the key is readable by8888 others.89-- **What a session can do.** Run 012, nothing else; no command is ever90- executed. A shell request starts 012 on a new sheet. An exec request89+- **What a session can do.** Run 012, nothing else; no command is90+ executed, unless the server's config turns on notebooks' nushell91+ commands (see [Notebooks](#notebooks)). A shell request starts 012 on a new sheet. An exec request9192 is accepted only with a terminal (`ssh -t`) and only when it is one9293 word, which is taken as a file name and never run: it resolves inside9394 the served directory like a name typed in File > Open, and anything@@ -148,6 +149,17 @@ server. A file's macros ask for trust once per session, since the
148149 session isn't the server's own computer, and scripts have no file,149150 network or clock access in any case.150151 152+## Notebooks153+154+A session opens [notebooks](nushell.md#notebooks) and shows their155+regions, but runs none of their commands: a command is a program on the156+server, with the server's user and every file it can reach, well157+beyond the served directory. `serve-shell = on` in the server's config158+lets sessions run them, as the user 012 serve runs as, following the159+`shell` option as the local app does; a file's commands still ask once160+per session before they run. Only turn it on when everyone holding an161+authorized key may run programs on the server.162+151163 ## Unsaved work152164 153165 A session that ends without the user quitting, because it was idle for@@ -7,8 +7,6 @@ import (
77 "slices"88 "strconv"99 "strings"10-11- "github.com/FelineStateMachine/012/internal/formula"1210 )1311 1412 // Regions are blocks of cells whose values come from outside the@@ -72,48 +70,6 @@ type regionMeta struct {
7270 why string // why the table isn't shown, or ""7371 }7472 75-// RegionData is a region's table: Rows by Cols values, row by row, the76-// header row first, with the formats they come in (a file size's Size,77-// a date's Date time) by index.78-type RegionData struct {79- Rows, Cols int80- Values []Value81- Formats map[int]Format82- // Note says what was left out, e.g. rows past max-cells.83- Note string84-}85-86-// At returns the value at row r, column c and its format.87-func (d *RegionData) At(r, c int) (Value, Format) {88- if d == nil || r < 0 || c < 0 || r >= d.Rows || c >= d.Cols {89- return Value{}, Format{}90- }91- i := r*d.Cols + c92- return d.Values[i], d.Formats[i]93-}94-95-// RegionDataOf copies the used range of src, from A1, as a region's96-// table: how a table read by an importer becomes one.97-func RegionDataOf(src *Sheet) *RegionData {98- used, ok := src.UsedRange()99- if !ok {100- return &RegionData{}101- }102- d := &RegionData{Rows: used.To.Row + 1, Cols: used.To.Col + 1}103- d.Values = make([]Value, d.Rows*d.Cols)104- for a := range src.cells.keysIn(Rect{To: used.To}) {105- i := a.Row*d.Cols + a.Col106- d.Values[i] = src.Value(a)107- if f := src.DisplayFormat(a); !f.IsZero() {108- if d.Formats == nil {109- d.Formats = map[int]Format{}110- }111- d.Formats[i] = f112- }113- }114- return d115-}116-11773 // Errors of regions.11874 var (11975 // ErrRegionEdit is what typing into a region's cell says.@@ -500,22 +456,3 @@ func sameRegions(a, b regionState) bool {
500456 }501457 return true502458 }503-504-// boundRegion is a name in a formula that isn't a named range: a505-// region's table (nu.r1), #REF! while it has none, or the name as506-// written, which shows #NAME?.507-func (s *Sheet) boundRegion(nn formula.Name) Node {508- t, r, ok := s.wb.regionName(nameKey(nn.Name))509- switch {510- case !ok:511- return nn512- case r == (Rect{}):513- return formula.RefErr{}514- }515- sheet := ""516- if t != s {517- sheet = t.name518- }519- const fixed = formula.AbsCol | formula.AbsRow520- return formula.Range{Rect: r, Abs: [2]formula.Abs{fixed, fixed}, Sheet: sheet}521-}@@ -0,0 +1,66 @@
1+package sheet2+3+import "github.com/FelineStateMachine/012/internal/formula"4+5+// What fills a region, and how formulas name one (see region.go).6+7+// RegionData is a region's table: Rows by Cols values, row by row, the8+// header row first, with the formats they come in (a file size's Size,9+// a date's Date time) by index.10+type RegionData struct {11+ Rows, Cols int12+ Values []Value13+ Formats map[int]Format14+ // Note says what was left out, e.g. rows past max-cells.15+ Note string16+}17+18+// At returns the value at row r, column c and its format.19+func (d *RegionData) At(r, c int) (Value, Format) {20+ if d == nil || r < 0 || c < 0 || r >= d.Rows || c >= d.Cols {21+ return Value{}, Format{}22+ }23+ i := r*d.Cols + c24+ return d.Values[i], d.Formats[i]25+}26+27+// RegionDataOf copies the used range of src, from A1, as a region's28+// table: how a table read by an importer becomes one.29+func RegionDataOf(src *Sheet) *RegionData {30+ used, ok := src.UsedRange()31+ if !ok {32+ return &RegionData{}33+ }34+ d := &RegionData{Rows: used.To.Row + 1, Cols: used.To.Col + 1}35+ d.Values = make([]Value, d.Rows*d.Cols)36+ for a := range src.cells.keysIn(Rect{To: used.To}) {37+ i := a.Row*d.Cols + a.Col38+ d.Values[i] = src.Value(a)39+ if f := src.DisplayFormat(a); !f.IsZero() {40+ if d.Formats == nil {41+ d.Formats = map[int]Format{}42+ }43+ d.Formats[i] = f44+ }45+ }46+ return d47+}48+49+// boundRegion is a name in a formula that isn't a named range: a50+// region's table (nu.r1), #REF! while it has none, or the name as51+// written, which shows #NAME?.52+func (s *Sheet) boundRegion(nn formula.Name) Node {53+ t, r, ok := s.wb.regionName(nameKey(nn.Name))54+ switch {55+ case !ok:56+ return nn57+ case r == (Rect{}):58+ return formula.RefErr{}59+ }60+ sheet := ""61+ if t != s {62+ sheet = t.name63+ }64+ const fixed = formula.AbsCol | formula.AbsRow65+ return formula.Range{Rect: r, Abs: [2]formula.Abs{fixed, fixed}, Sheet: sheet}66+}@@ -0,0 +1,100 @@
1+package nuprompt2+3+import (4+ "strings"5+ "testing"6+7+ tea "charm.land/bubbletea/v2"8+9+ "github.com/FelineStateMachine/012/internal/ui/lineedit"10+ "github.com/FelineStateMachine/012/internal/ui/theme"11+)12+13+type fakeHost struct {14+ th theme.Theme15+ line lineedit.Line16+ closed bool17+ ran []string18+ history []string19+ running string20+ stopped bool21+}22+23+func (h *fakeHost) Theme() *theme.Theme { return &h.th }24+func (h *fakeHost) Size() (int, int) { return 80, 24 }25+func (h *fakeHost) Line() *lineedit.Line { return &h.line }26+func (h *fakeHost) Close() { h.closed = true }27+func (h *fakeHost) ShellHistory() []string { return h.history }28+func (h *fakeHost) Running() string { return h.running }29+func (h *fakeHost) Stop() { h.stopped = true }30+func (h *fakeHost) Said() string { return "" }31+func (h *fakeHost) Submit(line string) tea.Cmd {32+ h.ran = append(h.ran, line)33+ return nil34+}35+func (h *fakeHost) Words() []Word {36+ return []Word{{Text: "$r1", Desc: "ls"}, {Text: "$big", Desc: "$r1 | first"}, {Text: "sort-by", Desc: "command"}, {Text: "str join", Desc: "command"}}37+}38+39+func typeText(p *Prompt, text string) {40+ for _, r := range text {41+ p.Key(tea.KeyPressMsg{Code: r, Text: string(r)})42+ }43+}44+45+func key(p *Prompt, code rune) { p.Key(tea.KeyPressMsg{Code: code}) }46+47+func TestCompletes(t *testing.T) {48+ h := &fakeHost{th: theme.New(true)}49+ p := New(h, "")50+ typeText(p, "$")51+ if len(p.Shown()) != 2 {52+ t.Fatalf("$ completes %+v", p.Shown())53+ }54+ typeText(p, "b")55+ key(p, tea.KeyTab)56+ if h.line.Text() != "$big" {57+ t.Errorf("tab put %q", h.line.Text())58+ }59+ typeText(p, " | st")60+ if got := p.Shown(); len(got) != 1 || got[0].Text != "str join" {61+ t.Errorf("st completes %+v", got)62+ }63+ key(p, tea.KeyTab)64+ if h.line.Text() != "$big | str join" {65+ t.Errorf("tab put %q", h.line.Text())66+ }67+ // A word that isn't a command's place isn't completed.68+ typeText(p, " so")69+ if len(p.Shown()) != 0 {70+ t.Errorf("an argument completes %+v", p.Shown())71+ }72+ if !strings.Contains(p.FormulaBar(), Mark) {73+ t.Errorf("formula bar %q", p.FormulaBar())74+ }75+}76+77+func TestRunsAndRecalls(t *testing.T) {78+ h := &fakeHost{th: theme.New(true), history: []string{"ls", "$r1 | first"}}79+ p := New(h, "")80+ key(p, tea.KeyUp)81+ if h.line.Text() != "$r1 | first" {82+ t.Errorf("up %q", h.line.Text())83+ }84+ key(p, tea.KeyUp)85+ key(p, tea.KeyEnter)86+ if len(h.ran) != 1 || h.ran[0] != "ls" || h.line.Text() != "" {87+ t.Errorf("ran %q, line %q", h.ran, h.line.Text())88+ }89+ // Esc stops what runs, then goes back to the grid.90+ h.running = "r1"91+ key(p, tea.KeyEscape)92+ if !h.stopped || h.closed {93+ t.Errorf("esc while running: stopped %v closed %v", h.stopped, h.closed)94+ }95+ h.running = ""96+ key(p, tea.KeyEscape)97+ if !h.closed {98+ t.Error("esc didn't close")99+ }100+}