cat / 012
clone

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

main
012 / internal / sheet / notebookfile.go
notebookfile.go 6.0K
1package sheet23import (4 "bufio"5 "encoding/json"6 "fmt"7 "strings"89 "src.rest/cat/012/internal/notebook"10)1112// A notebook tab in the file: a sheet marked "tab": "notebook", its13// cells one per line, each with its output as NUON text when the caps14// (OutputCaps) keep it, or "unsaved" when they don't. None of it needs a15// version: earlier builds ignore the fields and open the tab as an16// empty sheet.17//18// "tab": "notebook",19// "reactive": true,20// "notebookCells": [21// {"kind": "note", "source": "# Sales"},22// {"source": "sales = open sales.csv", "output": "[[Region, Units]; [North, 120]]"},23// {"source": "$sales | where Units > 500", "error": "Column not found", "detail": "help: ..."},24// {"source": "ls **/*", "unsaved": true}25// ]26type fileNotebookCell struct {27 Kind string `json:"kind,omitempty"`28 Source string `json:"source"`29 Output string `json:"output,omitempty"`30 Error string `json:"error,omitempty"`31 Detail string `json:"detail,omitempty"`32 Note string `json:"note,omitempty"`33 Unsaved bool `json:"unsaved,omitempty"`34}3536// notebookTab is what "tab" says of a notebook.37const notebookTab = "notebook"3839// maxSavedError caps what a file keeps of a failed run's message.40const maxSavedError = 4 << 104142// writeNotebook writes a notebook tab's fields.43func (s *Sheet) writeNotebook(b *bufio.Writer, indent string) error {44 if !s.regions.notebook {45 return nil46 }47 b.WriteString(",\n" + indent + `"tab": "notebook"`)48 if s.regions.reactive {49 b.WriteString(",\n" + indent + `"reactive": true`)50 }51 if len(s.regions.cells) == 0 {52 return nil53 }54 w := s.wb55 kept := notebook.Kept(s.regions.cells, w.Output, w.OutputCaps())56 b.WriteString(",\n" + indent + `"notebookCells": [`)57 for i, c := range s.regions.cells {58 fc := fileNotebookCell{Source: c.Source}59 if c.Kind == notebook.Note {60 fc.Kind = c.Kind.String()61 }62 if o := w.Output(c.ID); o != nil {63 fc.Error, fc.Detail, fc.Note = clip(o.Err), clip(o.Detail), clip(o.Note)64 switch {65 case kept[c.ID]:66 fc.Output = string(o.NUON)67 case o.NUON != nil || o.Unsaved:68 fc.Unsaved = true69 }70 }71 raw, err := json.Marshal(fc)72 if err != nil {73 return err74 }75 if i > 0 {76 b.WriteByte(',')77 }78 fmt.Fprintf(b, "\n%s %s", indent, raw)79 }80 b.WriteString("\n" + indent + "]")81 return nil82}8384func clip(s string) string {85 if len(s) > maxSavedError {86 return strings.ToValidUTF8(s[:maxSavedError], "") + "…"87 }88 return s89}9091// UnsavedOutputs names the cells whose outputs a file written now would92// leave out, over the caps.93func (w *Workbook) UnsavedOutputs() []string {94 var out []string95 for _, s := range w.sheets {96 kept := notebook.Kept(s.regions.cells, w.Output, w.OutputCaps())97 for i, c := range s.regions.cells {98 if o := w.Output(c.ID); o != nil && o.NUON != nil && !kept[c.ID] {99 name := c.Name()100 if name == "" {101 name = "cell " + fmt.Sprint(i+1)102 }103 out = append(out, name)104 }105 }106 }107 return out108}109110// readNotebook reads a notebook tab's cells and their outputs, as its111// cells would have left them.112func (s *Sheet) readNotebook(f fileSheet) error {113 if f.Tab != notebookTab {114 if len(f.NotebookCells) > 0 {115 return fmt.Errorf("notebook cells on a sheet that isn't a notebook")116 }117 return nil118 }119 w := s.wb120 s.regions.notebook, s.regions.reactive = true, f.Reactive121 outputs := map[int]*notebook.Output{}122 for i, fc := range f.NotebookCells {123 kind, ok := notebook.ParseKind(fc.Kind)124 if !ok {125 return fmt.Errorf("cell %d: invalid kind %q", i+1, fc.Kind)126 }127 c := notebook.Cell{ID: w.NewCellID(), Kind: kind, Source: fc.Source}128 s.regions.cells = append(s.regions.cells, c)129 if fc.Output == "" && fc.Error == "" && !fc.Unsaved {130 continue131 }132 o := &notebook.Output{Err: fc.Error, Detail: fc.Detail, Note: fc.Note, Unsaved: fc.Unsaved}133 if fc.Output != "" && !fc.Unsaved {134 o.NUON = []byte(fc.Output)135 }136 w.SetOutput(c.ID, o)137 outputs[c.ID] = w.Output(c.ID)138 }139 notebook.Settle(s.regions.cells, outputs)140 return nil141}142143// oldRegion is a command region of a notebook sheet as earlier builds144// wrote it, which opening the file converts: its command becomes a145// code cell of the workbook's notebook tab, and its table stays on the146// sheet as the cell's output sent there.147type oldRegion struct {148 s *Sheet149 fr fileRegion150}151152// convertOld turns the command regions of a file into code cells of a153// notebook tab, made after the last sheet that held them, and their154// tables into sent outputs where the tables were, saying so in the load155// notes.156func (w *Workbook) convertOld(old []oldRegion) {157 if len(old) == 0 {158 return159 }160 nb := w.Notebook()161 if nb == nil {162 nb = w.newSheet(w.freeName("Notebook"))163 nb.regions.notebook = true164 w.insert(nb, w.Index(old[len(old)-1].s)+1)165 }166 var from []string167 for _, o := range old {168 fr := o.fr169 src := fr.Command170 switch {171 case fr.Input != "":172 src = "$sheet." + quoteRef(fr.Input) + " | do {\n" + src + "\n}"173 case len(notebook.Parse(src).Stmts) > 1: // one statement, so the name is its output's174 src = "do {\n" + src + "\n}"175 }176 nb.regions.cells = append(nb.regions.cells, notebook.Cell{ID: w.NewCellID(), Source: fr.Name + " = " + src})177 at, _ := ParseAddr(fr.At)178 at.Row = min(at.Row+1, MaxRows-1) // the table was under the label line179 r := Region{Name: fr.Name, At: at, Output: true}180 o.s.regions.list = append(o.s.regions.list, r)181 o.s.meta(nameKey(r.Name)).stale = true182 o.s.regionsStale = true183 if name := o.s.name; len(from) == 0 || from[len(from)-1] != name {184 from = append(from, name)185 }186 }187 w.nb.notes = append(w.nb.notes, fmt.Sprintf("%s's shell commands are now code cells of %s; their tables stay where they were, sent from the cells, and show once the cells run",188 strings.Join(from, ", "), nb.name))189}190191// quoteRef writes Sheet!A1:B2 as $sheet. reads it: the sheet's name in192// quotes when it isn't a plain word.193func quoteRef(ref string) string {194 sheet, rest := SplitSheet(ref)195 if sheet == "" {196 return rest197 }198 for i := range len(sheet) {199 if c := sheet[i]; !isLetter(c) && !isDigit(c) && c != '_' {200 return "'" + strings.ReplaceAll(sheet, "'", "''") + "'!" + rest201 }202 }203 return sheet + "!" + rest204}