- commit
- 28c1052994c13baa756ca8c938924d582486194b
- parent
- 0aeb75d34c87d8b00619fb654a9a0de47432763b
- parent
- 41ba2bbe4ab8e2ef74b8b44e3f1984bf80816261
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 6 | +- |
| M | ROADMAP.md | 3 | +- |
| M | cmd/ | 4 | + |
| A | cmd/ | 116 | ++++++++++++++++++++++++++++++ |
| A | cmd/ | 97 | +++++++++++++++++++++++++ |
| A | demos/ | 41 | +++++++++++ |
| M | docs/ | 1 | + |
| A | docs/ | bin | |
| M | docs/ | 47 | ++++++++++-- |
| M | docs/ | 62 | +++++++++++++--- |
| M | docs/ | 5 | +- |
| A | e2e/ | 76 | ++++++++++++++++++++ |
| A | internal/ | 63 | ++++++++++++++++ |
| A | internal/ | 15 | ++++ |
| M | website/ | 2 | +- |
@@ -42,8 +42,10 @@ rest: building from a clone, importing files, `012 serve` and settings.
4242 - **Files**: a diff-friendly JSON format, import from CSV, TSV, JSON,4343 nushell's NUON, XLSX, SQLite, Parquet and Lotus 1-2-3, export to CSV,4444 TSV, JSON, NUON, XLSX and SQLite ([files](docs/files/README.md)).45-- **A stage in a pipeline**: `ls | to nuon | ^012 --pipe | from nuon`46- edits a table on the terminal and sends it on with its types45+- **A stage in a pipeline**: `ls | sheet | where size > 1kb` edits a46+ table on the terminal and sends it on with its types, through the47+ nushell module `012 nu --install-module` installs, or as48+ `ls | to nuon | ^012 --pipe | from nuon` without it4749 ([pipelines](docs/nushell/pipelines.md)).4850 - **Nushell notebooks**: `012 nu` (or Data > Shell in any workbook) runs nushell4951 pipelines whose tables become live, named regions of the sheet;@@ -36,7 +36,7 @@ formulas, charts and pivots use. It replaces the grid-based notebook sheet.
3636 | Item | Result | Size |3737 |---|---|---|3838 | Notebook tabs: code and note cells, outputs rendered in place (tables that scroll and open full-screen as a grid, text, records, errors), Jupyter keys inside the notebook (Shift+Enter run and next, Ctrl+Enter run, a/b add above/below, dd delete, m/y note/code, Esc/Enter command/edit mode) plus menus and palette; run cell, run all, run above, run below, stop, clear outputs; run counts and timings; cells named for their output (`$files`), stale marks when a cell they read has changed, an opt-in reactive mode that re-runs dependents; send an output to a sheet as a live region (`nu.files`); outputs saved in the workbook up to a size cap; existing notebook sheets converted, one code cell per region; `012 nu` opens a notebook | A TUI Jupyter with nushell and 012 | L |39-| Nushell's own completions in code cells; a way to call 012 from nu without `^012` (a shipped `012.nu` module, or `nu_plugin_012`) | | S to M |39+| Code cells highlighted, completed and checked as you type by nushell itself: `nu --ide-ast` token shapes mapped to theme roles, `nu --ide-complete` plus 012's cell, region and sheet names on Tab, `nu --ide-check` errors underlined; debounced in the background, plain text when nu is missing or slow; hover docs through `nu --lsp` later | Writing pipelines in 012 feels like nushell's own prompt | S to M |4040 4141 ### 3. Toward multiplayer4242 @@ -114,6 +114,7 @@ stream above rather than a design of their own.
114114 **Nushell**115115 116116 - 012 as a stage in a pipeline: `012 -` reads a table from standard input, `012 --pipe` sends the sheet or selection on, with nushell's types kept through NUON: [Pipelines](docs/nushell/pipelines.md)117+- The `sheet` command: a nushell module shipped in the binary (`012 nu --install-module`), so nu calls 012 without `^012` or NUON on either side: [Pipelines](docs/nushell/pipelines.md#the-sheet-command)117118 - `012 nu` and notebook sheets: nushell pipelines at a prompt on the formula bar become live, named regions that read each other and refresh in dependency order, saved as commands and never run on open: [Notebooks](docs/nushell/notebooks.md)118119 119120 **Files**@@ -68,6 +68,7 @@ func usage() error {
6868 " 012 [flags] -: a table from standard input (NUON, JSON, CSV or TSV)\n" +6969 " 012 [flags] --pipe [--to nuon|json|csv|tsv] [file]: on quitting, send the table to standard output\n" +7070 " 012 nu [flags] [file]: a nushell notebook, at its prompt (see docs/nushell/notebooks.md)\n" +71+ " 012 nu --module | --install-module [--force] [path]: the nushell module with sheet (see docs/nushell/README.md)\n" +7172 " 012 serve [flags] [dir]: serve sheets in dir over SSH (see docs/terminal/ssh.md)\n" +7273 " 012 config [path|edit|default|themes|set-key|delete-key]\n" +7374 " 012 version")@@ -84,6 +85,9 @@ func run(args []string, e env) error {
8485 // A file called serve opens as ./serve.8586 return runServe(args[1:], e)8687 }88+ if len(args) > 0 && args[0] == "nu" && isModuleCommand(args[1:]) {89+ return runNuModule(args[1:], e)90+ }8791 // A file called nu opens as ./nu.8892 notebook := len(args) > 0 && args[0] == "nu"8993 if notebook {@@ -0,0 +1,116 @@
1+package main2+3+import (4+ "bytes"5+ "errors"6+ "fmt"7+ "io/fs"8+ "os"9+ "os/exec"10+ "path/filepath"11+ "strings"12+13+ "github.com/FelineStateMachine/012/internal/nushell/module"14+)15+16+// 012 nu --module and 012 nu --install-module: the nushell module that17+// gives 012 the command `sheet` (see docs/nushell/README.md).18+19+// nuConfigDir asks nu for its config directory, where config.nu is and20+// whose scripts folder is on nushell's library path. Tests replace it.21+var nuConfigDir = func() (string, error) {22+ out, err := exec.Command("nu", "--no-config-file", "-c", "$nu.default-config-dir").Output()23+ if err != nil {24+ return "", fmt.Errorf("asking nu for its config directory: %w; name the file instead: 012 nu --install-module path/to/%s", err, module.Name)25+ }26+ return strings.TrimSpace(string(out)), nil27+}28+29+// isModuleCommand reports whether 012 nu's arguments ask for the module30+// rather than a notebook.31+func isModuleCommand(args []string) bool {32+ return len(args) > 0 && (args[0] == "--module" || args[0] == "--install-module")33+}34+35+// runNuModule is 012 nu --module (print the module) or 012 nu36+// --install-module [--force] [path].37+func runNuModule(args []string, e env) error {38+ if args[0] == "--module" {39+ if len(args) > 1 {40+ return usage()41+ }42+ _, err := fmt.Fprint(e.stdout, module.Source)43+ return err44+ }45+ force := false46+ var paths []string47+ for _, a := range args[1:] {48+ if a == "--force" {49+ force = true50+ } else {51+ paths = append(paths, a)52+ }53+ }54+ if len(paths) > 1 {55+ return usage()56+ }57+ path, useLine, err := modulePath(paths)58+ if err != nil {59+ return err60+ }61+ wrote, err := installModule(path, force)62+ if err != nil {63+ return err64+ }65+ if wrote {66+ fmt.Fprintln(e.stdout, "Wrote", path)67+ } else {68+ fmt.Fprintln(e.stdout, path, "is up to date")69+ }70+ fmt.Fprintln(e.stdout, "Add this line to config.nu (config nu opens it):")71+ fmt.Fprintln(e.stdout, useLine)72+ return nil73+}74+75+// modulePath is where the module goes, and the line config.nu needs to76+// use it: the path given, or scripts/012.nu in nu's config directory,77+// which config.nu names relative to itself.78+func modulePath(given []string) (path, useLine string, err error) {79+ if len(given) == 1 {80+ path, err := filepath.Abs(given[0])81+ if err != nil {82+ return "", "", err83+ }84+ if info, err := os.Stat(path); err == nil && info.IsDir() {85+ path = filepath.Join(path, module.Name)86+ }87+ return path, fmt.Sprintf("use %q *", path), nil88+ }89+ dir, err := nuConfigDir()90+ if err != nil {91+ return "", "", err92+ }93+ if dir == "" {94+ return "", "", errors.New("nu named no config directory")95+ }96+ return filepath.Join(dir, "scripts", module.Name), "use scripts/" + module.Name + " *", nil97+}98+99+// installModule writes the module to path, creating its directory. A100+// file already there is replaced only with force, unless it's the same101+// module; wrote is false then.102+func installModule(path string, force bool) (wrote bool, err error) {103+ old, err := os.ReadFile(path)104+ switch {105+ case err == nil && bytes.Equal(old, []byte(module.Source)):106+ return false, nil107+ case err == nil && !force:108+ return false, fmt.Errorf("%s exists and isn't this 012's module; --force replaces it", path)109+ case err != nil && !errors.Is(err, fs.ErrNotExist):110+ return false, err111+ }112+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {113+ return false, err114+ }115+ return true, os.WriteFile(path, []byte(module.Source), 0o644)116+}@@ -0,0 +1,97 @@
1+package main2+3+import (4+ "os"5+ "path/filepath"6+ "strings"7+ "testing"8+9+ "github.com/FelineStateMachine/012/internal/nushell/module"10+)11+12+func TestNuModulePrints(t *testing.T) {13+ e, out, _ := testEnv(t, nil)14+ if err := run([]string{"nu", "--module"}, e); err != nil {15+ t.Fatal(err)16+ }17+ if out.String() != module.Source || !strings.Contains(out.String(), "export def sheet [") {18+ t.Errorf("printed:\n%s", out)19+ }20+}21+22+// fakeNuConfig points 012 nu --install-module at a temporary nu config23+// directory.24+func fakeNuConfig(t *testing.T) string {25+ t.Helper()26+ dir := filepath.Join(t.TempDir(), "nushell")27+ old := nuConfigDir28+ nuConfigDir = func() (string, error) { return dir, nil }29+ t.Cleanup(func() { nuConfigDir = old })30+ return dir31+}32+33+func TestNuInstallModule(t *testing.T) {34+ e, out, _ := testEnv(t, nil)35+ dir := fakeNuConfig(t)36+ path := filepath.Join(dir, "scripts", "012.nu")37+ if err := run([]string{"nu", "--install-module"}, e); err != nil {38+ t.Fatal(err)39+ }40+ if got, err := os.ReadFile(path); err != nil || string(got) != module.Source {41+ t.Fatalf("installed %q, %v", got, err)42+ }43+ if want := "Wrote " + path + "\nAdd this line to config.nu (config nu opens it):\nuse scripts/012.nu *\n"; out.String() != want {44+ t.Errorf("printed %q, want %q", out, want)45+ }46+47+ // Again: the same module is left as it is.48+ out.Reset()49+ if err := run([]string{"nu", "--install-module"}, e); err != nil || !strings.HasPrefix(out.String(), path+" is up to date\n") {50+ t.Errorf("reinstalling: %v, %q", err, out)51+ }52+53+ // A different file there stays unless --force.54+ os.WriteFile(path, []byte("# mine\n"), 0o644)55+ err := run([]string{"nu", "--install-module"}, e)56+ if err == nil || !strings.Contains(err.Error(), "--force replaces it") {57+ t.Errorf("overwrote without --force: %v", err)58+ }59+ if got, _ := os.ReadFile(path); string(got) != "# mine\n" {60+ t.Errorf("file changed: %q", got)61+ }62+ if err := run([]string{"nu", "--install-module", "--force"}, e); err != nil {63+ t.Fatal(err)64+ }65+ if got, _ := os.ReadFile(path); string(got) != module.Source {66+ t.Errorf("--force didn't replace it: %q", got)67+ }68+}69+70+// An explicit path is used as given, or as the folder to put 012.nu in,71+// and config.nu names it in full.72+func TestNuInstallModuleAt(t *testing.T) {73+ e, out, _ := testEnv(t, nil)74+ old := nuConfigDir75+ nuConfigDir = func() (string, error) { t.Fatal("asked nu"); return "", nil }76+ t.Cleanup(func() { nuConfigDir = old })77+ dir := t.TempDir()78+ file := filepath.Join(dir, "lib", "sheet.nu")79+ if err := run([]string{"nu", "--install-module", file}, e); err != nil {80+ t.Fatal(err)81+ }82+ if got, _ := os.ReadFile(file); string(got) != module.Source {83+ t.Errorf("installed %q", got)84+ }85+ if !strings.HasSuffix(out.String(), "use \""+file+"\" *\n") {86+ t.Errorf("printed %q", out)87+ }88+ if err := run([]string{"nu", "--install-module", dir}, e); err != nil {89+ t.Fatal(err)90+ }91+ if _, err := os.Stat(filepath.Join(dir, "012.nu")); err != nil {92+ t.Error(err)93+ }94+ if err := run([]string{"nu", "--module", "extra"}, e); err == nil {95+ t.Error("--module took an argument")96+ }97+}@@ -0,0 +1,41 @@
1+# The sheet command: nushell's module from 012 nu --install-module, used2+# in a nu session. ls's table goes through sheet, three rows are sent3+# back and nu keeps filtering them by size; then quitting without4+# sending makes sheet fail with 012's reason, which try catches. Needs5+# nu on the PATH.6+Output out/sheet.gif7+Source lib/setup.tape8+Source lib/nu.tape9+10+# The module goes next to the binary, out of the recording.11+Hide12+Type "^012 nu --install-module ../bin/012.nu" Enter13+Sleep 1s14+Type "use ../bin/012.nu *; clear" Enter15+Sleep 1s16+Show17+18+Sleep 600ms19+Type "ls data | select name size | sort-by size --reverse | sheet | where size > 1kb"20+Sleep 600ms21+Enter22+Sleep 2s23+Ctrl+G24+Type "A1:B4" Enter25+Sleep 1s26+Screenshot out/stills/sheet-sheet.png27+Ctrl+Q28+Sleep 1.6s29+Enter30+Sleep 2.5s31+Screenshot out/stills/sheet-result.png32+Type "try { ls data | sheet } catch {|e| $e.msg }"33+Sleep 600ms34+Enter35+Sleep 2s36+Ctrl+Q37+Sleep 1.4s38+Type "d"39+Sleep 2.5s40+Screenshot out/stills/sheet-error.png41+Sleep 1s@@ -25,6 +25,7 @@ internal/jev the API key's resolution, answer cache and TypeSafe client
2525 internal/keyring the OS credential store the API key lives in2626 internal/macro macros: the Starlark scripting API, recorded actions as scripts, step limits2727 internal/nushell a notebook's commands: nu run as a process, tables in as NUON files, a table back28+ module 012.nu, the nushell module with sheet, embedded for 012 nu --module and --install-module2829 internal/telemetry opt-in JSON log and OTLP export of spans, events and frame stats2930 internal/serve the SSH server: auth, host key, a Model per session (charm.land/wish/v2)3031 internal/confine resolving typed file names, confined to a directory when servedBinary file.
@@ -18,7 +18,7 @@ each pipeline's table a live part of the sheet.
1818 1919 | Way | Looks like | For |2020 |---|---|---|21-| [A stage in a pipeline](pipelines.md) | `ls \| to nuon \| ^012 --pipe \| from nuon` | Looking at a table on the terminal, editing it, and sending it on to the next command |21+| [A stage in a pipeline](pipelines.md) | `ls \| sheet \| where size > 1kb` | Looking at a table on the terminal, editing it, and sending it on to the next command |2222 | [A notebook](notebooks.md) | `012 nu`, then `ls \| select name size` at the `nu❯` prompt | Pipelines you keep: each one's table is a named region of the sheet that formulas, charts and other pipelines read, run again when you ask |2323 | [A followed file](../files/following.md) | Data > Linked file, then `$app \| where status >= 500` | A log or export that keeps growing: its rows come in as they're written, and pipelines read the rows it has now |2424 @@ -27,14 +27,48 @@ The first needs nothing but nushell. The other two run `nu` from inside
2727 nushell](https://www.nushell.sh/book/installation.html)); without it the2828 prompt says so, and the rest of 012 works as before.2929 30+## Install the `sheet` command31+32+012 comes with a nushell module, `012.nu`, whose `sheet` command runs33+012 in a pipeline. Write it into nushell's `scripts` folder:34+35+```nu36+^012 nu --install-module37+```38+39+and add the line it prints to `config.nu` (`config nu` opens it):40+41+```nu42+use scripts/012.nu *43+```44+45+In a new nushell, `help sheet` describes it:46+47+| Command | Does |48+|---|---|49+| `sheet` | Opens the table piped in (or a file: `sheet budget.xlsx`) and returns what you send back, with its types ([Pipelines](pipelines.md#the-sheet-command)) |50+| `sheet view` | Opens the table piped in and returns nothing |51+| `sheet nu` | Opens a [notebook](notebooks.md), as `012 nu` |52+53+`^012 nu --install-module` refuses to replace a different file already54+there unless given `--force`; given a path, it writes the module there55+instead. `^012 nu --module` prints the module, to read or to put56+somewhere yourself.57+58+Without the module, 012 is run as `^012`: in nushell, a word that59+starts with a digit is a number, and the caret calls an external60+command by name ([Running external61+commands](https://www.nushell.sh/book/running_externals.html)). Other62+shells run it as `012`.63+3064 ## A 30-second tour3165 3266 ```nu3367 # A table in, edited, and out again, with its types34-ls | select name size | to nuon | ^012 --pipe | from nuon | where size > 1kb68+ls | select name size | sheet | where size > 1kb3569 3670 # A notebook: each pipeline's table lands in the grid37-^012 nu work.01271+sheet nu work.0123872 ```3973 4074 At the notebook's `nu❯` prompt:@@ -48,16 +82,11 @@ The second region reads the first as `$sales`; typing a new command for
4882 `sales` runs it and then the region that reads it. In a formula on any4983 sheet, `=SUM(nu.sales)` reads the same table.5084 51-In nushell, a word that starts with a digit is a number, so 012 is run52-as `^012`, the caret nushell uses to call an external command by name53-([Running external commands](https://www.nushell.sh/book/running_externals.html)).54-Other shells run it as `012`.55-5685 ## Pages5786 5887 | Page | For |5988 |---|---|60-| [Pipelines](pipelines.md) | `012 -` and `012 --pipe`: tables in on standard input and out on standard output, sending back, the exit status |89+| [Pipelines](pipelines.md) | `sheet`, `012 -` and `012 --pipe`: tables in on standard input and out on standard output, sending back, the exit status |6190 | [Notebooks](notebooks.md) | `012 nu` and the `nu❯` prompt: regions, `$r1` and `$in`, `nu.r1` in formulas, refreshing, saving and trust |6291 | [Types](types.md) | How each nushell type becomes a cell and goes back, and what doesn't survive the trip |6392 | [Cookbook](cookbook.md) | Worked examples: disk usage, a log followed live, a CSV cleaned and summarized, an API, processes, git history |@@ -19,19 +19,32 @@ ls | select name size | to nuon | ^012 --pipe | from nuon | where size > 1kb
1919 the table in NUON, nushell's own notation, so file sizes, durations and2020 dates arrive as themselves, and2121 [`from nuon`](https://www.nushell.sh/commands/docs/from_nuon.html) reads22-what 012 sends back with the same types ([Types](types.md)). In23-nushell, 012 is called as `^012`, since a word starting with a digit is24-a number there.22+what 012 sends back with the same types ([Types](types.md)). With the23+[`sheet` command](#the-sheet-command) installed, the same stage is24+`ls | select name size | sheet | where size > 1kb`.2525 2626 The screen is the terminal's own (`/dev/tty`, or the console on2727 Windows), not standard input or output, so the pipeline's data never2828 mixes with what 012 draws, and nothing but the table reaches standard2929 output.3030 31+```mermaid32+sequenceDiagram33+ participant nu as nushell34+ participant o12 as 012 --pipe35+ participant tty as the terminal36+ nu->>o12: to nuon: the table on standard input37+ o12->>tty: the sheet, drawn on /dev/tty38+ tty->>o12: keys: edit, select, Ctrl+Q, Enter39+ o12->>nu: the selection or the sheet, as NUON on standard output40+ Note over nu: from nuon: a table with its types41+```42+3143 | Command | Reads | Writes |3244 |---|---|---|3345 | `012 -` | A table from standard input, into a new sheet | Nothing; the table stays in 012 until you save or download it |3446 | `012 --pipe` | A table from standard input, or a file named after it | The sheet or the selection, to standard output, when you quit |47+| `sheet`, `sheet view` | As `012 --pipe` and `012 -`, from nushell values | A nushell table, or nothing |3548 3649 ## Reading a table: `012 -`3750 @@ -80,6 +93,32 @@ instead:
8093 open budget.xlsx | to nuon | ^012 --pipe --to csv | save budget.csv8194 ```8295 96+## The `sheet` command97+98+The nushell module that comes with 012 ([Install the `sheet`99+command](README.md#install-the-sheet-command)) wraps both commands, so a100+pipeline needs neither `^012` nor NUON on either side:101+102+```nu103+ls | select name size | sheet | where size > 1kb104+ps | sheet view105+sheet budget.xlsx | save -f budget.csv106+```107+108+109+110+`sheet` is `to nuon | ^012 --pipe --to nuon | from nuon`: it opens the111+table piped in, or the file it names, and returns what you send back112+with its types. Text piped in, such as `open --raw data.csv`, goes to113+012 as it is, to be read as `012 -` reads it. `sheet view` is114+`to nuon | ^012 -`, and returns nothing. Quitting without sending makes115+`sheet` fail with 012's reason:116+117+```nu118+try { ls | sheet | save -f picked.nuon } catch {|e| print $e.msg }119+# quit without sending anything to the pipeline120+```121+83122 ## The exit status84123 85124 Quitting without sending (D at the question) writes nothing and exits@@ -95,25 +134,26 @@ try { ls | to nuon | ^012 --pipe | from nuon | save -f picked.nuon } catch { pri
95134 96135 ## Recipes97136 98-Each of these reads a table into 012; add `--pipe` and `| from nuon`99-to carry on with what you send back.137+Each of these reads a table into 012 with `sheet view`; `sheet`138+instead carries on with what you send back. Without the module, each139+`sheet view` is `to nuon | ^012 -`.100140 101141 ```nu102142 # Files, largest first: sizes in the Size format, dates as dates103-ls | sort-by size --reverse | to nuon | ^012 -143+ls | sort-by size --reverse | sheet view104144 105145 # Processes: CPU as numbers, memory as file sizes106-ps | select pid name cpu mem | to nuon | ^012 -146+ps | select pid name cpu mem | sheet view107147 108148 # Every CSV here, one table, with the file each row came from109-glob *.csv | each {|f| open $f | insert file ($f | path basename) } | flatten | to nuon | ^012 -149+glob *.csv | each {|f| open $f | insert file ($f | path basename) } | flatten | sheet view110150 111151 # An API's JSON, its dates made datetimes on the way112-http get https://api.github.com/repos/nushell/nushell/releases | select name published_at | update published_at { into datetime } | to nuon | ^012 -152+http get https://api.github.com/repos/nushell/nushell/releases | select name published_at | update published_at { into datetime } | sheet view113153 114154 # This computer: sys host is a record, which comes in as one row115-sys host | to nuon | ^012 -116-sys disks | select mount total free | to nuon | ^012 -155+sys host | sheet view156+sys disks | select mount total free | sheet | sort-by free117157 ```118158 119159 The commands are nushell's:@@ -9,10 +9,11 @@ Each [nushell type](https://www.nushell.sh/book/types_of_data.html)
99 becomes a cell of a kind a spreadsheet can work with, and goes back to1010 the same type. A file size is a number of bytes in the Size format, so1111 `=SUM` adds it and `where size > 1kb` still compares it once it's back12-in nushell:12+in nushell ([`sheet`](pipelines.md#the-sheet-command) is 012 as a stage13+of a pipeline):1314 1415 ```nu15-ls | select name size | to nuon | ^012 --pipe | from nuon | describe16+ls | select name size | sheet | describe1617 # table<name: string, size: filesize>1718 ```1819 @@ -0,0 +1,76 @@
1+package e2e2+3+import (4+ "os"5+ "os/exec"6+ "path/filepath"7+ "strings"8+ "testing"9+)10+11+// The nushell module (012 nu --install-module, then `sheet` in nu): nu12+// runs as a pipeline stage in the terminal, with 012 on its PATH, the13+// module installed where 012 puts it and used by the line 012 prints14+// for config.nu.15+16+// startSheet installs the module in a temporary nu config directory and17+// runs script in nu with it, standard output captured.18+func startSheet(t *testing.T, startsOn, script string) *session {19+ t.Helper()20+ needNu(t)21+ dir := t.TempDir()22+ xdg := filepath.Join(dir, ".config") // the session's own (configEnv)23+ install := exec.Command(binPath, "nu", "--install-module")24+ install.Env = append(os.Environ(), "XDG_CONFIG_HOME="+xdg)25+ out, err := install.CombinedOutput()26+ if err != nil {27+ t.Fatalf("installing the module: %v\n%s", err, out)28+ }29+ lines := strings.Split(strings.TrimSpace(string(out)), "\n")30+ config := filepath.Join(xdg, "nushell", "config.nu")31+ if err := os.WriteFile(config, []byte(lines[len(lines)-1]+"\n"), 0o644); err != nil {32+ t.Fatal(err)33+ }34+ path := "PATH=" + filepath.Dir(binPath) + string(os.PathListSeparator) + os.Getenv("PATH")35+ return startWith(t, options{dir: dir, piped: true, program: "nu", startsOn: startsOn, env: []string{path}},36+ "--config", config, "-c", script)37+}38+39+const sizesNu = "[[name size]; [a 2kb] [b 10b] [c 5mb]]"40+41+// A table goes through sheet: a selection is sent back, and nu gets it42+// with its types, file sizes as file sizes.43+func TestSheetSendsSelection(t *testing.T) {44+ s := startSheet(t, "5.0 MB", "let t = "+sizesNu+" | sheet; print ($t | get size | describe); $t | to nuon")45+ s.keys("<shift+down>", "<shift+down>", "<shift+right>")46+ s.waitFor("send A1:B3 as NUON")47+ s.keys("<ctrl+q>")48+ s.waitFor("Send A1:B3 as NUON?")49+ s.keys("<enter>")50+ out, code := s.finish()51+ if want := "list<filesize>\n[[name, size]; [a, 2000b], [b, 10b]]\n"; code != 0 || out != want {52+ t.Errorf("exit %d, stdout %q, want %q", code, out, want)53+ }54+}55+56+// Quitting without sending raises sheet's error, with 012's reason.57+func TestSheetNotSent(t *testing.T) {58+ s := startSheet(t, "5.0 MB", "try { "+sizesNu+" | sheet } catch {|e| print $'caught: ($e.msg)' }")59+ s.keys("<ctrl+q>", "d")60+ out, code := s.finish()61+ if want := "caught: quit without sending anything to the pipeline\n"; code != 0 || out != want {62+ t.Errorf("exit %d, stdout %q, want %q", code, out, want)63+ }64+}65+66+// sheet view shows the table and returns nothing.67+func TestSheetView(t *testing.T) {68+ s := startSheet(t, "5.0 MB", sizesNu+" | sheet view | describe")69+ s.keys("<ctrl+q>")70+ s.waitFor("You have unsaved changes.")71+ s.keys("d")72+ out, code := s.finish()73+ if code != 0 || out != "nothing\n" {74+ t.Errorf("exit %d, stdout %q", code, out)75+ }76+}@@ -0,0 +1,63 @@
1+# 012 in nushell pipelines: `sheet` opens the table piped to it in 0122+# and returns what you send back, with its types.3+#4+# Install: 012 nu --install-module, then add `use scripts/012.nu *` to5+# config.nu. See https://github.com/FelineStateMachine/012 (docs/nushell).6+7+# Open a table in 012, and return the table sent back on quitting.8+#9+# The table piped in opens as a sheet, or the file named instead. Edit10+# it, sort it or select a range; quitting sends the selection or the11+# whole sheet back as NUON, so file sizes, durations and dates come12+# back as themselves. Quitting without sending raises an error, so13+# `try` can catch it. Text piped in (CSV, TSV, JSON, NUON) is read as14+# 012 reads it.15+@example "Pick files in 012 and keep filtering" { ls | sheet | where size > 1kb }16+@example "Edit a workbook and save what you send back as CSV" { sheet budget.xlsx | save -f budget.csv }17+@example "Carry on when nothing is sent back" { try { ps | sheet } catch { [] } }18+@search-terms [spreadsheet table edit 012]19+export def sheet [20+ file?: path # open this workbook or file instead of the input21+] {22+ let input = $in23+ let args = if $file == null { [--pipe --to nuon] } else { [--pipe --to nuon $file] }24+ let r = if $input == null {25+ ^012 ...$args | complete26+ } else if ($input | describe) in [string binary] {27+ $input | ^012 ...$args | complete28+ } else {29+ $input | to nuon | ^012 ...$args | complete30+ }31+ if $r.exit_code != 0 {32+ let why = $r.stderr | str trim | str replace --regex '^012: ' ''33+ error make {34+ msg: (if $why == "" { $"012 exited with status ($r.exit_code)" } else { $why })35+ help: "nothing was sent back; quit with Enter or S to send a table"36+ }37+ }38+ $r.stdout | from nuon39+}40+41+# Look at a table in 012, then carry on with nothing.42+#43+# Like `sheet`, without sending anything back: the table stays in 01244+# until you save or download it.45+@example "Look at the processes" { ps | sheet view }46+@search-terms [spreadsheet table view 012]47+export def "sheet view" [] {48+ let input = $in49+ if ($input | describe) in [string binary] {50+ $input | ^012 -51+ } else {52+ $input | to nuon | ^012 -53+ }54+ null55+}56+57+# Open a nushell notebook in 012.58+@example "Open a notebook" { sheet nu work.012 }59+export def "sheet nu" [60+ file?: path # the workbook to open61+] {62+ if $file == null { ^012 nu } else { ^012 nu $file }63+}@@ -0,0 +1,15 @@
1+// Package module holds 012.nu, the nushell module that gives 012 the2+// command `sheet`, so nushell users needn't type `^012` (a bare 012 is a3+// number to nushell). `012 nu --module` prints it and `012 nu4+// --install-module` installs it (see docs/nushell/README.md).5+package module6+7+import _ "embed"8+9+// Source is the text of 012.nu.10+//11+//go:embed 012.nu12+var Source string13+14+// Name is the module's file name, and so its name in nushell.15+const Name = "012.nu"@@ -60,7 +60,7 @@ const cells: Cell[] = [
6060 },6161 {6262 addr: 'A3',63- entry: "'ls | to nuon | ^012 --pipe | from nuon",63+ entry: "'ls | sheet | where size > 1kb",6464 hint: 'Nushell: pipelines, notebooks, types, a cookbook',6565 title: 'Nushell, both ways',6666 body: "A stage in a nushell pipeline with file sizes, durations and dates kept, or a notebook where each pipeline's table is a live, named region that formulas and charts read.",