cat / 012
clone

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

main
012 / internal / ui / nbjob.go
nbjob.go 4.2K
1package ui23import (4 "bytes"5 "context"6 "errors"7 "fmt"8 "slices"9 "time"1011 tea "charm.land/bubbletea/v2"1213 "src.rest/cat/012/internal/fileio"14 "src.rest/cat/012/internal/notebook"15 "src.rest/cat/012/internal/nushell"16 "src.rest/cat/012/internal/sheet"17)1819// What a cell's run reads: the outputs of the cells it names ($sales),20// linked files ($app), the selection on the sheet shown last21// ($selection) and ranges of sheets ($sheet.A1:C9), each handed to nu as22// NUON, never spliced into the pipeline.2324// jobFor is what running cell c takes: the outputs, variables, linked25// files and ranges it reads, as NUON. As a stream, the cell hands back26// no variables but its output (notebook.Source.StreamCommand).27func (m *Model) jobFor(s *sheet.Sheet, c notebook.Cell, run *nbRun, stream bool) (nushell.Job, error) {28 cells := s.NotebookCells()29 i := slices.IndexFunc(cells, func(x notebook.Cell) bool { return x.ID == c.ID })30 p, err := notebook.Prepare(cells, i, m.book().Output)31 job := nushell.Job{Command: p.Command, Vars: p.Exports, Tables: p.Tables, Config: m.configBool("nu-config", false)}32 if stream {33 job.Command, job.Vars = p.Stream, nil34 }35 run.reads = p.Reads36 if err != nil {37 return job, err38 }39 for _, name := range p.Others {40 if t, r, ok := m.book().Region(name); ok && r.Linked() && r.Name == name {41 data, err := regionNUON(t, r)42 if err != nil {43 return job, err44 }45 job.Tables[name] = data46 }47 }48 if p.Selection {49 if err := m.bindSelection(&job, run); err != nil {50 return job, err51 }52 }53 for _, ref := range p.Ranges {54 t, rng, err := m.rangeRef(ref.Ref)55 if err != nil {56 return job, err57 }58 data, err := encodeNUON(fileio.Snap(t, rng, t.Name()))59 if err != nil {60 return job, err61 }62 job.Tables[ref.Var] = data63 }64 return job, nil65}6667// bindSelection gives the job the selection on the sheet shown last.68func (m *Model) bindSelection(job *nushell.Job, run *nbRun) error {69 t := m.nb.grid70 if t == nil || m.book().Index(t) < 0 {71 return errors.New("it reads $selection: select a range on a sheet first")72 }73 data, err := encodeNUON(fileio.Snap(t, m.nb.sel, t.Name()))74 if err != nil {75 return err76 }77 job.Tables[notebook.Selection] = data78 run.selection = sheet.Qualified(t.Name(), m.nb.sel)79 return nil80}8182// rangeRef is the sheet and range $sheet.ref reads: a range of the83// sheet shown last, or of the sheet it names.84func (m *Model) rangeRef(ref string) (*sheet.Sheet, sheet.Rect, error) {85 name, cells := sheet.SplitSheet(ref)86 t := m.nb.grid87 if name != "" {88 t = m.book().Lookup(name)89 }90 if t == nil || t.IsNotebook() {91 for _, s := range m.book().Sheets() {92 if !s.IsNotebook() && name == "" {93 t = s94 break95 }96 }97 }98 rng, ok := sheet.ParseRange(cells)99 if t == nil || t.IsNotebook() || !ok {100 return nil, sheet.Rect{}, fmt.Errorf("$sheet.%s isn't a range of a sheet", ref)101 }102 return t, rng, nil103}104105// regionNUON is a linked file's table as NUON, read whole whatever its106// filter shows.107func regionNUON(t *sheet.Sheet, r sheet.Region) ([]byte, error) {108 rng, ok := t.RegionTable(r.Name)109 if !ok {110 rng = sheet.Rect{From: r.At, To: r.At}111 }112 snap := fileio.Snap(t, rng, r.Name)113 snap.HiddenRows = nil114 if !ok {115 snap.Cells = nil116 }117 return encodeNUON(snap)118}119120func encodeNUON(snap *fileio.Snapshot) ([]byte, error) {121 var b bytes.Buffer122 if _, err := fileio.Encode(&b, fileio.NUON, snap); err != nil {123 return nil, err124 }125 return b.Bytes(), nil126}127128// loadWords asks nu for its command names, once a session, for129// completing them.130func (m *Model) loadWords() tea.Cmd {131 if m.nb.asked || m.shellOff() != "" {132 return nil133 }134 m.nb.asked = true135 runner := m.runner()136 return func() tea.Msg {137 words, _ := nushell.Commands(context.Background(), runner, 30*time.Second)138 return nbWordsMsg{words}139 }140}141142// Config helpers that work without a config, as in tests.143144func (m *Model) configString(name, def string) string {145 if c := m.prefs.Config; c != nil {146 return c.String(name)147 }148 return def149}150151func (m *Model) configBool(name string, def bool) bool {152 if c := m.prefs.Config; c != nil {153 return c.Bool(name)154 }155 return def156}157158func (m *Model) configDuration(name string, def time.Duration) time.Duration {159 if c := m.prefs.Config; c != nil {160 return c.Duration(name)161 }162 return def163}