--send selection sends the selection, or the sheet while only the active cell is selected (as Enter at the question); --send sheet sends the used range whatever is selected. The status line names what Ctrl+Q sends. File > Quit without sending (and :q!) quits with status 1. --send is refused without --pipe. The nushell module's sheet takes --send selection|sheet and passes it on.
- commit
- 7dca66ce8520ec0b579b4040895424889ec26fb9
- parent
- 41ba2bbe4ab8e2ef74b8b44e3f1984bf80816261
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.5 of 3, Jev's estimate
| M | cmd/ | 2 | +- |
| M | cmd/ | 78 | +++++++++++++++++++++--------- |
| M | cmd/ | 32 | ++++++++++++- |
| M | docs/ | 2 | +- |
| M | docs/ | 35 | ++++++++++++-- |
| M | docs/ | 4 | +- |
| M | e2e/ | 15 | ++++++ |
| M | e2e/ | 39 | +++++++++++++++ |
| M | internal/ | 26 | +++++++--- |
| M | internal/ | 6 | +- |
| M | internal/ | 2 | +- |
| M | internal/ | 52 | +++++++++++++++++--- |
| M | internal/ | 67 | ++++++++++++++++++++++++-- |
@@ -66,7 +66,7 @@ func usage() error {
6666 return errors.New("usage: 012 " + config.FlagUsage() + " [file]: a " + sheet.FileExt +6767 " sheet, or a .csv, .tsv, .json, .nuon, .xlsx, .sqlite, .parquet or .wk1 file to import\n" +6868 " 012 [flags] -: a table from standard input (NUON, JSON, CSV or TSV)\n" +69- " 012 [flags] --pipe [--to nuon|json|csv|tsv] [file]: on quitting, send the table to standard output\n" +69+ " 012 [flags] --pipe [--to nuon|json|csv|tsv] [--send ask|selection|sheet] [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" +7171 " 012 nu --module | --install-module [--force] [path]: the nushell module with sheet (see docs/nushell/README.md)\n" +7272 " 012 serve [flags] [dir]: serve sheets in dir over SSH (see docs/terminal/ssh.md)\n" +@@ -20,11 +20,16 @@ import (
2020 2121 // pipeMode is what the command line asked of standard input and output.2222 type pipeMode struct {23- stdin bool // 012 -, or --pipe without a file: read a table from standard input24- pipe bool // --pipe: write the result to standard output on quitting25- to fileio.Kind // --to: the format to write, or 0 for the input's23+ stdin bool // 012 -, or --pipe without a file: read a table from standard input24+ pipe bool // --pipe: write the result to standard output on quitting25+ to fileio.Kind // --to: the format to write, or 0 for the input's26+ send ui.Send // --send: what quitting sends, or ask27+ sendAs string // --send's value, or "" without it2628 }2729 30+// sends are --send's values.31+var sends = map[string]ui.Send{"ask": ui.SendAsk, "selection": ui.SendSelection, "sheet": ui.SendSheet}32+2833 // on reports whether the UI must run on the terminal rather than on2934 // standard input and output.3035 func (p pipeMode) on() bool { return p.stdin || p.pipe }@@ -33,43 +38,67 @@ func (p pipeMode) on() bool { return p.stdin || p.pipe }
3338 // status 1 so the pipeline can tell.3439 var errNotSent = errors.New("quit without sending anything to the pipeline")3540 36-// parsePipe takes -, --pipe and --to from args, returning the rest.41+// parsePipe takes -, --pipe, --to and --send from args, returning the42+// rest.3743 func parsePipe(args []string) (pipeMode, []string, error) {3844 var p pipeMode3945 var rest []string4046 for i := 0; i < len(args); i++ {4147 name, value, hasValue := strings.Cut(args[i], "=")42- switch {43- case args[i] == "-":44- p.stdin = true45- case args[i] == "--pipe":46- p.pipe = true47- case name == "--to":48- if !hasValue {49- if i+1 == len(args) {50- return p, nil, errors.New("--to needs a format: nuon, json, csv or tsv")51- }52- i++53- value = args[i]48+ if name != "--to" && name != "--send" {49+ switch args[i] {50+ case "-":51+ p.stdin = true52+ case "--pipe":53+ p.pipe = true54+ default:55+ rest = append(rest, args[i])5456 }55- k, ok := fileio.KindNamed(value)56- if !ok || !k.IsText() {57- return p, nil, fmt.Errorf("--to %s: 012 writes nuon, json, csv or tsv", value)57+ continue58+ }59+ if !hasValue {60+ if i+1 == len(args) {61+ return p, nil, fmt.Errorf("%s needs a value: %s", name, pipeValues[name])5862 }59- p.to = k60- default:61- rest = append(rest, args[i])63+ i++64+ value = args[i]65+ }66+ if err := p.set(name, value); err != nil {67+ return p, nil, err6268 }6369 }6470 return p, rest, nil6571 }6672 67-// check reports what doesn't go together: --to without --pipe, a file68-// and -, or nothing to read.73+// pipeValues are what --to and --send take, for their errors.74+var pipeValues = map[string]string{"--to": "nuon, json, csv or tsv", "--send": "ask, selection or sheet"}75+76+// set takes --to's or --send's value.77+func (p *pipeMode) set(name, value string) error {78+ if name == "--send" {79+ s, ok := sends[value]80+ if !ok {81+ return fmt.Errorf("--send %s: 012 sends %s", value, pipeValues[name])82+ }83+ p.send, p.sendAs = s, value84+ return nil85+ }86+ k, ok := fileio.KindNamed(value)87+ if !ok || !k.IsText() {88+ return fmt.Errorf("--to %s: 012 writes %s", value, pipeValues[name])89+ }90+ p.to = k91+ return nil92+}93+94+// check reports what doesn't go together: --to or --send without95+// --pipe, a file and -, or nothing to read.6996 func (p *pipeMode) check(files []string, stdinTTY bool) error {7097 switch {7198 case p.to != 0 && !p.pipe:7299 return errors.New("--to goes with --pipe")100+ case p.sendAs != "" && !p.pipe:101+ return errors.New("--send goes with --pipe: 012 --pipe --send " + p.sendAs)73102 case p.stdin && len(files) > 0:74103 return errors.New("012 - reads standard input; name a file or -, not both")75104 case p.pipe && len(files) == 0:@@ -103,6 +132,7 @@ func setPipe(m *ui.Model, p pipeMode, stdin io.Reader) {
103132 }104133 if p.pipe {105134 m.SetPipe(p.to)135+ m.SetSend(p.send)106136 }107137 }108138 @@ -15,6 +15,7 @@ import (
1515 1616 var (1717 ctrlQ = tea.KeyPressMsg{Code: 'q', Mod: tea.ModCtrl}18+ down = tea.KeyPressMsg{Code: tea.KeyDown, Mod: tea.ModShift}1819 enter = tea.KeyPressMsg{Code: tea.KeyEnter}1920 keyD = tea.KeyPressMsg{Code: 'd', Text: "d"}2021 )@@ -129,6 +130,31 @@ func TestPipeTo(t *testing.T) {
129130 }130131 }131132 133+// TestPipeSend: with --send, Ctrl+Q sends without asking: the134+// selection, or the sheet whatever is selected.135+func TestPipeSend(t *testing.T) {136+ for _, tc := range []struct {137+ args []string138+ want string139+ }{140+ {[]string{"--pipe", "--send", "selection", "--to", "csv"}, "name\nCLAUDE.md\n"},141+ {[]string{"--pipe", "--send=sheet", "--to", "csv"}, "name,size\nCLAUDE.md,1.6 kB\ngo.mod,1.5 kB\n"},142+ } {143+ e, out, _ := pipeEnv(t, lsNUON, down, ctrlQ)144+ if err := run(tc.args, e); err != nil {145+ t.Fatal(err)146+ }147+ if out.String() != tc.want {148+ t.Errorf("%q: stdout %q, want %q", tc.args, out, tc.want)149+ }150+ }151+ // Only one cell selected: --send selection sends the sheet.152+ e, out, _ := pipeEnv(t, lsNUON, ctrlQ)153+ if err := run([]string{"--pipe", "--send", "selection", "--to", "tsv"}, e); err != nil || out.String() != "name\tsize\nCLAUDE.md\t1.6 kB\ngo.mod\t1.5 kB\n" {154+ t.Errorf("one cell: %v, stdout %q", err, out)155+ }156+}157+132158 func TestPipeQuitWithoutSending(t *testing.T) {133159 e, out, _ := pipeEnv(t, lsNUON, ctrlQ, keyD)134160 if err := run([]string{"--pipe"}, e); !errors.Is(err, errNotSent) {@@ -175,8 +201,12 @@ func TestPipeArguments(t *testing.T) {
175201 {[]string{"--pipe"}, true, "nothing piped in"},176202 {[]string{"--to", "csv"}, false, "--to goes with --pipe"},177203 {[]string{"--pipe", "--to", "xlsx"}, false, "012 writes nuon, json, csv or tsv"},178- {[]string{"--pipe", "--to"}, false, "--to needs a format"},204+ {[]string{"--pipe", "--to"}, false, "--to needs a value: nuon, json, csv or tsv"},179205 {[]string{"-", "a.csv"}, false, "not both"},206+ {[]string{"-", "--send", "sheet"}, false, "--send goes with --pipe: 012 --pipe --send sheet"},207+ {[]string{"--send=ask", "a.csv"}, false, "--send goes with --pipe"},208+ {[]string{"--pipe", "--send", "all"}, false, "--send all: 012 sends ask, selection or sheet"},209+ {[]string{"--pipe", "--send"}, false, "--send needs a value: ask, selection or sheet"},180210 } {181211 e, _, _ := pipeEnv(t, lsNUON)182212 e.isTTY = tc.tty@@ -46,7 +46,7 @@ In a new nushell, `help sheet` describes it:
4646 4747 | Command | Does |4848 |---|---|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)) |49+| `sheet` | Opens the table piped in (or a file: `sheet budget.xlsx`) and returns what you send back, with its types; `--send selection` or `--send sheet` sends it on quitting without asking ([Pipelines](pipelines.md#the-sheet-command)) |5050 | `sheet view` | Opens the table piped in and returns nothing |5151 | `sheet nu` | Opens a [notebook](notebooks.md), as `012 nu` |5252 @@ -43,7 +43,7 @@ sequenceDiagram
4343 | Command | Reads | Writes |4444 |---|---|---|4545 | `012 -` | A table from standard input, into a new sheet | Nothing; the table stays in 012 until you save or download it |46-| `012 --pipe` | A table from standard input, or a file named after it | The sheet or the selection, to standard output, when you quit |46+| `012 --pipe` | A table from standard input, or a file named after it | The sheet or the selection, to standard output, when you quit (asking which, unless `--send` says) |4747 | `sheet`, `sheet view` | As `012 --pipe` and `012 -`, from nushell values | A nushell table, or nothing |4848 4949 ## Reading a table: `012 -`@@ -77,8 +77,26 @@ standard output:
7777 one is made, or else the sheet; S sends the whole sheet instead; D7878 quits without sending; Esc goes back to the sheet.7979 - File > Quit and send selection and File > Quit and send sheet send80- without asking. They're in the menus and the palette only with81- `--pipe`.80+ without asking, and File > Quit without sending (or `:q!`) sends81+ nothing. They're in the menus and the palette only with `--pipe`.82+83+`--send` makes Quit send without asking, for a pipeline that always84+wants the same thing back:85+86+| `--send` | Quit sends |87+|---|---|88+| `ask` (the default) | What you choose at the question above |89+| `selection` | The selection; with only the active cell selected, the sheet, as Enter at the question does |90+| `sheet` | The sheet, whatever is selected |91+92+```nu93+open orders.csv | to nuon | ^012 --pipe --send sheet | from nuon | save -f orders.nuon94+```95+96+The status line still names what Ctrl+Q will send, and File > Quit97+without sending stays in the menu. The active cell on its own isn't a98+selection to send, as at the question: `--send selection` sends the99+sheet until a range is selected.82100 83101 The sheet is its used range from A1. A selection is sent as selected,84102 and its first row names the columns. Either way, rows a filter hides@@ -109,7 +127,13 @@ sheet budget.xlsx | save -f budget.csv
109127 110128 `sheet` is `to nuon | ^012 --pipe --to nuon | from nuon`: it opens the111129 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 to130+with its types. `sheet --send sheet` and `sheet --send selection` pass131+`--send` on, so quitting sends without asking:132+133+```nu134+ls | sheet --send sheet | where size > 1kb135+```136+ Text piped in, such as `open --raw data.csv`, goes to113137 012 as it is, to be read as `012 -` reads it. `sheet view` is114138 `to nuon | ^012 -`, and returns nothing. Quitting without sending makes115139 `sheet` fail with 012's reason:@@ -121,7 +145,8 @@ try { ls | sheet | save -f picked.nuon } catch {|e| print $e.msg }
121145 122146 ## The exit status123147 124-Quitting without sending (D at the question) writes nothing and exits148+Quitting without sending (D at the question, File > Quit without149+sending, `:q!`) writes nothing and exits125150 with status 1, so the pipeline stops rather than carrying on with126151 nothing. Nushell treats that127152 as a failed external command and stops the pipeline with an error@@ -113,7 +113,7 @@ range, F2 renames or repoints it and Ctrl+D deletes it.
113113 114114 | Key | Action |115115 |---|---|116-| Ctrl+S, Ctrl+O, Ctrl+Q | Save, open (and import other formats), quit |116+| Ctrl+S, Ctrl+O, Ctrl+Q | Save, open (and import other formats), quit; with `012 --pipe`, quitting asks what to send, or sends what `--send` chose ([Pipelines](../nushell/pipelines.md#sending-it-on-012---pipe)) |117117 | F10, Alt+F, Alt+E, Alt+V, Alt+I, Alt+O, Alt+D, Alt+H | Open a menu; arrows move, Enter runs, Esc closes |118118 | Ctrl+K, Alt+/, Ctrl+Shift+P | Search the menus: find and run any command |119119 | Shift+F10, right-click | The cell, column or row menu |@@ -231,7 +231,7 @@ The `:` line takes:
231231 | `:40` | Go to row 40 |232232 | `:w`, `:w name`, `:w out.csv` | Save, Save as, or Download as another format, as the File menu does |233233 | `:w!`, `:w! name`, `:wq!` | Save even if the file changed on disk since it was opened, and replace a file of the name given, without asking (as choosing Overwrite or Replace would) |234-| `:q`, `:q!` | Quit (asking about unsaved changes), quit discarding them |234+| `:q`, `:q!` | Quit (asking about unsaved changes), quit discarding them; with `012 --pipe`, `:q` quits as Ctrl+Q does and `:q!` quits without sending |235235 | `:wq`, `:x` | Save and quit; `:x` saves only if something changed |236236 | `:e name`, `:e!` | Open a sheet or import a file; refused with unsaved changes unless `:e!` |237237 | `:edit.fill_down`, `:fill down` | Any command, by its id or title |@@ -53,6 +53,21 @@ func TestSheetSendsSelection(t *testing.T) {
5353 }5454 }5555 56+// sheet --send sheet gives back all of ls's table on Ctrl+Q, whatever57+// is selected.58+func TestSheetSendSheet(t *testing.T) {59+ s := startSheet(t, "b.txt", "cd (mktemp -d); 'x' | save a.txt; 'yyy' | save b.txt; "+60+ "let t = ls | sheet --send sheet; print ($t | columns | to nuon); "+61+ "print (($t | select name type size) == (ls | select name type size)); print ($t | length)")62+ s.keys("<shift+down>")63+ s.waitFor("Ctrl+Q send the sheet as NUON")64+ s.keys("<ctrl+q>")65+ out, code := s.finish()66+ if want := "[name, type, size, modified]\ntrue\n2\n"; code != 0 || out != want {67+ t.Errorf("exit %d, stdout %q, want %q", code, out, want)68+ }69+}70+5671 // Quitting without sending raises sheet's error, with 012's reason.5772 func TestSheetNotSent(t *testing.T) {5873 s := startSheet(t, "5.0 MB", "try { "+sizesNu+" | sheet } catch {|e| print $'caught: ($e.msg)' }")@@ -105,6 +105,45 @@ func TestPipeSendSelectionAsCSV(t *testing.T) {
105105 }106106 }107107 108+// TestPipeSendSelectionWithoutAsking: --send selection makes Ctrl+Q109+// send the selection at once.110+func TestPipeSendSelectionWithoutAsking(t *testing.T) {111+ s := startWith(t, options{piped: true, stdin: lsNUON}, "--pipe", "--send", "selection", "--to", "csv")112+ s.waitFor("Ctrl+Q send the sheet as CSV")113+ s.keys("<shift+down>", "<shift+right>")114+ s.waitFor("Ctrl+Q send A1:B2 as CSV")115+ s.keys("<ctrl+q>")116+ out, code := s.finish()117+ if code != 0 || out != "name,size\nCLAUDE.md,1.6 kB\n" {118+ t.Errorf("exit %d, stdout %q", code, out)119+ }120+}121+122+// TestPipeSendSheetWithoutAsking: --send sheet makes Ctrl+Q send the123+// whole sheet, whatever is selected; File > Quit without sending still124+// sends nothing.125+func TestPipeSendSheetWithoutAsking(t *testing.T) {126+ s := startWith(t, options{piped: true, stdin: "a,b\n1,2\n3,4\n"}, "--pipe", "--send", "sheet")127+ s.waitFor("Read 3 rows of CSV from standard input")128+ s.keys("<shift+down>")129+ s.waitFor("Ctrl+Q send the sheet as CSV")130+ s.keys("<ctrl+q>")131+ out, code := s.finish()132+ if code != 0 || out != "a,b\n1,2\n3,4\n" {133+ t.Errorf("exit %d, stdout %q", code, out)134+ }135+136+ s = startWith(t, options{piped: true, stdin: "a,b\n1,2\n"}, "--pipe", "--send", "sheet")137+ s.waitFor("Read 2 rows of CSV from standard input")138+ s.keys("<ctrl+k>", "without sending")139+ s.waitFor("Quit without sending")140+ s.keys("<enter>")141+ out, code = s.finish()142+ if code != 1 || out != "" {143+ t.Errorf("quit without sending: exit %d, stdout %q", code, out)144+ }145+}146+108147 // TestPipeQuitWithoutSending exits with status 1 and writes nothing.109148 func TestPipeQuitWithoutSending(t *testing.T) {110149 s := startWith(t, options{piped: true, stdin: "a,b\n1,2\n"}, "--pipe")@@ -7,20 +7,32 @@
77 # Open a table in 012, and return the table sent back on quitting.88 #99 # 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.10+# it, sort it or select a range; quitting asks whether to send the11+# selection or the whole sheet back as NUON, so file sizes, durations12+# and dates come back as themselves. With --send, quitting sends13+# without asking. Quitting without sending raises an error, so `try`14+# can catch it. Text piped in (CSV, TSV, JSON, NUON) is read as 01215+# reads it.1516 @example "Pick files in 012 and keep filtering" { ls | sheet | where size > 1kb }17+@example "Edit the table and get all of it back when you quit" { ls | sheet --send sheet }18+@example "Get back the range selected when you quit, its first row naming the columns" { ls | select name size | sheet --send selection }1619 @example "Edit a workbook and save what you send back as CSV" { sheet budget.xlsx | save -f budget.csv }1720 @example "Carry on when nothing is sent back" { try { ps | sheet } catch { [] } }1821 @search-terms [spreadsheet table edit 012]1922 export def sheet [2023 file?: path # open this workbook or file instead of the input24+ --send: string # send without asking on quitting: selection (the sheet when one cell is selected) or sheet2125 ] {2226 let input = $in23- let args = if $file == null { [--pipe --to nuon] } else { [--pipe --to nuon $file] }27+ if $send != null and $send not-in [selection sheet] {28+ error make {29+ msg: $"--send ($send): sheet sends selection or sheet"30+ label: {text: "selection or sheet", span: (metadata $send).span}31+ }32+ }33+ let send_args = if $send == null { [] } else { [--send $send] }34+ let file_args = if $file == null { [] } else { [$file] }35+ let args = [--pipe --to nuon ...$send_args ...$file_args]2436 let r = if $input == null {2537 ^012 ...$args | complete2638 } else if ($input | describe) in [string binary] {@@ -32,7 +44,7 @@ export def sheet [
3244 let why = $r.stderr | str trim | str replace --regex '^012: ' ''3345 error make {3446 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"47+ help: (if $send == null { "nothing was sent back; quit with Enter or S to send a table" } else { $"nothing was sent back; Ctrl+Q sends the ($send)" })3648 }3749 }3850 $r.stdout | from nuon@@ -32,11 +32,11 @@ func (m *Model) ask(q question) {
3232 }3333 3434 // quit exits, asking first when there are unsaved changes. Enter saves35-// and quits, so the quick path never loses work. In a pipeline it asks36-// what to send instead (askSend).35+// and quits, so the quick path never loses work. In a pipeline it sends36+// a table instead, or asks which (quitPiped).3737 func (m *Model) quit() tea.Cmd {3838 if m.pipe.on {39- return m.askSend()39+ return m.quitPiped()4040 }4141 if !m.changed {4242 return m.exit()@@ -39,7 +39,7 @@ var menuBar = []menuDef{
3939 {cmd: "settings.theme"}, {cmd: "settings.jev_key"}, sep,4040 {cmd: "settings.config_edit"}, {cmd: "settings.config_reload"},4141 }}, sep,42- {cmd: "pipe.send_selection"}, {cmd: "pipe.send_sheet"}, {cmd: "quit"},42+ {cmd: "pipe.send_selection"}, {cmd: "pipe.send_sheet"}, {cmd: "pipe.quit_unsent"}, {cmd: "quit"},4343 }},4444 {title: "Edit", accel: 'e', items: []menuItem{4545 {cmd: "edit.undo"}, {cmd: "edit.redo"}, sep,@@ -16,8 +16,9 @@ import (
1616 // table to standard output when it quits: the selection or the sheet,1717 // as the user chooses, in the input's format unless another was asked1818 // for. The screen is the terminal's own (cmd/012 opens it), so nothing19-// else reaches standard output. Quitting asks what to send, and20-// quitting without sending lets the pipeline know by the exit status.19+// else reaches standard output. Quitting asks what to send, or sends20+// what --send chose without asking, and quitting without sending lets21+// the pipeline know by the exit status.2122 2223 // StdinName is what a table read from standard input is called: its2324 // sheet's name.@@ -27,8 +28,9 @@ const StdinName = "stdin"
2728 type pipeState struct {2829 in io.Reader // standard input, read as the program starts; nil once read2930 30- on bool // --pipe: quitting writes a table to standard output31- to fileio.Kind // the format asked for (--to), or 0 for the input's31+ on bool // --pipe: quitting writes a table to standard output32+ to fileio.Kind // the format asked for (--to), or 0 for the input's33+ send Send // what quitting sends (--send)3234 3335 // Set by a quit that sends: what to write, and in which format.3436 sent *fileio.Snapshot@@ -43,6 +45,18 @@ func (m *Model) ReadStdin(r io.Reader) { m.pipe.in = r }
4345 // (a text format; 0 for the input's, or NUON when the input isn't text).4446 func (m *Model) SetPipe(to fileio.Kind) { m.pipe.on, m.pipe.to = true, to }4547 48+// Send is what quitting sends in a pipeline (012 --pipe --send).49+type Send int50+51+const (52+ SendAsk Send = iota // ask: the selection, the sheet, or nothing53+ SendSelection // the selection, or the sheet when only one cell is selected54+ SendSheet // the sheet, whatever is selected55+)56+57+// SetSend makes quitting in a pipeline send s rather than ask.58+func (m *Model) SetSend(s Send) { m.pipe.send = s }59+4660 // Piped is what quitting sent: the table and its format, or false when4761 // the user quit without sending.4862 func (m *Model) Piped() (*fileio.Snapshot, fileio.Kind, bool) {@@ -61,6 +75,10 @@ func init() {
6175 enabled: func(m *Model) bool { return m.pipe.on },6276 hidden: func(m *Model) bool { return !m.pipe.on },6377 run: func(m *Model) tea.Cmd { return m.send(sheet.Rect{}) }},78+ &command{id: "pipe.quit_unsent", macro: macroNever, title: "Quit without sending",79+ desc: "Quit, writing nothing to standard output: the pipeline sees exit status 1",80+ hidden: func(m *Model) bool { return !m.pipe.on },81+ run: (*Model).exit},6482 )6583 }6684 @@ -98,16 +116,34 @@ func (m *Model) pipeKind() fileio.Kind {
98116 return fileio.NUON99117 }100118 119+// sendRange is what quitting would send: the selection, when a range120+// is selected and --send doesn't ask for the sheet, or else the sheet121+// (the zero Rect).122+func (m *Model) sendRange() sheet.Rect {123+ if m.pipe.send != SendSheet && m.hasRange() {124+ return m.selection()125+ }126+ return sheet.Rect{}127+}128+101129 // sendWhat names what quitting would send: "B2:D9" or "the sheet".102130 func (m *Model) sendWhat() string {103- if m.hasRange() {104- return m.selection().String()131+ if r := m.sendRange(); r != (sheet.Rect{}) {132+ return r.String()105133 }106134 return "the sheet"107135 }108136 109-// askSend is Quit in a pipeline: send the selection, if there is one,110-// or the sheet, or quit without sending.137+// quitPiped is Quit in a pipeline: send what --send chose, or ask.138+func (m *Model) quitPiped() tea.Cmd {139+ if m.pipe.send == SendAsk {140+ return m.askSend()141+ }142+ return m.send(m.sendRange())143+}144+145+// askSend asks what to send: the selection, if there is one, or the146+// sheet, or nothing.111147 func (m *Model) askSend() tea.Cmd {112148 var choices []choice113149 if m.hasRange() {@@ -104,8 +104,66 @@ func TestPipeQuitWithoutSending(t *testing.T) {
104104 }105105 }106106 107-// TestPipeCommands: the send commands are in File and the palette only108-// in a pipeline, and run from there.107+// TestPipeSendSelection: with --send selection, Quit sends the108+// selection without asking, or the sheet when only one cell is109+// selected, and the status line says which.110+func TestPipeSendSelection(t *testing.T) {111+ m := stdinModel(t, filesNUON, true, 0)112+ m.SetSend(SendSelection)113+ if st := line(m, m.height-1); !strings.Contains(st, "Ctrl+Q send the sheet as NUON") {114+ t.Errorf("status line %q", st)115+ }116+ press(t, m, "<shift+down>", "<shift+right>")117+ if st := line(m, m.height-1); !strings.Contains(st, "Ctrl+Q send A1:B2 as NUON") {118+ t.Errorf("status line %q", st)119+ }120+ if _, quit := press(t, m, "<ctrl+q>").(tea.QuitMsg); !quit {121+ t.Fatal("asked rather than quit")122+ }123+ if snap, _, ok := m.Piped(); !ok || snap.Range.String() != "A1:B2" {124+ t.Errorf("sent %v %v", snap.Range, ok)125+ }126+127+ m = stdinModel(t, filesNUON, true, 0)128+ m.SetSend(SendSelection)129+ press(t, m, "<down>")130+ cmd, _ := m.runCmdLine("q")131+ if _, quit := run(m, cmd).(tea.QuitMsg); !quit {132+ t.Fatal(":q asked rather than quit")133+ }134+ if snap, _, ok := m.Piped(); !ok || snap.Range.String() != "A1:B3" {135+ t.Errorf("one cell selected sent %v %v", snap.Range, ok)136+ }137+}138+139+// TestPipeSendSheet: with --send sheet, Quit sends the sheet whatever140+// is selected, and Quit without sending is still in the palette.141+func TestPipeSendSheet(t *testing.T) {142+ m := stdinModel(t, filesNUON, true, fileio.CSV)143+ m.SetSend(SendSheet)144+ press(t, m, "<shift+down>")145+ if st := line(m, m.height-1); !strings.Contains(st, "Ctrl+Q send the sheet as CSV") {146+ t.Errorf("status line %q", st)147+ }148+ if _, quit := run(m, m.runCommand("quit")).(tea.QuitMsg); !quit {149+ t.Fatal("File > Quit asked rather than quit")150+ }151+ if snap, k, ok := m.Piped(); !ok || k != fileio.CSV || snap.Range.String() != "A1:B3" {152+ t.Errorf("sent %v %v %v", snap.Range, k, ok)153+ }154+155+ m = stdinModel(t, filesNUON, true, 0)156+ m.SetSend(SendSheet)157+ if _, quit := run(m, m.runCommand("pipe.quit_unsent")).(tea.QuitMsg); !quit {158+ t.Fatal("Quit without sending didn't quit")159+ }160+ if _, _, ok := m.Piped(); ok {161+ t.Error("Quit without sending sent")162+ }163+}164+165+// TestPipeCommands: the send commands and Quit without sending are in166+// File and the palette only in a pipeline, and run from there.109167 func TestPipeCommands(t *testing.T) {110168 titles := func(m *Model) []string {111169 var out []string@@ -115,11 +173,12 @@ func TestPipeCommands(t *testing.T) {
115173 return out116174 }117175 m := newModel()118- if slices.Contains(titles(m), "Quit and send sheet") || len(m.applicable(menuBar[0].items)) != len(menuBar[0].items)-2 {176+ if slices.Contains(titles(m), "Quit and send sheet") || len(m.applicable(menuBar[0].items)) != len(menuBar[0].items)-3 {119177 t.Error("send commands outside a pipeline")120178 }121179 m = stdinModel(t, filesNUON, true, 0)122- if !slices.Contains(titles(m), "Quit and send sheet") || !slices.Contains(titles(m), "Quit and send selection") {180+ if !slices.Contains(titles(m), "Quit and send sheet") || !slices.Contains(titles(m), "Quit and send selection") ||181+ !slices.Contains(titles(m), "Quit without sending") {123182 t.Error("send commands missing in a pipeline")124183 }125184 if commands["pipe.send_selection"].available(m) {