cat / 012
clone

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

Merge main

Dami committed 6fb7f09
commit
6fb7f09d6b9afd480871dfb9968761afc1c40903
parent
efbed527a23a21792eb5d163ac1ead7138961e22
parent
6c03099a895951ce3844d7178f97887bf1693d6b
author
Dami <dami.etoile@gmail.com>
83 files changed, +2155 -54
MROADMAP.md12+-
Mdocs/contributing/testing.md4+-
Mdocs/formulas/arrays.md5+-
Adocs/media/notebook-nu-help-dark.pngbin
Adocs/media/notebook-nu-help-light.pngbin
Adocs/media/notebook-nu-hover-dark.pngbin
Adocs/media/notebook-nu-hover-light.pngbin
Mdocs/media/palette-dark.pngbin
Mdocs/media/palette-light.pngbin
Mdocs/nushell/notebooks.md26++
Mdocs/reference/keys.md1+
Me2e/notebook_test.go46++++
Me2e/screennotebook_test.go14+
Me2e/stills_test.go2+
Ae2e/testdata/nu/ide/hover.ast.json1+
Ae2e/testdata/nu/ide/hover.check.jsonl3+
Ae2e/testdata/nu/ide/hover.hover-73.json1+
Ae2e/testdata/nu/ide/hover.nu4+
Me2e/testdata/nu/nu10+-
Ae2e/testdata/screens/docs-notebook-nu-help-light.html24++
Ae2e/testdata/screens/docs-notebook-nu-help.html24++
Ae2e/testdata/screens/docs-notebook-nu-hover-light.html16+
Ae2e/testdata/screens/docs-notebook-nu-hover.html16+
Me2e/testdata/screens/docs-palette-light.html2+-
Me2e/testdata/screens/docs-palette.html2+-
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-nu-help-high-contrast.html30+++
Ae2e/testdata/screens/notebook-nu-help-light.html30+++
Ae2e/testdata/screens/notebook-nu-help.html30+++
Ae2e/testdata/screens/notebook-nu-hover-high-contrast.html30+++
Ae2e/testdata/screens/notebook-nu-hover-light.html30+++
Ae2e/testdata/screens/notebook-nu-hover.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/nushell/hover.go285++++++++++++++++++++++++
Ainternal/nushell/hover_test.go97++++++++
Ainternal/nushell/testdata/ide/hover-flag.json1+
Ainternal/nushell/testdata/ide/hover-into-string.json1+
Ainternal/nushell/testdata/ide/hover-none.json0
Ainternal/nushell/testdata/ide/hover-sort-by.json1+
Ainternal/nushell/testdata/ide/hover-variable.json1+
Ainternal/nushell/testdata/ide/hover.nu2+
Minternal/sheet/merge.go9+-
Minternal/sheet/merge_undo_test.go18++
Minternal/sheet/randedit_test.go4+-
Minternal/sheet/regionclash.go56+++++
Minternal/sheet/regionclash_test.go44++++
Minternal/sheet/regionwrite.go12+
Minternal/sheet/spill.go9+-
Minternal/sheet/spillblock.go55++++-
Minternal/sheet/spillcycle_test.go36+++
Ainternal/sheet/testdata/fuzz/FuzzRandomEdits/6452b8f0efea90632+
Ainternal/sheet/testdata/fuzz/FuzzRandomEdits/8fbc50e2c14bfe4c2+
Ainternal/sheet/testdata/fuzz/FuzzRandomEdits/a704f635622be9562+
Ainternal/ui/cmdhelp/cmdhelp.go228+++++++++++++++++++
Ainternal/ui/cmdhelp/cmdhelp_test.go73++++++
Minternal/ui/hosts.go2+
Minternal/ui/nblang.go26++-
Minternal/ui/nblang_test.go80+++++++
Minternal/ui/nbscreen.go2+
Minternal/ui/nbview/edit.go14+-
Ainternal/ui/nbview/hover.go352++++++++++++++++++++++++++++++
Ainternal/ui/nbview/hover_test.go179+++++++++++++++
Minternal/ui/nbview/keys.go2+-
Minternal/ui/nbview/mouse.go2+-
Minternal/ui/nbview/nu_test.go24++-
Ainternal/ui/nbview/nuhover.go152+++++++++++++
Minternal/ui/nbview/panel.go6+
Minternal/ui/nbview/syntax.go1+
Ainternal/ui/nbview/testdata/nu/hover-101.json1+
Ainternal/ui/nbview/testdata/nu/hover-113.json1+
Ainternal/ui/nbview/testdata/nu/hover-135.json1+
Ainternal/ui/nbview/testdata/nu/hover-83.json1+
Ainternal/ui/nbview/testdata/nu/hover-96.json1+
Ainternal/ui/nbview/testdata/nu/hover.nu5+
Minternal/ui/nbview/view_test.go3+-
Minternal/ui/notebook.go5+-
M ROADMAP.md +1 -11
@@ -22,17 +22,6 @@ The grid is Excel's, 1,048,576 x 16,384, and memory is bounded by the
2222 rises in measured steps; see [Bounds of support](docs/contributing/limits.md#what-would-raise-the-bounds).2323 Tables past it are read in place as [linked sources](docs/files/sources.md).2424 25-### 2. A TUI Jupyter for nushell26-27-Notebook tabs are a TUI Jupyter for nushell28-([Notebooks](docs/nushell/notebooks.md)): code and note cells, outputs in29-place, sent to sheets as live regions, and cells highlighted, checked and30-completed by nu as they're written.31-32-| Item | Result | Size |33-|---|---|---|34-| A command's signature and description for the word at the caret, on the context line, through `nu --ide-hover` or `nu --lsp` | Writing a cell without leaving for `help` | S |35-3625 ## Shelved3726 3827 Explored, not scheduled; the fit is still open.
@@ -118,6 +107,7 @@ needs cgo.
118107 - 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)119108 - 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)120109 - 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)110+- The word at the caret on the context line: a command's signature and summary (`--ide-hover`), F1 for its whole help and docs page, and a `$name`'s cell and shape: [Notebooks](docs/nushell/notebooks.md#writing-a-cell)121111 - Table and record outputs drawn and worked as 012's own grid: formats by type, fitted and resizable columns, select, copy, sort, filter and find in place, full-screen, charts and pivots on the sheet the output is sent to: [Notebooks](docs/nushell/notebooks.md#outputs-as-grids)122112 123113 **Files**
M docs/contributing/testing.md +2 -2
M docs/formulas/arrays.md +4 -1
A docs/media/notebook-nu-help-dark.png +0 -0

Binary file.

A docs/media/notebook-nu-help-light.png +0 -0

Binary file.

A docs/media/notebook-nu-hover-dark.png +0 -0

Binary file.

A docs/media/notebook-nu-hover-light.png +0 -0

Binary file.

M docs/media/palette-dark.png +0 -0

Binary file.

M docs/media/palette-light.png +0 -0

Binary file.

M docs/nushell/notebooks.md +26 -0
@@ -241,6 +241,7 @@ delete, copy, cut, move, make notes or code, hide or clear outputs.
241241 | Up, Down, Home, End | Move by the lines on screen, wrapped lines too |242242 | Ctrl+A, Ctrl+E | The start, the end of the line |243243 | Tab | Complete the word at the caret: a cell's `$name`, `$selection`, a linked file, then what nu completes there (commands, flags, paths); one completion goes in at once ([Writing a cell](#writing-a-cell)) |244+| F1 | The help of the command at the caret, with its page in nushell's docs ([Writing a cell](#writing-a-cell)) |244245 245246 A terminal without the kitty keyboard protocol sends Shift+Enter and246247 Ctrl+Enter as Enter ([Keys the terminal has to tell
@@ -291,6 +292,31 @@ it the context line says what nu said:
291292 292293 Tab asks `nu --ide-complete` too, after the notebook's own names, so it293294 completes flags, subcommands and paths as well as cells and commands.295+296+Once the caret rests on a word, the context line says what it is, so297+writing a cell doesn't mean leaving it for `help`. On a command, `nu298+--ide-hover` gives its signature (its parameters with their types,299+then its flags) and what it does; on a flag, what the flag does. Where300+the line is short, the flags give way first, then the types, then the301+end of the description. A problem at the caret takes the line instead.302+303+![A cell being edited with the caret on sort-by: the context line shows sort-by's signature, its parameter's types, and what it does, with F1 for help at the right](../media/notebook-nu-hover-dark.png#gh-dark-mode-only)304+![A cell being edited with the caret on sort-by: the context line shows sort-by's signature, its parameter's types, and what it does, with F1 for help at the right](../media/notebook-nu-hover-light.png#gh-light-mode-only)305+306+F1 opens the command's whole help over the notebook: what it does, a307+link to its page in nushell's docs, its usage, flags, parameters, input308+and output types and examples. Up and Down scroll it, Esc goes back to309+the cell. F1 on a word that isn't a command opens the keyboard shortcuts,310+as it does anywhere else.311+312+![The help of sort-by over the notebook: its description, a link to its page in nushell's docs, its usage, flags, parameters and input and output types, with a count of its lines in the bottom border](../media/notebook-nu-help-dark.png#gh-dark-mode-only)313+![The help of sort-by over the notebook: its description, a link to its page in nushell's docs, its usage, flags, parameters and input and output types, with a count of its lines in the bottom border](../media/notebook-nu-help-light.png#gh-light-mode-only)314+315+On a `$name` another cell binds, the line says the cell and the shape316+of its value as it last ran (`$files: table, 12 rows × 4 columns from317+cell 3`), from the notebook's own outputs without asking nu; on318+`$selection`, a linked file or `$sheet.A1:C9`, what it reads; on a319+variable the cell binds itself with `let`, the type nu gives it.294320 nu doesn't know the variables a notebook binds, so it's asked about the295321 cell with `$name` for each name the cells assign and each linked file,296322 `$selection` and `$sheet` declared before it; a `$sheet.A1:C9` range
M docs/reference/keys.md +1 -0
M e2e/notebook_test.go +46 -0
@@ -6,6 +6,9 @@ import (
66 "path/filepath"77 "strings"88 "testing"9+ "unicode/utf8"10+11+ ghostty "go.mitchellh.com/libghostty"912 )1013 1114 // Notebooks: cells written and run with the real nu, one reading
@@ -101,6 +104,32 @@ func TestNotebookHighlightsAndCompletesWithNu(t *testing.T) {
101104 s.waitFor("sort-by size --reverse")102105 }103106 107+// With the real nu, the caret resting on a command says its signature108+// on the context line, and on a flag what the flag does; F1 opens the109+// command's help, with its page in nushell's docs as a link. On a110+// cell's $name, the line says the cell and its output's shape.111+func TestNotebookHoverWithNu(t *testing.T) {112+ needNu(t)113+ s := startWith(t, options{cols: 120, rows: 30, startsOn: "EDIT"}, "nu")114+ s.keys("n = [[k v]; [1 a] [2 b] [3 c]]", "<shift+enter>")115+ s.waitFor("Out[1]:")116+ s.keys("<enter>", "$n | sort-by k --reverse", "<home>")117+ s.waitFor("$n: table, 3 rows × 2 columns from cell 1")118+ s.keys("<right>", "<right>", "<right>", "<right>", "<right>", "<right>")119+ s.waitFor("sort-by <...comparator: cell-path|closure>")120+ s.waitFor("Sort by the given cell path or closure.")121+ s.keys("<end>")122+ s.waitFor("sort-by --reverse, -r")123+ s.keys("<f1>")124+ s.waitFor("Docs: https://www.nushell.sh/commands/docs/sort-by.html")125+ s.waitFor("HELP")126+ if got := s.linkAt("https://www.nushell.sh"); got != "https://www.nushell.sh/commands/docs/sort-by.html" {127+ t.Errorf("the docs link to %q", got)128+ }129+ s.keys("<esc>")130+ s.waitFor("EDIT")131+}132+104133 // Data > Shell from a workbook makes a Notebook tab; $selection is105134 // the range selected on the sheet shown before it.106135 func TestNotebookReadsSelection(t *testing.T) {
@@ -143,3 +172,20 @@ func TestNotebookLongPipeline(t *testing.T) {
143172 t.Errorf("the pipeline isn't whole after editing:\n%s", s.screen())144173 }145174 }175+176+// linkAt is the hyperlink under the first cell of text on the screen.177+func (s *session) linkAt(text string) string {178+ for y, line := range strings.Split(s.screen(), "\n") {179+ if i := strings.Index(line, text); i >= 0 {180+ s.mu.Lock()181+ defer s.mu.Unlock()182+ ref, err := s.vt.GridRef(ghostty.Point{Tag: ghostty.PointTagActive, X: uint16(utf8.RuneCountInString(line[:i])), Y: uint32(y)})183+ if err != nil {184+ return ""185+ }186+ u, _ := ref.HyperlinkURI()187+ return u188+ }189+ }190+ return ""191+}
M e2e/screennotebook_test.go +14 -0
M e2e/stills_test.go +2 -0
A e2e/testdata/nu/ide/hover.ast.json +1 -0
A e2e/testdata/nu/ide/hover.check.jsonl +3 -0
A e2e/testdata/nu/ide/hover.hover-73.json +1 -0
A e2e/testdata/nu/ide/hover.nu +4 -0
M e2e/testdata/nu/nu +8 -2
A e2e/testdata/screens/docs-notebook-nu-help-light.html +24 -0
@@ -0,0 +1,24 @@
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">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐</span><span style="color:var(--vt-palette-0)">e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">Docs:</span> <span style="color:var(--vt-palette-4);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</span> <span style="color:var(--vt-palette-8)">│</span>9+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-4)">sort-by</span> {<span style="color:var(--vt-palette-4)">flags</span>} <span style="font-weight:bold">&lt;</span>...comparator&gt; <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</span> <span style="color:var(--vt-palette-8)">│</span>19+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof&lt;cell-path,</span> <span style="font-weight:bold">closure&gt;</span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-19</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">42</span> <span style="color:var(--vt-palette-8)">─┘</span> <span style="color:var(--vt-palette-2)">▶</span>23+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>24+Sort by the given cell path or closure. <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
A e2e/testdata/screens/docs-notebook-nu-help.html +24 -0
@@ -0,0 +1,24 @@
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">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Docs:</span> <span style="color:var(--vt-palette-12);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</span> <span style="color:var(--vt-palette-8)">│</span>9+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-6)">sort-by</span> {<span style="color:var(--vt-palette-6)">flags</span>} <span style="font-weight:bold">&lt;</span>...comparator&gt; <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</span> <span style="color:var(--vt-palette-8)">│</span>19+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof&lt;cell-path,</span> <span style="font-weight:bold">closure&gt;</span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-19</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">42</span> <span style="color:var(--vt-palette-8)">─┘</span> <span style="color:var(--vt-palette-2)">▶</span>23+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>24+Sort by the given cell path or closure. <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/docs-notebook-nu-hover-light.html +16 -0
@@ -0,0 +1,16 @@
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">EDIT</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)">idle</span>3+ sort-by &lt;...comparator: cell-path|closure&gt; … <span style="color:var(--vt-palette-0)">Sort</span> <span style="color:var(--vt-palette-0)">by</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">given</span> <span style="color:var(--vt-palette-0)">cell</span> <span style="color:var(--vt-palette-0)">path</span> <span style="color:var(--vt-palette-0)">or</span> <span style="color:var(--vt-palette-0)">closure.</span> <span style="color:var(--vt-palette-8)">──</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F1</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">help</span>4+5+ <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>6+ <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>7+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>8+ <span style="color:var(--vt-palette-0)">Out[2]:</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">name</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">type</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">size</span><span style="background:var(--vt-palette-7)"> </span>9+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> 012 file 4.2 kB10+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> README.md file 9.8 kB11+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> demos dir 3.1 kB12+13+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>14+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-0)">[</span> <span style="color:var(--vt-palette-0)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>15+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>16+<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/docs-notebook-nu-hover.html +16 -0
@@ -0,0 +1,16 @@
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">EDIT</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)">idle</span>3+ sort-by &lt;...comparator: cell-path|closure&gt; … <span style="color:var(--vt-palette-8)">Sort</span> <span style="color:var(--vt-palette-8)">by</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">given</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="color:var(--vt-palette-8)">path</span> <span style="color:var(--vt-palette-8)">or</span> <span style="color:var(--vt-palette-8)">closure.</span> <span style="color: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)">F1</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">help</span>4+5+ <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>6+ <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>7+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>8+ <span style="color:var(--vt-palette-8)">Out[2]:</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">name</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">type</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">size</span><span style="background:var(--vt-palette-8)"> </span>9+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> 012 file 4.2 kB10+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> README.md file 9.8 kB11+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> demos dir 3.1 kB12+13+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>14+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">[</span> <span style="color:var(--vt-palette-8)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>15+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>16+<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/docs-palette-light.html +1 -1
@@ -14,5 +14,5 @@
1414 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as HTML <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">231</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">232</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 Start a new, empty sheet <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)">move</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Enter</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">run</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/docs-palette.html +1 -1
@@ -14,5 +14,5 @@
1414 <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-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-8)">│</span> Download as HTML <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>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 Start a new, empty sheet <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/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)">132</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)">133</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)">132</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)">133</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> Date <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Shift+3</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> 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>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> 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>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>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)">67</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)">132</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)">133</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-nu-help-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">HELP</span><span style="background:#40e0ff"> </span>2+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▶</span><span style="color:#757575;background:#000000">┌─</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">sort-by</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">────────────────────────────────────────────────────────────────────────────────────┐</span><span style="color:#959595;background:#000000">e</span><span style="background:#000000"> </span>3+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>4+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">by</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">given</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cell</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">path</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">or</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">closure.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>5+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>6+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Docs:</span><span style="background:#000000"> </span><span style="color:#a0c0ff;background:#000000;text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><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:#757575;background:#000000">│</span><span style="background:#000000"> </span>8+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Usage</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><span style="color:#40e0ff;background:#000000">sort-by</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">{</span><span style="color:#40e0ff;background:#000000">flags</span><span style="color:#ffffff;background:#000000">}</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">&lt;</span><span style="color:#ffffff;background:#000000">...comparator&gt;</span><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:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>11+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Flags</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>12+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-r,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--reverse</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</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">reverse</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">order.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>13+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-i,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--ignore-case</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">string-based</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">data</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">case-insensitively.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>14+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-n,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--natural</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">alphanumeric</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">string-based</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">data</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">naturally</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">(1,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">9,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">99,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">100,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">...).</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>15+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-c,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--custom</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Use</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">closures</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">specify</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">a</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">custom</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">order,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">rather</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">than</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">compute</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">a</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>16+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">comparison</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">key.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><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:#757575;background:#000000">│</span><span style="background:#000000"> </span>18+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Parameters</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><span style="color:#ffffff;background:#000000;font-weight:bold">...comparator:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">oneof&lt;cell-path,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">closure&gt;</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">cell</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">path(s)</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">or</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">closure(s)</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">compare</span><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:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">elements</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">by.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>21+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>22+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Input</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">and</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">output</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>23+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">list&lt;any&gt;</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">|</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">list&lt;any&gt;</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>24+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">record</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">|</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">table</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>25+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">table</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">|</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">table</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>26+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">▶</span>27+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Examples</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>28+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">└─────────────────────────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">1-25</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">of</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">42</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">─┘</span><span style="background:#000000"> </span>29+<span style="background:#000000"> </span>30+<span style="color:#ffffff;background:#1f1f1f">Sort</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">by</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">the</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">given</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">cell</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">path</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">or</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">closure.</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Up/Down</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">scroll</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Esc</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/notebook-nu-help-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">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐</span><span style="color:var(--vt-palette-0)">e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">Docs:</span> <span style="color:var(--vt-palette-4);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</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> <span style="color:var(--vt-palette-4)">sort-by</span> {<span style="color:var(--vt-palette-4)">flags</span>} <span style="font-weight:bold">&lt;</span>...comparator&gt; <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</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> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof&lt;cell-path,</span> <span style="font-weight:bold">closure&gt;</span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Input</span> <span style="font-weight:bold">and</span> <span style="font-weight:bold">output</span> <span style="color:var(--vt-palette-8)">│</span>23+ <span style="color:var(--vt-palette-8)">│</span> list&lt;any&gt; | list&lt;any&gt; <span style="color:var(--vt-palette-8)">│</span>24+ <span style="color:var(--vt-palette-8)">│</span> record | table <span style="color:var(--vt-palette-8)">│</span>25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> table | table <span style="color:var(--vt-palette-8)">│</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Examples</span> <span style="color:var(--vt-palette-8)">│</span>28+ <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)">42</span> <span style="color:var(--vt-palette-8)">─┘</span>29+30+Sort by the given cell path or closure. <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
A e2e/testdata/screens/notebook-nu-help.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">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Docs:</span> <span style="color:var(--vt-palette-12);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</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> <span style="color:var(--vt-palette-6)">sort-by</span> {<span style="color:var(--vt-palette-6)">flags</span>} <span style="font-weight:bold">&lt;</span>...comparator&gt; <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</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> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof&lt;cell-path,</span> <span style="font-weight:bold">closure&gt;</span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Input</span> <span style="font-weight:bold">and</span> <span style="font-weight:bold">output</span> <span style="color:var(--vt-palette-8)">│</span>23+ <span style="color:var(--vt-palette-8)">│</span> list&lt;any&gt; | list&lt;any&gt; <span style="color:var(--vt-palette-8)">│</span>24+ <span style="color:var(--vt-palette-8)">│</span> record | table <span style="color:var(--vt-palette-8)">│</span>25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> table | table <span style="color:var(--vt-palette-8)">│</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Examples</span> <span style="color:var(--vt-palette-8)">│</span>28+ <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)">42</span> <span style="color:var(--vt-palette-8)">─┘</span>29+30+Sort by the given cell path or closure. <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-nu-hover-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">EDIT</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">idle</span><span style="background:#000000"> </span>3+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">sort-by</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">&lt;...comparator:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cell-path|closure&gt;</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">…</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">by</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">the</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">given</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">path</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">or</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">closure.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">──</span><span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">F1</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">help</span><span style="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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">name</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">type</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">size</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span>11+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">2</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">3</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">4</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">5</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">name</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">type</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">size</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span>21+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">2</span><span style="background:#3a3a3a"> </span><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="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">3</span><span style="background:#3a3a3a"> </span><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:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span><span style="background:#000000"> </span>26+<span style="color:#40e040;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">]:</span><span style="background:#000000"> </span><span style="color:#40e040;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">sort-by</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">size</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">--reverse</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┃</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">▶</span>27+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span><span style="background:#000000"> </span>28+<span style="background:#000000"> </span>29+<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-nu-hover-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">EDIT</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)">idle</span>3+ sort-by &lt;...comparator: cell-path|closure&gt; … <span style="color:var(--vt-palette-0)">Sort</span> <span style="color:var(--vt-palette-0)">by</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">given</span> <span style="color:var(--vt-palette-0)">cell</span> <span style="color:var(--vt-palette-0)">path</span> <span style="color:var(--vt-palette-0)">or</span> <span style="color:var(--vt-palette-0)">closure.</span> <span style="color:var(--vt-palette-8)">──</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F1</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">help</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="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">name</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">type</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">size</span><span style="background:var(--vt-palette-7)"> </span>11+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> 012 file 4.2 kB12+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> CLAUDE.md file 1.0 kB13+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> README.md file 9.8 kB14+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> demos dir 3.1 kB15+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> 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="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">name</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">type</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">size</span><span style="background:var(--vt-palette-7)"> </span>21+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> 012 file 4.2 kB22+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> README.md file 9.8 kB23+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> demos dir 3.1 kB24+25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-0)">[</span> <span style="color:var(--vt-palette-0)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>28+29+30+<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-nu-hover.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">EDIT</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)">idle</span>3+ sort-by &lt;...comparator: cell-path|closure&gt; … <span style="color:var(--vt-palette-8)">Sort</span> <span style="color:var(--vt-palette-8)">by</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">given</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="color:var(--vt-palette-8)">path</span> <span style="color:var(--vt-palette-8)">or</span> <span style="color:var(--vt-palette-8)">closure.</span> <span style="color: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)">F1</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">help</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="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">name</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">type</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">size</span><span style="background:var(--vt-palette-8)"> </span>11+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> 012 file 4.2 kB12+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> CLAUDE.md file 1.0 kB13+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> README.md file 9.8 kB14+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> demos dir 3.1 kB15+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> 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="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">name</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">type</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">size</span><span style="background:var(--vt-palette-8)"> </span>21+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> 012 file 4.2 kB22+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> README.md file 9.8 kB23+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> demos dir 3.1 kB24+25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">[</span> <span style="color:var(--vt-palette-8)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>28+29+30+<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)">Read</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">source</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-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>15-<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">68</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</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)">68</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</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)">231</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)">232</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)">231</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)">232</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> Link a <span style="color:var(--vt-palette-3);font-weight:bold">s</span>ourc<span style="color:var(--vt-palette-3);font-weight:bold">e</span> <span style="color:var(--vt-palette-8)">Data</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-3);font-weight:bold">L</span><span style="color:var(--vt-palette-8)">inked</span> <span style="color:var(--vt-palette-8)">file</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)">63</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</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)">63</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</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> Download as HTML <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>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)">231</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</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)">232</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</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/nushell/hover.go +285 -0
@@ -0,0 +1,285 @@
1+package nushell2+3+import (4+ "cmp"5+ "context"6+ "encoding/json"7+ "fmt"8+ "strconv"9+ "strings"10+)11+12+// What nu says of the word at a byte of a script, through13+//14+// nu --ide-hover 7 file {"hover":"Sort by the given cell path or closure.\n### Usage\n...","span":{"start":5,"end":12}}15+//16+// A command's hover is its help in Markdown (Help reads it), a17+// variable's is its type ("int"), a flag's is just "flag", and a byte18+// on no word prints nothing. nu --lsp answers the same through the19+// language server's protocol; a process per question, as the other20+// questions are, needs no session with it.21+22+// Hover is nu's text about the word at a byte, and the word's bytes.23+type Hover struct {24+ From, To int25+ Text string26+}27+28+// HoverAt is what nu says of the word at offset, a byte of script;29+// false when it's on none.30+func HoverAt(ctx context.Context, r Runner, script string, offset int) (Hover, bool, error) {31+ out, err := ask(ctx, r, script, "--ide-hover", strconv.Itoa(offset))32+ if err != nil {33+ return Hover{}, false, err34+ }35+ return ParseHover(out)36+}37+38+// ParseHover reads what --ide-hover printed.39+func ParseHover(out []byte) (Hover, bool, error) {40+ if len(out) == 0 {41+ return Hover{}, false, nil42+ }43+ var h struct {44+ Hover *string45+ Span *span46+ }47+ if err := json.Unmarshal(out, &h); err != nil || h.Hover == nil || h.Span == nil {48+ return Hover{}, false, fmt.Errorf("%w: %s", ErrOld, firstLine(out))49+ }50+ return Hover{From: h.Span.Start, To: h.Span.End, Text: *h.Hover}, true, nil51+}52+53+// Help is a command's help, as its hover has it.54+type Help struct {55+ Name string // sort-by, str join56+ Desc string // what it does, its paragraphs57+ Usage string // sort-by {flags} <...comparator>58+ // Flags are its flags but --help; Params its positional59+ // parameters, in order.60+ Flags []Flag61+ Params []Param62+ // Types are its input and output types, "table | table".63+ Types []string64+ Examples []Example65+}66+67+// Flag is a command's flag: -r, --reverse, the type of its value if it68+// takes one, and what it does.69+type Flag struct {70+ Short, Long, Arg, Desc string71+}72+73+// Param is a positional parameter: comparator, its type as nu writes it74+// (oneof<cell-path, closure>), and what it is.75+type Param struct {76+ Name, Type, Desc string77+}78+79+// Example is one of a command's examples: what it does, and the code.80+type Example struct {81+ Desc, Code string82+}83+84+// ParseHelp reads a command's hover; false when it isn't one (a85+// variable's type, a flag's "flag").86+func ParseHelp(md string) (Help, bool) {87+ desc, rest, ok := strings.Cut(md, "### Usage")88+ if !ok {89+ return Help{}, false90+ }91+ h := Help{Desc: strings.TrimSpace(desc)}92+ for _, sec := range strings.Split("### Usage"+rest, "\n### ") {93+ head, body, _ := strings.Cut(strings.TrimPrefix(sec, "### "), "\n")94+ switch strings.TrimSpace(head) {95+ case "Usage":96+ h.Usage = strings.TrimSpace(strings.Trim(strings.TrimSpace(body), "`"))97+ h.Name = commandName(h.Usage)98+ case "Flags":99+ h.Flags = parseFlags(body)100+ case "Parameters":101+ h.Params = parseParams(body)102+ case "Input/output types":103+ h.Types = fenced(body)104+ case "Example(s)":105+ h.Examples = parseExamples(body)106+ }107+ }108+ return h, h.Name != ""109+}110+111+// commandName is the words of usage before its flags and parameters.112+func commandName(usage string) string {113+ var words []string114+ for w := range strings.FieldsSeq(usage) {115+ if strings.ContainsAny(w[:1], "{<(.[") {116+ break117+ }118+ words = append(words, w)119+ }120+ return strings.Join(words, " ")121+}122+123+// fenced are the lines of body's first code fence, trimmed.124+func fenced(body string) []string {125+ var out []string126+ in := false127+ for line := range strings.SplitSeq(body, "\n") {128+ t := strings.TrimSpace(line)129+ switch {130+ case strings.HasPrefix(t, "```") && in:131+ return out132+ case strings.HasPrefix(t, "```"):133+ in = true134+ case in && t != "":135+ out = append(out, t)136+ }137+ }138+ return out139+}140+141+// ticked splits `-r`, `--reverse` `<int>` - Sort in reverse order.\142+// into its backquoted parts and what follows " - ".143+func ticked(line string) ([]string, string) {144+ line = strings.TrimSuffix(strings.TrimSpace(line), "\\")145+ var parts []string146+ for strings.HasPrefix(line, "`") {147+ end := strings.Index(line[1:], "`")148+ if end < 0 {149+ break150+ }151+ parts = append(parts, line[1:end+1])152+ line = strings.TrimLeft(line[end+2:], ", ")153+ }154+ return parts, strings.TrimSpace(strings.TrimPrefix(line, "- "))155+}156+157+func parseFlags(body string) []Flag {158+ var out []Flag159+ for line := range strings.SplitSeq(body, "\n") {160+ parts, desc := ticked(line)161+ var f Flag162+ for _, p := range parts {163+ switch {164+ case strings.HasPrefix(p, "--"):165+ f.Long = p166+ case strings.HasPrefix(p, "-"):167+ f.Short = p168+ case strings.HasPrefix(p, "<"):169+ f.Arg = strings.Trim(p, "<>")170+ }171+ }172+ if (f.Long != "" || f.Short != "") && f.Long != "--help" {173+ f.Desc = desc174+ out = append(out, f)175+ }176+ }177+ return out178+}179+180+func parseParams(body string) []Param {181+ var out []Param182+ for line := range strings.SplitSeq(body, "\n") {183+ parts, desc := ticked(line)184+ if len(parts) == 0 {185+ continue186+ }187+ name, typ, _ := strings.Cut(parts[0], ":")188+ out = append(out, Param{Name: strings.TrimSpace(name), Type: strings.TrimSpace(typ), Desc: desc})189+ }190+ return out191+}192+193+// parseExamples reads the examples: an empty fence, then each194+// example's description outside a fence and its code inside one.195+func parseExamples(body string) []Example {196+ segs := strings.Split(body, "```")197+ var out []Example198+ for i := 2; i+1 < len(segs); i += 2 {199+ desc := strings.TrimSpace(segs[i])200+ var code []string201+ for line := range strings.SplitSeq(strings.Trim(segs[i+1], "\n"), "\n") {202+ code = append(code, strings.TrimPrefix(line, " "))203+ }204+ c := strings.TrimSpace(strings.Join(code, "\n"))205+ if desc != "" || c != "" {206+ out = append(out, Example{Desc: desc, Code: c})207+ }208+ }209+ return out210+}211+212+// Summary is the first paragraph of the description.213+func (h Help) Summary() string {214+ first, _, _ := strings.Cut(h.Desc, "\n\n")215+ return strings.Join(strings.Fields(first), " ")216+}217+218+// Signature is the command's usage with its parameters' types and its219+// flags by name: sort-by <...comparator: cell-path|closure> --reverse.220+func (h Help) Signature() string {221+ sig := []string{h.Name}222+ rest := strings.TrimSpace(strings.TrimPrefix(h.Usage, h.Name))223+ for w := range strings.FieldsSeq(rest) {224+ if w == "{flags}" {225+ continue226+ }227+ if p, ok := h.param(w); ok && p.Type != "" {228+ w = w[:len(w)-1] + ": " + typeWords(p.Type) + w[len(w)-1:]229+ }230+ sig = append(sig, w)231+ }232+ for _, f := range h.Flags {233+ name := cmp.Or(f.Long, f.Short)234+ if f.Arg != "" {235+ name += " <" + f.Arg + ">"236+ }237+ sig = append(sig, name)238+ }239+ return strings.Join(sig, " ")240+}241+242+// Brief is the signature where there's no room for it: the usage243+// without types, and … for its flags: sort-by <...comparator> ….244+func (h Help) Brief() string {245+ brief := strings.Join(strings.Fields(strings.Replace(h.Usage, "{flags}", "", 1)), " ")246+ if len(h.Flags) > 0 {247+ brief += " …"248+ }249+ return brief250+}251+252+// param is the parameter usage's word w names: <...comparator>,253+// <rows?>.254+func (h Help) param(w string) (Param, bool) {255+ name := strings.Trim(w, "<>()[]?.")256+ for _, p := range h.Params {257+ if strings.Trim(p.Name, ".?") == name {258+ return p, true259+ }260+ }261+ return Param{}, false262+}263+264+// typeWords writes oneof<int, filesize> as int|filesize.265+func typeWords(t string) string {266+ if in, ok := strings.CutPrefix(t, "oneof<"); ok {267+ return strings.ReplaceAll(strings.TrimSuffix(in, ">"), ", ", "|")268+ }269+ return t270+}271+272+// Flag is the command's flag named w, long (--reverse) or short (-r).273+func (h Help) Flag(w string) (Flag, bool) {274+ for _, f := range h.Flags {275+ if w != "" && (f.Long == w || f.Short == w) {276+ return f, true277+ }278+ }279+ return Flag{}, false280+}281+282+// DocsURL is the page of nushell's documentation for command name.283+func DocsURL(name string) string {284+ return "https://www.nushell.sh/commands/docs/" + strings.ReplaceAll(name, " ", "_") + ".html"285+}
A internal/nushell/hover_test.go +97 -0
@@ -0,0 +1,97 @@
1+package nushell2+3+import (4+ "context"5+ "errors"6+ "os/exec"7+ "slices"8+ "strings"9+ "testing"10+)11+12+// hoverAsked are the bytes of testdata/ide/hover.nu that hover-*.json13+// record nu's hover at.14+var hoverAsked = map[string]int{"sort-by": 17, "flag": 30, "into-string": 45, "variable": 76, "none": 15}15+16+func TestParseHover(t *testing.T) {17+ src := string(recorded(t, "hover.nu"))18+ for name, want := range map[string]string{"sort-by": "sort-by", "flag": "--reverse", "into-string": "into string", "variable": "$n"} {19+ h, ok, err := ParseHover(recorded(t, "hover-"+name+".json"))20+ if err != nil || !ok || src[h.From:h.To] != want {21+ t.Errorf("%s: %+v %v %v", name, h, ok, err)22+ }23+ }24+ if h, ok, err := ParseHover(recorded(t, "hover-variable.json")); !ok || err != nil || h.Text != "int" {25+ t.Errorf("a variable's type: %q", h.Text)26+ }27+ if _, ok, err := ParseHover(recorded(t, "hover-none.json")); ok || err != nil {28+ t.Errorf("no word: %v %v", ok, err)29+ }30+ if _, _, err := ParseHover([]byte(`{"other": 1}`)); !errors.Is(err, ErrOld) {31+ t.Errorf("unreadable answer: %v", err)32+ }33+}34+35+func TestParseHelp(t *testing.T) {36+ h, _, _ := ParseHover(recorded(t, "hover-sort-by.json"))37+ help, ok := ParseHelp(h.Text)38+ if !ok || help.Name != "sort-by" || help.Summary() != "Sort by the given cell path or closure." {39+ t.Fatalf("help %+v", help)40+ }41+ if got := help.Signature(); got != "sort-by <...comparator: cell-path|closure> --reverse --ignore-case --natural --custom" {42+ t.Errorf("signature %q", got)43+ }44+ if got := help.Brief(); got != "sort-by <...comparator> …" {45+ t.Errorf("brief %q", got)46+ }47+ if f, ok := help.Flag("-r"); !ok || f.Long != "--reverse" || f.Desc != "Sort in reverse order." {48+ t.Errorf("-r: %+v", f)49+ }50+ if _, ok := help.Flag("--help"); ok {51+ t.Error("--help is every command's")52+ }53+ if !slices.Equal(help.Types, []string{"list<any> | list<any>", "record | table", "table | table"}) {54+ t.Errorf("types %q", help.Types)55+ }56+ if len(help.Examples) != 6 || help.Examples[0] != (Example{"Sort files by modified date.", "ls | sort-by modified"}) {57+ t.Errorf("examples %q", help.Examples)58+ }59+ h, _, _ = ParseHover(recorded(t, "hover-into-string.json"))60+ help, _ = ParseHelp(h.Text)61+ if got := help.Signature(); got != "into string <...rest: cell-path> --group-digits --decimals <int>" {62+ t.Errorf("signature %q", got)63+ }64+ if help.Name != "into string" || DocsURL(help.Name) != "https://www.nushell.sh/commands/docs/into_string.html" {65+ t.Errorf("docs of %q: %s", help.Name, DocsURL(help.Name))66+ }67+ if _, ok := ParseHelp("int"); ok {68+ t.Error("a variable's type read as help")69+ }70+}71+72+// A command of the script's own, without a description.73+func TestParseHelpOfADef(t *testing.T) {74+ help, ok := ParseHelp("\n### Usage\n```\n greet {flags} <name>\n```\n\n### Flags\n\n `--loud`\\\n `-h`, `--help` - Display the help message for this command\n\n### Parameters\n\n `name: string`\n\n")75+ if !ok || help.Desc != "" || help.Signature() != "greet <name: string> --loud" {76+ t.Errorf("help %+v %q", help, help.Signature())77+ }78+}79+80+// The real nu, when it's the version recorded, says what hover-*.json81+// record.82+func TestNuHover(t *testing.T) {83+ if _, err := exec.LookPath("nu"); err != nil {84+ t.Skip("nu isn't installed")85+ }86+ if v, err := Version(context.Background(), Nu{}); err != nil || v != strings.TrimSpace(string(recorded(t, "version.txt"))) {87+ t.Skipf("nu %s %v isn't the version recorded", v, err)88+ }89+ src := string(recorded(t, "hover.nu"))90+ for name, at := range hoverAsked {91+ got, gotOK, err := HoverAt(context.Background(), Nu{}, src, at)92+ want, wantOK, _ := ParseHover(recorded(t, "hover-"+name+".json"))93+ if err != nil || got != want || gotOK != wantOK {94+ t.Errorf("%s: nu says %+v %v %v, recorded %+v", name, got, gotOK, err, want)95+ }96+ }97+}
A internal/nushell/testdata/ide/hover-flag.json +1 -0
A internal/nushell/testdata/ide/hover-into-string.json +1 -0
@@ -0,0 +1 @@
1+{"hover":"Convert value to a string.\n### Usage\n```\n into string {flags} <...rest>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\\\n `-g`, `--group-digits` - Group digits together by the locale specific thousands separator.\\\n `-d`, `--decimals` `<int>` - Decimal digits to which to round.\n\n### Parameters\n\n `...rest: cell-path` - For a data structure input, convert data at the given cell paths.\n\n\n### Input/output types\n\n```\n binary | string\n int | string\n number | string\n string | string\n glob | string\n bool | string\n filesize | string\n datetime | string\n duration | string\n cell-path | string\n range | string\n semver | string\n semver-range | string\n list<any> | list<string>\n table | table\n record | record\n\n```\n### Example(s)\n```\n```\n convert int to string and append three decimal places.\n```\n 5 | into string --decimals 3\n\n```\n convert float to string and round to nearest integer.\n```\n 1.7 | into string --decimals 0\n\n```\n convert float to string.\n```\n 1.7 | into string --decimals 1\n\n```\n convert float to string and limit to 2 decimals.\n```\n 1.734 | into string --decimals 2\n\n```\n convert float to string.\n```\n 4.3 | into string\n\n```\n convert string to string.\n```\n '1234' | into string\n\n```\n convert boolean to string.\n```\n true | into string\n\n```\n convert date to string.\n```\n '2020-10-10 10:00:00 +02:00' | into datetime | into string\n\n```\n convert filepath to string.\n```\n ls Cargo.toml | get name | into string\n\n```\n convert filesize to string.\n```\n 1kB | into string\n\n```\n convert duration to string.\n```\n 9day | into string\n\n```\n convert cell-path to string.\n```\n $.name | into string\n\n```\n convert semver to string.\n```\n '1.2.3' | into semver | into string\n\n","span":{"start":41,"end":52}}
A internal/nushell/testdata/ide/hover-none.json +0 -0

File mode 100644

A internal/nushell/testdata/ide/hover-sort-by.json +1 -0
A internal/nushell/testdata/ide/hover-variable.json +1 -0
A internal/nushell/testdata/ide/hover.nu +2 -0
M internal/sheet/merge.go +4 -5
M internal/sheet/merge_undo_test.go +18 -0
M internal/sheet/randedit_test.go +2 -2
M internal/sheet/regionclash.go +56 -0
@@ -132,3 +132,59 @@ func (s *Sheet) wakeAllRegions() {
132132 }133133 }134134 }135+136+// table is the cells the region with meta me takes: its table shown or,137+// blocked, the one it needs; none before its rows first arrive.138+func (me *regionMeta) table() (Rect, bool) {139+ switch {140+ case me == nil || !me.has:141+ return Rect{}, false142+ case me.why != "":143+ return me.need, true144+ }145+ return me.written, true146+}147+148+// regionMoved returns the anchors of the arrays over the region's149+// table, before and after it changed, spilling or blocked by a cycle,150+// to compute again: an array reading the table's cells is blocked by151+// it (spillsIntoItself) wherever the table goes, which the cells it152+// reads may not tell it.153+func (s *Sheet) regionMoved(before, after Rect, had bool) []loc {154+ var out []loc155+ for b, sp := range s.spills {156+ if (sp.why == "" || sp.circular) && (had && overlaps(sp.area, before) || overlaps(sp.area, after)) {157+ sp.stale = true158+ out = append(out, loc{s, b})159+ }160+ }161+ return out162+}163+164+// heldByArrays is the region r's table, when an array spilling over it165+// decides whether it shows: shown, or blocked by arrays alone, which it166+// would show without.167+func (s *Sheet) heldByArrays(r Region) (Rect, bool) {168+ me := s.regionMeta[nameKey(r.Name)]169+ t, ok := me.table()170+ if !ok || me.why == "" {171+ return t, ok172+ }173+ if t.To.Row >= MaxRows || t.To.Col >= MaxCols {174+ return Rect{}, false175+ }176+ for a := range s.cells.anyKeysIn(t) {177+ if a == r.At || !s.cells.filledAt(a) {178+ continue179+ }180+ if _, spilled := s.SpillAnchor(a); !spilled || s.cells.derivedAt(a) != slotSpill {181+ return Rect{}, false182+ }183+ }184+ for _, q := range s.regions.list {185+ if q.Name != r.Name && t.Contains(q.At) {186+ return Rect{}, false187+ }188+ }189+ return t, true190+}
M internal/sheet/regionclash_test.go +44 -0
@@ -101,3 +101,47 @@ func TestPasteIntoARegionsBlankCell(t *testing.T) {
101101 apply(t, s, op)102102 wantShown(t, s, map[string]string{"A1": "#REF!", "A2": "", "B2": "9"})103103 }104+105+// An array that would spill over a region's table whose cells it reads106+// is a circular dependency: spilling, it would block the region and107+// read its #REF!, come out smaller and free it. It shows #REF! and the108+// region its table, whichever came first, and opened again.109+func TestArrayOverRegionItReadsTheCellsOf(t *testing.T) {110+ op := LiveOp{Region: "a", Reset: true, Header: liveRow("x", "y"), Rows: []LiveRow{liveRow("1", "2")}}111+ feeds := map[string]LiveOp{"a": op}112+ want := map[string]string{"I9": "#REF!", "H10": "x", "I11": "2"}113+ for _, arrayFirst := range []bool{false, true} {114+ s := New()115+ sent(t, s, "a", "H10")116+ if !arrayFirst {117+ apply(t, s, op)118+ }119+ s.Set(at("I9"), "=SORT(H9:H11)")120+ if arrayFirst {121+ apply(t, s, op)122+ }123+ feedStale(s.Book(), feeds)124+ if stale := s.Book().StaleOutputs(); len(stale) > 0 {125+ t.Errorf("array first %v: still stale %v", arrayFirst, stale)126+ }127+ wantShown(t, s, want)128+ opened := roundTrip(t, s)129+ feedAll(opened.Book(), feeds)130+ feedStale(opened.Book(), feeds)131+ wantShown(t, opened, want)132+ }133+}134+135+// A region blocked by a formula and an array reading its cells, the formula136+// cleared, is blocked by the array alone, which then finds the cycle.137+func TestRegionFreedButForAnArrayReadingIt(t *testing.T) {138+ op := LiveOp{Region: "a", Reset: true, Header: liveRow("x", "y", "z"), Rows: []LiveRow{liveRow("1", "2", "3")}}139+ s := New()140+ sent(t, s, "a", "F10")141+ s.Set(at("F11"), "=SEQUENCE(1)")142+ s.Set(at("H9"), "=SORT(F6:F10)")143+ apply(t, s, op)144+ s.Set(at("F11"), "")145+ feedStale(s.Book(), map[string]LiveOp{"a": op})146+ wantShown(t, s, map[string]string{"H9": "#REF!", "F10": "x", "H11": "3"})147+}
M internal/sheet/regionwrite.go +12 -0
@@ -76,6 +76,9 @@ func (s *Sheet) writeRegions() []loc {
7676 for k, me := range s.regionMeta {7777 if s.regionIndex(k) < 0 {7878 changed = append(changed, s.clearOwned(me, noRect)...)79+ if t, ok := me.table(); ok {80+ changed = append(changed, s.regionMoved(t, noRect, true)...)81+ }7982 delete(s.regionMeta, k)8083 s.wakeAllRegions()8184 for u := range s.wb.nameUsers[nameKey(regionPrefix+k)] {
@@ -97,6 +100,9 @@ func (s *Sheet) writeRegions() []loc {
97100 // naming it read #REF! until then, so they're among the cells returned.98101 func (s *Sheet) emptyRegion(r Region, me *regionMeta) []loc {99102 changed := s.clearOwned(me, noRect)103+ if t, ok := me.table(); ok {104+ changed = append(changed, s.regionMoved(t, noRect, true)...)105+ }100106 if me.has {101107 changed = s.regionReaders(r, changed)102108 }
@@ -115,6 +121,7 @@ func (s *Sheet) emptyRegion(r Region, me *regionMeta) []loc {
115121 func (s *Sheet) writeTable(r Region, me *regionMeta, header LiveRow, rows []LiveRow, reset bool) []loc {116122 cols, nrows := tableSize(me, header, rows, reset)117123 was, had, shape := me.written, me.has, [2]int{me.rows, me.cols}124+ before, _ := me.table()118125 need := s.tableArea(r, cols, nrows)119126 changed := s.makeRoomAt(r)120127 whole := me.why != "" || me.placed // it may not hold every cell it wrote
@@ -160,6 +167,11 @@ func (s *Sheet) writeTable(r Region, me *regionMeta, header LiveRow, rows []Live
160167 for _, c := range changed {161168 s.freedFor(c.a)162169 }170+ // Blocked, what's in its way may have gone but for arrays, which then171+ // find it (heldByArrays).172+ if after, _ := me.table(); !had || after != before || me.why != "" {173+ changed = append(changed, s.regionMoved(before, after, had)...)174+ }163175 if !had || was != me.written || shape != [2]int{me.rows, me.cols} { // moved, or its table changed shape164176 changed = s.regionReaders(r, changed)165177 }
M internal/sheet/spill.go +6 -3
M internal/sheet/spillblock.go +50 -5
@@ -19,7 +19,13 @@ import "slices"
1919 // (regionsGiveWay), so what reads it by name reads #REF! once the array2020 // spills, whatever the array holds: the walk doesn't follow it, or the2121 // array would be blocked or not by whether the region's rows arrived22-// before it spilled.22+// before it spilled. A formula reading the table's cells, though, leads23+// the walk from each of them, those outside area too: spilling empties24+// them, the anchor showing #REF!, which may make the array smaller and25+// free the region, spilling again once it's sent its rows, without26+// end. The table is the one shown or, blocked by arrays alone, the one27+// it needs, so the array is blocked either way (regionMoved has it28+// checked again).2329 func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {2430 if area.From == area.To {2531 return "", nil
@@ -30,13 +36,18 @@ func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {
3036 if w.cells(s, area, a, loc{}, true) {3137 return circular(w.origin), nil3238 }39+ for _, r := range s.regions.list {40+ if t, ok := s.heldByArrays(r); ok && overlaps(t, area) && w.cells(s, t, a, loc{}, true) {41+ return "Circular dependency: the array would spill over " + r.Name + "'s table, whose cells its formula reads", nil42+ }43+ }3344 for i := 0; i < len(w.queue); i++ {3445 u := w.queue[i]3546 w.origin, w.parent = w.nodes[i].from, i3647 // An array's cells first, so a cycle through them is found as3748 // one, and the array blocked with it, rather than through the3849 // anchor alone.39- if sp := u.s.spills[u.a]; sp != nil && (sp.why == "" || sp.circular) {50+ if sp := u.s.spills[u.a]; sp != nil && sp.walked() {4051 w.cells(u.s, sp.area, u.a, u, false)4152 }4253 if !w.found {
@@ -101,6 +112,25 @@ func (w *Workbook) cycleMoved(circ bool, old *spill) bool {
101112 return false102113 }103114 115+// blockedMoved reports whether the arrays are to be checked again, an116+// array whose spill was old found blocked over area by something other117+// than a cycle: counted as spilling over the cells it needs (walked), it118+// may close or open a cycle through arrays that read them, when it's119+// blocked over other cells, or blocked still, the first time in the120+// recalculation: a formula on the way around a cycle through it may121+// have changed, which the others aren't computed again for, as in122+// cycleMoved.123+func (w *Workbook) blockedMoved(old *spill, area Rect) bool {124+ switch {125+ case old == nil || old.why == "" || old.area != area:126+ return true127+ case !w.spillRechecked:128+ w.spillRechecked = true129+ return true130+ }131+ return false132+}133+104134 // noteCycle counts the array at l found blocked by a cycle, from spilling105135 // or not, in the recalculation.106136 func (w *Workbook) noteCycle(l loc) {
@@ -230,13 +260,13 @@ func (w *Workbook) recheckCircular() []loc {
230260 }231261 232262 // forgetSpill drops the anchor at a's spill for good, its formula233-// computing one value or none, returning the cells that changed. When a234-// cycle blocked it, it counted as spilling over the cells it needed, so263+// computing one value or none, returning the cells that changed. When264+// blocked, it counted as spilling over the cells it needed (walked), so235265 // the arrays blocked by a cycle are checked again.236266 func (s *Sheet) forgetSpill(a Addr) []loc {237267 old := s.spills[a]238268 changed := s.dropSpill(a)239- if old != nil && old.circular {269+ if old != nil && old.why != "" {240270 changed = append(changed, s.wb.recheckCircular()...)241271 }242272 return changed
@@ -260,3 +290,18 @@ func blockCircular(arrays []loc) []loc {
260290 }261291 return changed262292 }293+294+// walked reports whether spillsIntoItself follows the cells of the295+// array: spilled, or blocked, over the cells it needs, what it would296+// spill over given room. A blocked array whose size follows what it297+// reads may be blocked or free by whether another spills into its298+// inputs, and that other by whether it spills: counted as spilling,299+// the two are blocked together, as when the cycle is found in one go,300+// rather than by the order they were computed in. The cells of an array301+// too big for max-cells aren't walked.302+func (sp *spill) walked() bool {303+ if sp.why == "" || sp.circular {304+ return true305+ }306+ return (sp.area.To.Row-sp.area.From.Row+1)*(sp.area.To.Col-sp.area.From.Col+1) <= MaxCells()307+}
M internal/sheet/spillcycle_test.go +36 -0
@@ -64,3 +64,39 @@ func TestCycleGoneWithAnArray(t *testing.T) {
6464 t.Errorf("C9 opened: %+v", sp)6565 }6666 }67+68+// An array spilling into the inputs of another, blocked by a value but69+// only as wide as it is for the first's spill, and reading what the70+// other needs: counted as spilling, the blocked one closes the cycle,71+// so both are blocked whichever was typed first, as when opened.72+func TestCycleThroughABlockedArray(t *testing.T) {73+ cells := map[string]string{"C9": "1.5", "C13": "=SORT(D19:E20)", "D18": "=SORT(C9:E13)", "E19": "=SEQUENCE(3)"}74+ want := map[string]string{"C13": "#REF!", "D18": "#REF!", "D13": "", "E20": "2"}75+ for _, order := range [][]string{{"C9", "E19", "C13", "D18"}, {"C9", "D18", "C13", "E19"}, {"D18", "C13", "C9", "E19"}} {76+ wantSettled(t, "blocked", order, cells, want)77+ }78+}79+80+// A formula typed last closing a cycle through an array blocked by a81+// value has the arrays checked again, as when the file opens.82+func TestCycleClosedThroughABlockedArray(t *testing.T) {83+ cells := map[string]string{"D9": "=SORT(G15:H19)", "E9": "=COUNTIF(A9:A12,\">2\")", "B16": "=ABS($A$9)+1", "H19": "=SORT(B13:D16)", "H16": "=COUNTIF(H20,\">2\")"}84+ wantSettled(t, "closed", []string{"D9", "E9", "B16", "H19", "H16"}, cells, map[string]string{"D9": "#REF!", "H19": "#REF!"})85+}86+87+// A blocked array that goes, its formula computing one value, frees the88+// arrays whose cycle went through the cells it needed (spill.walked):89+// once Total is defined, B7 reads its own #REF! through G9.90+func TestCycleGoneWithABlockedArray(t *testing.T) {91+ cells := map[string]string{92+ "H9": "=VLOOKUP(G8,G14:G15,1,FALSE)", "E7": "=SORT(F10:G14)", "G9": "=SUM(Total)*2", "B7": "=SORT(G7:H11)",93+ "H5": "=SORT(C4:E8)", "B9": "=VLOOKUP(A13,C14:D18,1,FALSE)", "G18": "=ABS(C14)+1",94+ }95+ s := New()96+ for _, a := range []string{"H9", "E7", "G9", "B7", "H5", "B9"} {97+ s.Set(at(a), cells[a])98+ }99+ s.Book().DefineName("Total", s, NewRect(Addr{}, Addr{Col: 1, Row: 9}))100+ s.Set(at("G18"), cells["G18"])101+ wantShown(t, s, map[string]string{"B7": "#REF!", "H5": ""})102+}
A internal/sheet/testdata/fuzz/FuzzRandomEdits/6452b8f0efea9063 +2 -0
A internal/sheet/testdata/fuzz/FuzzRandomEdits/8fbc50e2c14bfe4c +2 -0
A internal/sheet/testdata/fuzz/FuzzRandomEdits/a704f635622be956 +2 -0
A internal/ui/cmdhelp/cmdhelp.go +228 -0
@@ -0,0 +1,228 @@
1+// Package cmdhelp is a nushell command's help, F1 on a command in a2+// notebook's cell: a scrollable box over the notebook with what the3+// command does, a link to its page in nushell's docs, its usage, flags,4+// parameters, input and output types and examples, as nu's help has5+// them. It knows the UI only through Host.6+package cmdhelp7+8+import (9+ "strconv"10+ "strings"11+12+ tea "charm.land/bubbletea/v2"13+ "github.com/charmbracelet/x/ansi"14+15+ "github.com/FelineStateMachine/012/internal/nushell"16+ "github.com/FelineStateMachine/012/internal/ui/overlay"17+ "github.com/FelineStateMachine/012/internal/ui/theme"18+)19+20+// Host is what the help needs of the UI.21+type Host interface {22+ Theme() *theme.Theme23+ Size() (width, height int)24+ // Close closes the open overlay.25+ Close()26+ // Code draws a line of nushell highlighted.27+ Code(src string) string28+}29+30+// View is the open help.31+type View struct {32+ h Host33+ help nushell.Help34+ top int35+}36+37+// ID identifies the view's box in mouse events.38+const ID = "cmdhelp"39+40+// maxWidth is the widest the text runs, for reading.41+const maxWidth = 9242+43+// New returns the help of a command, scrolled to the top.44+func New(h Host, help nushell.Help) *View { return &View{h: h, help: help} }45+46+// Top is the first line shown.47+func (s *View) Top() int { return s.top }48+49+func (s *View) Indicator() string { return "HELP" }50+51+// width is the text's width inside the box.52+func (s *View) width() int {53+ w, _ := s.h.Size()54+ return max(min(w-6, maxWidth), 20)55+}56+57+// Lines are the help's lines at the box's width.58+func (s *View) Lines() []string {59+ th := s.h.Theme()60+ w := s.width()61+ hp := s.help62+ var out []string63+ wrap := func(text, indent string) {64+ for line := range strings.SplitSeq(ansi.Wordwrap(text, w-len(indent), ""), "\n") {65+ out = append(out, indent+line)66+ }67+ }68+ heading := func(title string) { out = append(out, "", th.Title.Render(title)) }69+ for _, para := range strings.Split(hp.Desc, "\n\n") {70+ if len(out) > 0 {71+ out = append(out, "")72+ }73+ wrap(strings.Join(strings.Fields(para), " "), "")74+ }75+ if hp.Desc != "" { // a command of the cell's own has no page76+ url := nushell.DocsURL(hp.Name)77+ out = append(out, "", th.Muted.Render("Docs: ")+th.Link.Hyperlink(url).Render(url))78+ }79+ heading("Usage")80+ out = append(out, " "+s.h.Code(hp.Usage))81+ if len(hp.Flags) > 0 {82+ heading("Flags")83+ out = append(out, s.table(flagRows(hp.Flags))...)84+ }85+ if len(hp.Params) > 0 {86+ heading("Parameters")87+ out = append(out, s.table(paramRows(hp.Params))...)88+ }89+ if len(hp.Types) > 0 {90+ heading("Input and output")91+ for _, t := range hp.Types {92+ out = append(out, " "+t)93+ }94+ }95+ if len(hp.Examples) > 0 {96+ heading("Examples")97+ for i, ex := range hp.Examples {98+ if i > 0 {99+ out = append(out, "")100+ }101+ wrap(ex.Desc, " ")102+ for line := range strings.SplitSeq(ex.Code, "\n") {103+ out = append(out, " "+s.h.Code(line))104+ }105+ }106+ }107+ return out108+}109+110+// flagRows are the flags as names and what they do: -r, --reverse.111+func flagRows(flags []nushell.Flag) [][2]string {112+ rows := make([][2]string, len(flags))113+ for i, f := range flags {114+ name := f.Long115+ if f.Short != "" {116+ name = strings.TrimSuffix(f.Short+", "+f.Long, ", ")117+ }118+ if f.Arg != "" {119+ name += " <" + f.Arg + ">"120+ }121+ rows[i] = [2]string{name, f.Desc}122+ }123+ return rows124+}125+126+func paramRows(params []nushell.Param) [][2]string {127+ rows := make([][2]string, len(params))128+ for i, p := range params {129+ rows[i] = [2]string{strings.TrimSuffix(p.Name+": "+p.Type, ": "), p.Desc}130+ }131+ return rows132+}133+134+// table draws names and what they are in two columns, the second135+// wrapped beside the first.136+func (s *View) table(rows [][2]string) []string {137+ th := s.h.Theme()138+ w := s.width()139+ nameW := 0140+ for _, r := range rows {141+ nameW = max(nameW, ansi.StringWidth(r[0]))142+ }143+ nameW = min(nameW, w/2)144+ var out []string145+ for _, r := range rows {146+ name := ansi.Truncate(r[0], nameW, "…")147+ lead := " " + th.Key.Render(name) + strings.Repeat(" ", nameW-ansi.StringWidth(name)+2)148+ pad := strings.Repeat(" ", nameW+4)149+ for i, line := range strings.Split(ansi.Wordwrap(r[1], max(w-nameW-4, 10), ""), "\n") {150+ if i == 0 {151+ out = append(out, lead+line)152+ } else {153+ out = append(out, pad+line)154+ }155+ }156+ }157+ return out158+}159+160+// visible is how many lines fit between the menu bar and the status161+// line.162+func (s *View) visible(total int) int {163+ _, height := s.h.Size()164+ return max(min(total, height-2-2-1), 1)165+}166+167+func (s *View) Layout() []overlay.Box {168+ th := s.h.Theme()169+ width, height := s.h.Size()170+ lines := append([]string{""}, s.Lines()...) // breathing room under the title171+ inner := s.width() + 2172+ n := s.visible(len(lines))173+ s.top = min(max(s.top, 0), max(len(lines)-n, 0))174+ rows := make([]string, n)175+ for i := range rows {176+ line := ""177+ if j := s.top + i; j < len(lines) {178+ line = lines[j]179+ }180+ rows[i] = theme.Cells(th.MenuBar, " "+line, inner)181+ }182+ footer := ""183+ if n < len(lines) {184+ footer = strconv.Itoa(s.top+1) + "-" + strconv.Itoa(s.top+n) + " of " + strconv.Itoa(len(lines))185+ }186+ b := th.Frame(inner, s.help.Name, footer, rows)187+ bw, bh := ansi.StringWidth(b[0]), len(b)188+ return []overlay.Box{{ID: ID, X: (width - bw) / 2, Y: max(overlay.MenuLine+1, (height-1-bh)/2), Lines: b}}189+}190+191+func (s *View) Key(k tea.KeyPressMsg) tea.Cmd {192+ total := len(s.Lines()) + 1193+ page := s.visible(total)194+ switch k.String() {195+ case "up", "k":196+ s.top--197+ case "down", "j":198+ s.top++199+ case "pgup":200+ s.top -= page201+ case "pgdown", "space":202+ s.top += page203+ case "home":204+ s.top = 0205+ case "end":206+ s.top = total207+ case "esc", "enter", "q", "f1":208+ s.h.Close()209+ }210+ s.top = min(max(s.top, 0), max(total-page, 0))211+ return nil212+}213+214+func (s *View) Mouse(e overlay.MouseEvent) tea.Cmd {215+ switch {216+ case e.Kind == overlay.MouseWheel && e.Button == tea.MouseWheelUp:217+ s.top = max(s.top-3, 0)218+ case e.Kind == overlay.MouseWheel && e.Button == tea.MouseWheelDown:219+ s.top += 3 // layout clamps220+ case e.Kind == overlay.MousePress && e.Box != ID:221+ s.h.Close()222+ }223+ return nil224+}225+226+func (s *View) Status() (string, string) {227+ return s.help.Summary(), s.h.Theme().KeyHints("Up/Down", "scroll", "Esc", "close")228+}
A internal/ui/cmdhelp/cmdhelp_test.go +73 -0
@@ -0,0 +1,73 @@
1+package cmdhelp2+3+import (4+ "strconv"5+ "strings"6+ "testing"7+8+ tea "charm.land/bubbletea/v2"9+ "github.com/charmbracelet/x/ansi"10+11+ "github.com/FelineStateMachine/012/internal/nushell"12+ "github.com/FelineStateMachine/012/internal/ui/theme"13+)14+15+type fakeHost struct {16+ th theme.Theme17+ w, h int18+ closed bool19+}20+21+func (f *fakeHost) Theme() *theme.Theme { return &f.th }22+func (f *fakeHost) Size() (int, int) { return f.w, f.h }23+func (f *fakeHost) Close() { f.closed = true }24+func (f *fakeHost) Code(src string) string { return src }25+26+var sortBy = nushell.Help{27+ Name: "sort-by", Desc: "Sort by the given cell path or closure.", Usage: "sort-by {flags} <...comparator>",28+ Flags: []nushell.Flag{{Short: "-r", Long: "--reverse", Desc: "Sort in reverse order."},29+ {Short: "-i", Long: "--ignore-case", Desc: "Sort string-based data case-insensitively."}},30+ Params: []nushell.Param{{Name: "...comparator", Type: "oneof<cell-path, closure>", Desc: "The cell path(s) or closure(s) to compare elements by."}},31+ Types: []string{"table | table"},32+ Examples: []nushell.Example{{Desc: "Sort files by modified date.", Code: "ls | sort-by modified"}, {Desc: "Reversed.", Code: "ls | sort-by name -r"}},33+}34+35+func boxText(v *View) []string {36+ lines := v.Layout()[0].Lines37+ out := make([]string, len(lines))38+ for i, l := range lines {39+ out[i] = ansi.Strip(l)40+ }41+ return out42+}43+44+// The help says what the command does, links its docs, and lists its45+// usage, flags, parameters, types and examples, scrolling in a box46+// that fits the screen.47+func TestHelpShowsTheCommand(t *testing.T) {48+ f := &fakeHost{th: theme.New(true), w: 80, h: 16}49+ v := New(f, sortBy)50+ lines := boxText(v)51+ box := strings.Join(lines, "\n")52+ for _, want := range []string{"┌─ sort-by ", "Sort by the given cell path", "Docs: https://www.nushell.sh/commands/docs/sort-by.html", "Usage", "-r, --reverse"} {53+ if !strings.Contains(box, want) {54+ t.Errorf("no %q in\n%s", want, box)55+ }56+ }57+ for _, l := range lines {58+ if w := ansi.StringWidth(l); w > f.w {59+ t.Errorf("a line %d wide: %q", w, l)60+ }61+ }62+ if total := len(v.Lines()) + 1; !strings.Contains(lines[len(lines)-1], "of "+strconv.Itoa(total)) {63+ t.Errorf("the count: %q", lines[len(lines)-1])64+ }65+ v.Key(tea.KeyPressMsg{Code: tea.KeyEnd})66+ if box := strings.Join(boxText(v), "\n"); !strings.Contains(box, "ls | sort-by name -r") {67+ t.Errorf("scrolled to the end\n%s", box)68+ }69+ v.Key(tea.KeyPressMsg{Code: tea.KeyEscape})70+ if !f.closed {71+ t.Error("Esc didn't close")72+ }73+}
M internal/ui/hosts.go +2 -0
M internal/ui/nblang.go +25 -1
@@ -4,12 +4,16 @@ import (
44 "slices"55 "strings"66 7+ tea "charm.land/bubbletea/v2"8+79 "github.com/FelineStateMachine/012/internal/sheet"10+ "github.com/FelineStateMachine/012/internal/ui/cmdhelp"811 "github.com/FelineStateMachine/012/internal/ui/nbview"912 )1013 1114 // What the code editor knows of nushell: nu itself, asked about the12-// cell being written (nbview.Nu), with the notebook's own words. nu is15+// cell being written (nbview.Nu), with the notebook's own words, and16+// F1's help on the command at the caret. nu is1317 // asked only where a cell could run without asking: not in 012 serve1418 // unless serve-shell allows it, not with shell = off, and not about a1519 // file from another computer until its cells are trusted. Otherwise,
@@ -88,3 +92,23 @@ func (m *Model) nbWords(s *sheet.Sheet) []nbview.Word {
8892 }8993 return out9094 }95+96+func init() {97+ register(&command{id: "nb.word_help", macro: macroNever, title: "Help on the command",98+ desc: "Show the whole help of the nushell command or flag at the caret, with its page in nushell's docs",99+ enabled: func(m *Model) bool { v := m.nbView(); return v != nil && v.Editing() },100+ run: func(m *Model) tea.Cmd { return m.nbView().WordHelp() }})101+}102+103+// showWordHelp opens the help of the command at the caret, or the104+// keyboard shortcuts, as F1 anywhere else, when nu has none for it.105+func (m *Model) showWordHelp(msg nbview.HelpMsg) tea.Cmd {106+ if msg.Help == nil {107+ return m.runCommand("help")108+ }109+ m.openOverlay(cmdhelp.New(m.host(), *msg.Help))110+ return nil111+}112+113+// Code draws a line of nushell as a notebook's cells do.114+func (h host) Code(src string) string { return nbview.Highlighted(&h.m.th, src) }
M internal/ui/nblang_test.go +80 -0
@@ -3,9 +3,11 @@ package ui
33 import (44 "bytes"55 "context"6+ "encoding/json"67 "fmt"78 "io"89 "slices"10+ "strconv"911 "strings"1012 "testing"1113
@@ -13,6 +15,8 @@ import (
1315 1416 "github.com/FelineStateMachine/012/internal/nushell"1517 "github.com/FelineStateMachine/012/internal/sheet"18+ "github.com/FelineStateMachine/012/internal/ui/cmdhelp"19+ "github.com/FelineStateMachine/012/internal/ui/shortcuts"1620 )1721 1822 // ideNu is fakeNu answering the code editor's questions too, as nu
@@ -22,6 +26,9 @@ type ideNu struct {
2226 *fakeNu2327 ide []string2428 problem string29+ // help is the hover of every command, a command's help as nu30+ // writes it.31+ help string2532 }2633 2734 func (f *ideNu) Run(ctx context.Context, job nushell.Job, script string, stdout io.Writer) error {
@@ -33,6 +40,9 @@ func (f *ideNu) Run(ctx context.Context, job nushell.Job, script string, stdout
3340 if i := strings.Index(script, f.problem); job.IDE[0] == "--ide-check" && f.problem != "" && i >= 0 {3441 answer = fmt.Sprintf(`{"type":"diagnostic","severity":"Error","message":"Not a flag.","span":{"start":%d,"end":%d}}`, i, i+len(f.problem))3542 }43+ if job.IDE[0] == "--ide-hover" && f.help != "" {44+ answer = f.hover(script, job.IDE[1])45+ }3646 _, err := io.WriteString(stdout, answer)3747 return err3848 }
@@ -92,3 +102,73 @@ func TestNotebookAsksNuAsTrusted(t *testing.T) {
92102 t.Errorf("served without serve-shell: asked %v", got)93103 }94104 }105+106+// hover answers as nu does about the word at byte at of script: a107+// flag's "flag", a command's help.108+func (f *ideNu) hover(script, at string) string {109+ i, _ := strconv.Atoi(at)110+ from, to := i, i111+ for from > 0 && script[from-1] != ' ' && script[from-1] != '\n' {112+ from--113+ }114+ for to < len(script) && script[to] != ' ' {115+ to++116+ }117+ text := f.help118+ switch {119+ case from == to:120+ return ""121+ case script[from] == '-':122+ text = "flag"123+ case script[from] == '$' || script[from] == '|':124+ return ""125+ }126+ b, _ := json.Marshal(map[string]any{"hover": text, "span": map[string]int{"start": from, "end": to}})127+ return string(b)128+}129+130+// sortByHelp is sort-by's hover, as nu 0.116 writes it.131+const sortByHelp = "Sort by the given cell path or closure.\n### Usage\n```\n sort-by {flags} <...comparator>\n```\n\n### Flags\n\n" +132+ " `-h`, `--help` - Display the help message for this command\\\n `-r`, `--reverse` - Sort in reverse order.\\\n" +133+ " `-i`, `--ignore-case` - Sort string-based data case-insensitively.\n\n### Parameters\n\n" +134+ " `...comparator: oneof<cell-path, closure>` - The cell path(s) or closure(s) to compare elements by.\n\n\n" +135+ "### Input/output types\n\n```\n list<any> | list<any>\n table | table\n\n```\n### Example(s)\n```\n```\n" +136+ " Sort files by modified date.\n```\n ls | sort-by modified\n\n"137+138+// The caret resting on a command says its signature on the context139+// line; F1 opens its help, and Esc goes back to the cell being edited.140+// F1 where there's no command opens the shortcuts.141+func TestNotebookWordHelp(t *testing.T) {142+ nu := &ideNu{fakeNu: &fakeNu{out: map[string]string{}}, help: sortByHelp}143+ m := newModel()144+ m.SetShellRunner(nu)145+ m.SetMachine("here")146+ m.nb.still = true147+ m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})148+ run(m, m.runCommand("nb.open"))149+ press(t, m, "b", "<enter>", "ls | sort-by name -r", "<home>", "<right>", "<right>", "<right>", "<right>", "<right>", "<right>")150+ if got := line(m, contextLine); !strings.Contains(got, "sort-by <...comparator: cell-path|closure> --reverse") ||151+ !strings.Contains(got, "Sort by the given cell path or closure.") || !strings.Contains(got, "F1") {152+ t.Fatalf("context line %q", got)153+ }154+ press(t, m, "<end>")155+ if got := line(m, contextLine); !strings.Contains(got, "sort-by --reverse, -r") || !strings.Contains(got, "Sort in reverse order.") {156+ t.Errorf("on the flag: %q", got)157+ }158+ press(t, m, "<f1>")159+ if _, ok := m.overlay.(*cmdhelp.View); !ok {160+ t.Fatalf("F1 opened %T", m.overlay)161+ }162+ if s := screen(m); !strings.Contains(s, "https://www.nushell.sh/commands/docs/sort-by.html") || !strings.Contains(s, "-r, --reverse") ||163+ !strings.Contains(s, "ls | sort-by modified") || !strings.Contains(line(m, 0), "HELP") {164+ t.Errorf("help\n%s", s)165+ }166+ press(t, m, "<esc>")167+ if m.overlay != nil || !m.nbView().Editing() {168+ t.Errorf("Esc: overlay %T, editing %v", m.overlay, m.nbView().Editing())169+ }170+ press(t, m, " ", "<f1>")171+ if _, ok := m.overlay.(*shortcuts.View); !ok {172+ t.Errorf("F1 on no word opened %T", m.overlay)173+ }174+}
M internal/ui/nbscreen.go +2 -0
M internal/ui/nbview/edit.go +11 -3
A internal/ui/nbview/hover.go +352 -0
@@ -0,0 +1,352 @@
1+package nbview2+3+import (4+ "context"5+ "slices"6+ "strings"7+ "time"8+9+ tea "charm.land/bubbletea/v2"10+ "github.com/charmbracelet/x/ansi"11+12+ "github.com/FelineStateMachine/012/internal/notebook"13+ "github.com/FelineStateMachine/012/internal/nuon"14+ "github.com/FelineStateMachine/012/internal/nushell"15+ "github.com/FelineStateMachine/012/internal/ui/lineedit"16+ "github.com/FelineStateMachine/012/internal/ui/theme"17+)18+19+// The word at the caret, on the context line: once the caret rests on20+// a command or a flag, its signature and what it does, as nu's hover21+// says; on a $name the notebook binds, the cell it comes from and the22+// shape of its value, from the notebook's own outputs without asking23+// nu. A problem at the caret wins the line. F1 opens the command's24+// whole help (HelpMsg).25+26+// Hover is what the context line says of a word of a cell's source.27+type Hover struct {28+ From, To int // the word's bytes29+ Text string // sort-by <...comparator: cell-path|closure> --reverse, or $files: table, 12 rows × 4 columns from cell 330+ Desc string // what it does, or ""31+ // Brief is Text where there's no room for it: sort-by <...comparator> ….32+ Brief string33+ // Help is the command's whole help, for F1; nil for a variable.34+ Help *nushell.Help35+}36+37+// Hoverer says what the word at a byte of src is.38+type Hoverer interface {39+ Hover(ctx context.Context, src string, offset int) (Hover, bool)40+}41+42+// HelpMsg asks the UI to show a command's help, or its keyboard43+// shortcuts when Help is nil: F1 on a word nu has no help for.44+type HelpMsg struct{ Help *nushell.Help }45+46+type (47+ // restedMsg is the caret having rested since move moves.48+ restedMsg struct {49+ view *View50+ moves int51+ }52+ // hoveredMsg is the answer about the word at the caret after moves53+ // moves; open says F1 asked, and waits to show its help.54+ hoveredMsg struct {55+ view *View56+ moves int57+ src string58+ hover Hover59+ ok bool60+ open bool61+ }62+)63+64+// rest notes that the caret moved or the text changed: what was asked65+// about the word it was at is stale, and the word it's at is asked66+// about once it rests.67+func (v *View) rest() tea.Cmd {68+ e := &v.edit69+ e.moves++70+ e.stopHover()71+ moves := e.moves72+ return tea.Tick(pause, func(time.Time) tea.Msg { return restedMsg{view: v, moves: moves} })73+}74+75+func (e *editor) stopHover() {76+ if e.hoverStop != nil {77+ e.hoverStop()78+ e.hoverStop = nil79+ }80+}81+82+// updateHover takes the hover's messages.83+func (v *View) updateHover(msg tea.Msg) (tea.Cmd, bool) {84+ switch msg := msg.(type) {85+ case restedMsg:86+ if msg.view != v {87+ return nil, false88+ }89+ if v.edit.on && msg.moves == v.edit.moves {90+ return v.askHover(false), true91+ }92+ case hoveredMsg:93+ if msg.view != v {94+ return nil, false95+ }96+ if !v.edit.on || msg.moves != v.edit.moves {97+ return nil, true98+ }99+ v.edit.hover, v.edit.hoverFor = msg.hover, msg.src100+ if !msg.ok {101+ v.edit.hover = Hover{}102+ }103+ if msg.open {104+ return helpCmd(v.edit.hover.Help), true105+ }106+ default:107+ return nil, false108+ }109+ return nil, true110+}111+112+func helpCmd(h *nushell.Help) tea.Cmd { return func() tea.Msg { return HelpMsg{Help: h} } }113+114+// askHover asks about the word at the caret: a $name of the notebook's115+// at once, anything else in the background.116+func (v *View) askHover(open bool) tea.Cmd {117+ e := &v.edit118+ src, off, moves := e.text(), e.area.Offset(), e.moves119+ at, ok := wordAt(src, off)120+ if !ok || !v.editingCode() {121+ e.hover = Hover{}122+ if open {123+ return helpCmd(nil)124+ }125+ return nil126+ }127+ if h, ok := v.cellHover(src, at); ok {128+ e.hover, e.hoverFor = h, src129+ if open {130+ return helpCmd(nil)131+ }132+ return nil133+ }134+ hv := v.Providers.Hoverer135+ if hv == nil {136+ return helpCmdIf(open)137+ }138+ e.stopHover()139+ ctx, stop := context.WithCancel(context.Background())140+ e.hoverStop = stop141+ return func() tea.Msg {142+ h, ok := hv.Hover(ctx, src, at)143+ if ctx.Err() != nil {144+ return nil145+ }146+ return hoveredMsg{view: v, moves: moves, src: src, hover: h, ok: ok, open: open}147+ }148+}149+150+func helpCmdIf(open bool) tea.Cmd {151+ if open {152+ return helpCmd(nil)153+ }154+ return nil155+}156+157+// editingCode reports whether the cell being edited is code.158+func (v *View) editingCode() bool {159+ for _, c := range v.h.Cells() {160+ if c.ID == v.edit.id {161+ return c.Kind == notebook.Code162+ }163+ }164+ return false165+}166+167+// WordHelp is F1 in edit mode: the help of the command at the caret,168+// asked for if it hasn't been, or the shortcuts when there's none.169+func (v *View) WordHelp() tea.Cmd {170+ if h, ok := v.hovered(); ok && h.Help != nil {171+ return helpCmd(h.Help)172+ }173+ if !v.edit.on {174+ return helpCmd(nil)175+ }176+ return v.askHover(true)177+}178+179+// hovered is what's said of the word at the caret, while the text is180+// what it was said of.181+func (v *View) hovered() (Hover, bool) {182+ e := &v.edit183+ if !e.on || e.hover.Text == "" || e.hoverFor != e.text() {184+ return Hover{}, false185+ }186+ off := e.area.Offset()187+ return e.hover, off >= e.hover.From && off <= e.hover.To188+}189+190+// isWordByte reports whether b is part of a word: not a space, a191+// bracket, a pipe or a quote.192+func isWordByte(b byte) bool { return !strings.ContainsRune(" \t\n|;(){}[],'\"`", rune(b)) }193+194+// wordAt is the byte of src to ask about for the caret at off: the195+// word it's in, or the one it's just after.196+func wordAt(src string, off int) (int, bool) {197+ switch {198+ case off < len(src) && isWordByte(src[off]):199+ return off, true200+ case off > 0 && off <= len(src) && isWordByte(src[off-1]):201+ return off - 1, true202+ }203+ return 0, false204+}205+206+// wordBounds are the bytes of the word holding byte at.207+func wordBounds(src string, at int) (int, int) {208+ from, to := at, at209+ for from > 0 && isWordByte(src[from-1]) {210+ from--211+ }212+ for to < len(src) && isWordByte(src[to]) {213+ to++214+ }215+ return from, to216+}217+218+// varName is the name $name at byte from of src reads, and where it219+// ends: files of $files.name.220+func varName(src string, from int) (string, int) {221+ if from >= len(src) || src[from] != '$' {222+ return "", from223+ }224+ to := from + 1225+ for to < len(src) && (isNameByte(src[to])) {226+ to++227+ }228+ return src[from+1 : to], to229+}230+231+func isNameByte(b byte) bool {232+ return b == '_' || b == '-' || b >= '0' && b <= '9' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= 0x80233+}234+235+// cellHover is what the notebook knows of the $name at byte at: the236+// cell whose output or variable it is, and its value's shape.237+func (v *View) cellHover(src string, at int) (Hover, bool) {238+ from, _ := wordBounds(src, at)239+ name, to := varName(src, from)240+ if name == "" || at >= to {241+ return Hover{}, false242+ }243+ for i, c := range v.h.Cells() {244+ if c.Kind != notebook.Code || c.ID == v.edit.id {245+ continue246+ }247+ var shape string248+ switch {249+ case c.Name() == name:250+ shape = shapeWords(v.shown(c), v.h.Output(c.ID))251+ case slices.Contains(c.Parse().Assigned(), name):252+ shape = varShape(v.h.Output(c.ID), name)253+ default:254+ continue255+ }256+ text := "$" + name + ": " + shape + " from cell " + itoa(i+1)257+ if shape == "" {258+ text = "$" + name + ": from cell " + itoa(i+1) + ", not run yet"259+ }260+ return Hover{From: from, To: to, Text: text}, true261+ }262+ return Hover{}, false263+}264+265+// varShape is the shape of variable name of a cell's last run.266+func varShape(o *notebook.Output, name string) string {267+ if o == nil || o.Vars[name] == nil {268+ return ""269+ }270+ val, err := nuon.Parse(o.Vars[name])271+ if err != nil {272+ return "a value"273+ }274+ sh := shownOf(val)275+ if sh.kind == outValue {276+ return val.Kind.String()277+ }278+ return shapeWords(sh, nil)279+}280+281+// shapeWords is an output's shape, "table, 12 rows × 4 columns", or ""282+// before it has run.283+func shapeWords(sh *shown, o *notebook.Output) string {284+ switch sh.kind {285+ case outTable:286+ return "table, " + more(sh.total, "row") + " × " + more(sh.cols, "column")287+ case outRecord:288+ return "record, " + more(sh.total, "field")289+ case outList:290+ return "list, " + more(sh.total, "item")291+ case outText:292+ if sh.total > 1 {293+ return "string, " + more(sh.total, "line")294+ }295+ return "string"296+ case outError:297+ return "an error"298+ case outValue:299+ if o != nil {300+ if val, err := nuon.Parse(o.NUON); err == nil {301+ return val.Kind.String()302+ }303+ }304+ return "a value"305+ case outNone:306+ if o != nil && o.Count > 0 {307+ return "nothing"308+ }309+ }310+ return ""311+}312+313+// hoverLine draws h in room columns: the signature in the text's own314+// color, then what it does, muted;315+// flags are left out, then the types, then the description cut, before316+// the rest is.317+func hoverLine(th *theme.Theme, h Hover, room int) string {318+ text, desc := h.Text, h.Desc319+ width := func() int {320+ w := ansi.StringWidth(text)321+ if desc != "" {322+ w += 2 + ansi.StringWidth(desc)323+ }324+ return w325+ }326+ for width() > room && desc != "" {327+ i := strings.LastIndex(text, " --")328+ if i < 0 {329+ break330+ }331+ text = strings.TrimSuffix(text[:i], " …") + " …"332+ }333+ if width() > room && h.Brief != "" {334+ text = h.Brief335+ }336+ if desc == "" {337+ return th.Cell.Render(ansi.Truncate(text, max(room, 1), "…"))338+ }339+ out := th.Cell.Render(ansi.Truncate(text, max(room, 1), "…"))340+ if left := room - ansi.StringWidth(text) - 2; left > 3 {341+ out += " " + th.Muted.Render(ansi.Truncate(desc, left, "…"))342+ }343+ return out344+}345+346+// Highlighted draws a line of nushell in its syntax roles, as 012's347+// own highlighter has them: a command's usage and examples in its help.348+func Highlighted(th *theme.Theme, src string) string {349+ buf := []rune(src)350+ roles := rolesOf(src, Tokens{}.Highlight(context.Background(), src))351+ return drawRow(th, buf, lineedit.Row{Start: 0, End: len(buf)}, roles, nil)352+}
A internal/ui/nbview/hover_test.go +179 -0
@@ -0,0 +1,179 @@
1+package nbview2+3+import (4+ "context"5+ "strings"6+ "testing"7+8+ tea "charm.land/bubbletea/v2"9+ "github.com/charmbracelet/x/ansi"10+11+ "github.com/FelineStateMachine/012/internal/notebook"12+)13+14+// nu's hover, as testdata/nu records it: a command's signature and15+// summary, a flag in its command's help, a variable's type, and the16+// notebook's own word for a name it binds.17+func TestNuHovers(t *testing.T) {18+ _, n := nuFor(recordedNu(t))19+ for _, c := range []struct{ word, text, desc string }{20+ {"sort-by", "sort-by <...comparator: cell-path|closure> --reverse --ignore-case --natural --custom", "Sort by the given cell path or closure."},21+ {"-r", "sort-by --reverse, -r", "Sort in reverse order."},22+ {"--decimals", "into string --decimals <int>, -d", "Decimal digits to which to round."},23+ {"$n", "$n: int", ""},24+ {"$files", "$files: a cell's output", ""},25+ } {26+ at := strings.Index(hoverSrc, c.word+" ")27+ if c.word == "$n" {28+ at = strings.LastIndex(hoverSrc, c.word)29+ }30+ h, ok := n.Hover(context.Background(), hoverSrc, at+1)31+ if !ok || h.Text != c.text || h.Desc != c.desc || hoverSrc[h.From:h.To] != c.word {32+ t.Errorf("%s: %+v %v", c.word, h, ok)33+ }34+ if wantHelp := c.desc != ""; (h.Help != nil) != wantHelp {35+ t.Errorf("%s: help %v", c.word, h.Help)36+ }37+ }38+ n.SetWords([]string{"files"}, []Word{{Text: "$sheet.", Desc: "a range of a sheet: $sheet.A1:C9"}})39+ if h, ok := n.Hover(context.Background(), "$sheet.A1:C9 | first", 3); !ok || h.Text != "$sheet.A1:C9: a range of a sheet" {40+ t.Errorf("a range: %+v", h)41+ }42+}43+44+func TestCommandStart(t *testing.T) {45+ for _, c := range []struct{ src, flag, want string }{46+ {"ls | sort-by size -r", "-r", "sort-by"},47+ {"ls | each {|f| $f | sort-by x -r }", "-r", "sort-by"},48+ {"let big = ls -a", "-a", "ls"},49+ {"ls\n | where ('a|b' | str contains -i x)", "-i", "str"},50+ {"^git log --oneline", "--oneline", "git"},51+ } {52+ src := c.src53+ at, ok := commandStart(src, notebook.Parse(src).Strings, strings.Index(src, c.flag))54+ if !ok || !strings.HasPrefix(src[at:], c.want) {55+ t.Errorf("%q: %d %q", src, at, src[at:])56+ }57+ }58+}59+60+// settle runs cmd and hands the view the messages it makes, and theirs.61+func settle(v *View, cmd tea.Cmd) []tea.Msg {62+ var out []tea.Msg63+ for _, msg := range drain(cmd) {64+ out = append(out, msg)65+ if next, ok := v.Update(msg); ok {66+ out = append(out, settle(v, next)...)67+ }68+ }69+ return out70+}71+72+// press types keys, then lets the caret rest.73+func press(v *View, keys ...string) {74+ var cmd tea.Cmd75+ for _, k := range keys {76+ cmd, _ = v.Key(key(k))77+ }78+ settle(v, cmd)79+}80+81+// The caret resting on a command shows its signature on the context82+// line, F1 asks for its help, and a problem at the caret wins the line.83+func TestHoverOnTheContextLine(t *testing.T) {84+ f := recordedNu(t)85+ h := newHost(hoverSrc)86+ v := newView(h, 120, 20)87+ v.EditKeys["f1"] = "nb.word_help"88+ _, n := nuFor(f)89+ v.Providers = Providers{Highlighter: n, Checker: n, Hoverer: n}90+ settle(v, v.StartEdit())91+ press(v, "home", "right", "right", "right", "right", "right", "right", "right", "right", "right", "right")92+ left, right := v.ContextLine()93+ if got := ansi.Strip(left); !strings.HasPrefix(got, "sort-by <...comparator: cell-path|closure>") || !strings.Contains(got, "Sort by the given") {94+ t.Errorf("context line %q", got)95+ }96+ if !strings.Contains(ansi.Strip(right), "F1 help") {97+ t.Errorf("keys %q", ansi.Strip(right))98+ }99+ msgs := settle(v, v.WordHelp())100+ if len(msgs) != 1 || msgs[0].(HelpMsg).Help == nil || msgs[0].(HelpMsg).Help.Name != "sort-by" {101+ t.Errorf("F1: %v", msgs)102+ }103+104+ v.edit.diags, v.edit.diagFor = []Diagnostic{{From: 19, To: 26, Msg: "Something's wrong."}}, v.edit.text()105+ if left, _ := v.ContextLine(); ansi.Strip(left) != "Something's wrong." {106+ t.Errorf("the problem gives way: %q", ansi.Strip(left))107+ }108+ v.edit.diags = nil109+110+ cmd, _ := v.Key(key("home")) // the answer about the word left is dropped111+ if _, ok := v.Update(hoveredMsg{view: v, moves: v.edit.moves - 1, src: v.edit.text(), hover: Hover{Text: "stale", To: 99}, ok: true}); !ok || v.edit.hover.Text == "stale" {112+ t.Error("a stale answer was kept")113+ }114+ settle(v, cmd)115+ if left, _ := v.ContextLine(); strings.Contains(ansi.Strip(left), "sort-by") {116+ t.Errorf("the caret left the word: %q", ansi.Strip(left))117+ }118+}119+120+// F1 on no command asks for the shortcuts.121+func TestWordHelpWithoutAWord(t *testing.T) {122+ h := newHost("ls ")123+ v := newView(h, 80, 20)124+ settle(v, v.StartEdit())125+ msgs := settle(v, v.WordHelp())126+ if len(msgs) != 1 || msgs[0].(HelpMsg).Help != nil {127+ t.Errorf("F1 after a space: %v", msgs)128+ }129+}130+131+// A $name the notebook binds says its cell and its value's shape,132+// without asking nu.133+func TestCellHover(t *testing.T) {134+ h := newHost("files = ls", "x = 4\ny = [1 2 3]", "$files | append $x | append $y | append $gone")135+ h.outs[1] = &notebook.Output{Count: 1, NUON: []byte("[{name: a, size: 1, type: file}, {name: b, size: 2, type: dir}]")}136+ h.outs[2] = &notebook.Output{Count: 2, NUON: []byte("[1, 2, 3]"), Vars: map[string][]byte{"x": []byte("4")}}137+ v := newView(h, 100, 30)138+ v.Select(2, false)139+ settle(v, v.StartEdit())140+ src := v.edit.text()141+ for word, want := range map[string]string{142+ "$files": "$files: table, 2 rows × 3 columns from cell 1",143+ "$x": "$x: int from cell 2",144+ "$y": "$y: list, 3 items from cell 2",145+ } {146+ v.edit.area.Pos = len([]rune(src[:strings.Index(src, word)+2]))147+ settle(v, v.rest())148+ if left, _ := v.ContextLine(); ansi.Strip(left) != want {149+ t.Errorf("%s: %q", word, ansi.Strip(left))150+ }151+ }152+ h.outs[1] = nil153+ v.edit.area.Pos = 2154+ settle(v, v.rest())155+ if left, _ := v.ContextLine(); ansi.Strip(left) != "$files: from cell 1, not run yet" {156+ t.Errorf("not run: %q", ansi.Strip(left))157+ }158+ v.edit.area.Pos = len([]rune(src)) - 2159+ settle(v, v.rest())160+ if left, _ := v.ContextLine(); ansi.Strip(left) != "" {161+ t.Errorf("a name no cell binds: %q", ansi.Strip(left))162+ }163+}164+165+// A long signature gives up its flags, then its description, to fit.166+func TestHoverLineFits(t *testing.T) {167+ th := newHost().Theme()168+ h := Hover{Text: "sort-by <...comparator: cell-path> --reverse --ignore-case", Brief: "sort-by <...comparator> …", Desc: "Sort by the given cell path or closure."}169+ for room, want := range map[int]string{170+ 200: "sort-by <...comparator: cell-path> --reverse --ignore-case Sort by the given cell path or closure.",171+ 90: "sort-by <...comparator: cell-path> --reverse … Sort by the given cell path or closure.",172+ 60: "sort-by <...comparator> … Sort by the given cell path or c…",173+ 40: "sort-by <...comparator> … Sort by the …",174+ } {175+ if got := ansi.Strip(hoverLine(th, h, room)); got != want || ansi.StringWidth(got) > room {176+ t.Errorf("%d: %q", room, got)177+ }178+ }179+}
M internal/ui/nbview/keys.go +1 -1
M internal/ui/nbview/mouse.go +1 -1
M internal/ui/nbview/nu_test.go +21 -3
@@ -29,7 +29,7 @@ import (
2929 type fakeRunner struct {3030 mu sync.Mutex3131 version string32- answers map[string]string // flag + text asked about → answer32+ answers map[string]string // flags, a line, the text asked about → answer3333 err error // every question fails so3434 block bool // questions wait until they're stopped3535 asked []string
@@ -49,7 +49,7 @@ func (f *fakeRunner) Run(ctx context.Context, job nushell.Job, script string, st
4949 <-ctx.Done()5050 return context.Cause(ctx)5151 }52- out, ok := f.answers[job.IDE[0]+script]52+ out, ok := f.answers[asked(job.IDE, script)]5353 if !ok {5454 return &nushell.Error{Msg: "not recorded: " + script}5555 }
@@ -57,6 +57,9 @@ func (f *fakeRunner) Run(ctx context.Context, job nushell.Job, script string, st
5757 return err5858 }5959 60+// asked is how fakeRunner files a question.61+func asked(flags []string, script string) string { return strings.Join(flags, " ") + "\n" + script }62+6063 func (f *fakeRunner) questions() []string {6164 f.mu.Lock()6265 defer f.mu.Unlock()
@@ -97,15 +100,26 @@ func recordings(t *testing.T) []recording {
97100 }98101 out = append(out, recording{flags: flags, script: script, answer: read(r.answer)})99102 }103+ script := read("hover.nu")104+ for _, at := range hoverAsked {105+ out = append(out, recording{flags: []string{"--ide-hover", fmt.Sprint(at)}, script: script, answer: read(fmt.Sprintf("hover-%d.json", at))})106+ }100107 return out101108 }102109 110+// hoverAsked are the bytes of hover.nu nu's hover is recorded at: the111+// sort-by, -r, into string, --decimals and $n of hoverSrc.112+var hoverAsked = []int{83, 96, 101, 113, 135}113+114+// hoverSrc is the cell hover.nu asks about.115+const hoverSrc = "let n = 4\n$files | sort-by size -r | into string --decimals 2 | append $n"116+103117 // recordedNu is a fake nu answering what testdata/nu records.104118 func recordedNu(t *testing.T) *fakeRunner {105119 t.Helper()106120 f := &fakeRunner{version: "0.116.0\n", answers: map[string]string{}}107121 for _, r := range recordings(t) {108- f.answers[r.flags[0]+r.script] = r.answer122+ f.answers[asked(r.flags, r.script)] = r.answer109123 }110124 return f111125 }
@@ -224,6 +238,9 @@ func TestNuFallsBack(t *testing.T) {
224238 }225239 asked := len(f.questions())226240 n.Highlight(context.Background(), src)241+ if _, ok := n.Hover(context.Background(), src, 6); ok {242+ t.Errorf("%s: a hover", name)243+ }227244 if got := n.Complete(context.Background(), "$fi", 3); len(got) != 1 || got[0].Text != "$files" || len(f.questions()) != asked {228245 t.Errorf("%s: completions %v, asked %v", name, got, f.questions())229246 }
@@ -234,6 +251,7 @@ func TestNuFallsBack(t *testing.T) {
234251 n.Highlight(context.Background(), src)235252 n.Check(context.Background(), src)236253 n.Complete(context.Background(), src, 2)254+ n.Hover(context.Background(), src, 6)237255 if q := f.questions(); len(q) != 0 {238256 t.Errorf("asked nu while it isn't allowed: %v", q)239257 }
A internal/ui/nbview/nuhover.go +152 -0
@@ -0,0 +1,152 @@
1+package nbview2+3+import (4+ "context"5+ "slices"6+ "strings"7+8+ "github.com/FelineStateMachine/012/internal/notebook"9+ "github.com/FelineStateMachine/012/internal/nushell"10+)11+12+// Hover implements Hoverer: for a command, its signature and help from13+// nu's --ide-hover; for a flag, the flag in its command's help; for a14+// variable, the notebook's word for it ($selection, a linked file), or15+// the type nu gives a variable the cell binds itself.16+func (n *Nu) Hover(ctx context.Context, src string, at int) (Hover, bool) {17+ names, words := n.known()18+ from, to := wordBounds(src, at)19+ if h, ok := wordHover(words, src, from, to); ok {20+ return h, true21+ }22+ p := prepare(src, names)23+ nh, ok := n.hoverAt(ctx, p, from) // the word's start, whichever byte of it the caret is at24+ if !ok {25+ return Hover{}, false26+ }27+ from, to = nh.From-p.at, nh.To-p.at28+ if from < 0 || to > len(src) || from >= to || p.inHead(from, to) {29+ return Hover{}, false30+ }31+ if nh.Text == "flag" {32+ return n.flagHover(ctx, src, p, from, to)33+ }34+ if help, ok := nushell.ParseHelp(nh.Text); ok {35+ return Hover{From: from, To: to, Text: help.Signature(), Brief: help.Brief(), Desc: help.Summary(), Help: &help}, true36+ }37+ word := src[from:to]38+ if strings.HasPrefix(word, "$") && nh.Text != "any" && !strings.ContainsAny(nh.Text, "\n ") {39+ return Hover{From: from, To: to, Text: word + ": " + nh.Text}, true40+ }41+ return Hover{}, false42+}43+44+// hoverAt is nu's hover at byte at of the source p was prepared from.45+func (n *Nu) hoverAt(ctx context.Context, p prepared, at int) (h nushell.Hover, ok bool) {46+ err := n.s.ask(ctx, func(ctx context.Context) (err error) {47+ h, ok, err = nushell.HoverAt(ctx, n.s.Runner, p.text, p.at+at)48+ return err49+ })50+ return h, err == nil && ok51+}52+53+// flagHover is what the help of the command a flag belongs to says of54+// the flag at src[from:to].55+func (n *Nu) flagHover(ctx context.Context, src string, p prepared, from, to int) (Hover, bool) {56+ cmd, ok := commandStart(p.text[p.at:], notebook.Parse(src).Strings, from)57+ if !ok {58+ return Hover{}, false59+ }60+ nh, ok := n.hoverAt(ctx, p, cmd)61+ if !ok {62+ return Hover{}, false63+ }64+ help, ok := nushell.ParseHelp(nh.Text)65+ if !ok {66+ return Hover{}, false67+ }68+ f, ok := help.Flag(src[from:to])69+ if !ok {70+ return Hover{}, false71+ }72+ text := help.Name + " " + f.Long73+ if f.Arg != "" {74+ text += " <" + f.Arg + ">"75+ }76+ if f.Short != "" {77+ text += ", " + f.Short78+ }79+ return Hover{From: from, To: to, Text: strings.TrimSpace(text), Desc: f.Desc, Help: &help}, true80+}81+82+// commandStart is the first byte of the command whose argument begins83+// at byte at of src: after the |, ;, line or bracket opening the84+// pipeline's element it's in, strings in strs left alone, and after a85+// let's =.86+func commandStart(src string, strs [][2]int, at int) (int, bool) {87+ depth, i := 0, at88+scan:89+ for i > 0 {90+ i--91+ if s, ok := stringAround(strs, i); ok {92+ i = s93+ continue94+ }95+ switch src[i] {96+ case ')', '}', ']':97+ depth++98+ case '(', '{', '[':99+ if depth == 0 {100+ i++101+ break scan102+ }103+ depth--104+ case '|', ';', '\n':105+ if depth == 0 {106+ i++107+ break scan108+ }109+ }110+ }111+ for i < at && (src[i] == ' ' || src[i] == '\t' || src[i] == '^') {112+ i++113+ }114+ if _, end := wordBounds(src, i); slices.Contains([]string{"let", "mut", "const"}, src[i:end]) {115+ if eq := strings.IndexByte(src[end:at], '='); eq >= 0 {116+ i = end + eq + 1117+ for i < at && (src[i] == ' ' || src[i] == '\t') {118+ i++119+ }120+ }121+ }122+ return i, i < at123+}124+125+// stringAround is where the string holding byte i starts.126+func stringAround(strs [][2]int, i int) (int, bool) {127+ for _, s := range strs {128+ if i >= s[0] && i < s[1] {129+ return s[0], true130+ }131+ }132+ return 0, false133+}134+135+// wordHover is the notebook's own word for the $name at src[from:to]:136+// $selection, a linked file, a range of a sheet.137+func wordHover(words []Word, src string, from, to int) (Hover, bool) {138+ name, end := varName(src, from)139+ if name == "" {140+ return Hover{}, false141+ }142+ for _, w := range words {143+ switch {144+ case w.Text == "$"+name:145+ return Hover{From: from, To: end, Text: w.Text + ": " + w.Desc}, true146+ case strings.HasSuffix(w.Text, ".") && strings.HasPrefix(src[from:to], w.Text):147+ desc, _, _ := strings.Cut(w.Desc, ":") // a range of a sheet: $sheet.A1:C9148+ return Hover{From: from, To: to, Text: src[from:to] + ": " + desc}, true149+ }150+ }151+ return Hover{}, false152+}
M internal/ui/nbview/panel.go +6 -0
M internal/ui/nbview/syntax.go +1 -0
A internal/ui/nbview/testdata/nu/hover-101.json +1 -0
@@ -0,0 +1 @@
1+{"hover":"Convert value to a string.\n### Usage\n```\n into string {flags} <...rest>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\\\n `-g`, `--group-digits` - Group digits together by the locale specific thousands separator.\\\n `-d`, `--decimals` `<int>` - Decimal digits to which to round.\n\n### Parameters\n\n `...rest: cell-path` - For a data structure input, convert data at the given cell paths.\n\n\n### Input/output types\n\n```\n binary | string\n int | string\n number | string\n string | string\n glob | string\n bool | string\n filesize | string\n datetime | string\n duration | string\n cell-path | string\n range | string\n semver | string\n semver-range | string\n list<any> | list<string>\n table | table\n record | record\n\n```\n### Example(s)\n```\n```\n convert int to string and append three decimal places.\n```\n 5 | into string --decimals 3\n\n```\n convert float to string and round to nearest integer.\n```\n 1.7 | into string --decimals 0\n\n```\n convert float to string.\n```\n 1.7 | into string --decimals 1\n\n```\n convert float to string and limit to 2 decimals.\n```\n 1.734 | into string --decimals 2\n\n```\n convert float to string.\n```\n 4.3 | into string\n\n```\n convert string to string.\n```\n '1234' | into string\n\n```\n convert boolean to string.\n```\n true | into string\n\n```\n convert date to string.\n```\n '2020-10-10 10:00:00 +02:00' | into datetime | into string\n\n```\n convert filepath to string.\n```\n ls Cargo.toml | get name | into string\n\n```\n convert filesize to string.\n```\n 1kB | into string\n\n```\n convert duration to string.\n```\n 9day | into string\n\n```\n convert cell-path to string.\n```\n $.name | into string\n\n```\n convert semver to string.\n```\n '1.2.3' | into semver | into string\n\n","span":{"start":101,"end":112}}
A internal/ui/nbview/testdata/nu/hover-113.json +1 -0
A internal/ui/nbview/testdata/nu/hover-135.json +1 -0
A internal/ui/nbview/testdata/nu/hover-83.json +1 -0
A internal/ui/nbview/testdata/nu/hover-96.json +1 -0
A internal/ui/nbview/testdata/nu/hover.nu +5 -0
M internal/ui/nbview/view_test.go +2 -1
M internal/ui/notebook.go +3 -2