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 / nbview / output.go
output.go 5.6K
1package nbview23import (4 "strconv"5 "strings"67 "github.com/charmbracelet/x/ansi"89 "src.rest/cat/012/internal/fileio"10 "src.rest/cat/012/internal/locale"11 "src.rest/cat/012/internal/notebook"12 "src.rest/cat/012/internal/nuon"13 "src.rest/cat/012/internal/sheet"14 "src.rest/cat/012/internal/ui/theme"15)1617// Outputs as they show under a code cell. What a cell printed, NUON, is18// parsed once into a shown output: a table (a list of records) or a19// record, which the UI draws as its own grid (grid.go), a list, text,20// or a single value; values are the cells importing them would make,21// formatted as cells are. A long output scrolls in a window of its own22// (outwin.go) until O shows it whole; its lines are drawn only when23// they're on screen, so an output of ten thousand rows costs the rows24// showing.2526// window is how many rows (or lines) an output's window shows.27const window = 102829// outKind is what an output holds.30type outKind int3132const (33 outNone outKind = iota // nothing printed34 outTable // a list of records: a grid35 outRecord // a record: a grid of its fields36 outList // a list of values that aren't records37 outText // a string, its lines38 outValue // a single value39 outError // the run failed40 outUnsaved // too large for the file to keep41 outBad // what nu printed isn't NUON42)4344// shown is an output parsed for showing.45type shown struct {46 kind outKind47 cols int // a table's columns48 rows [][]sheet.LiveCell // a list's items, or the one value49 text []string // text's lines, or an error's50 total int // rows or lines in all51 wrap []string // text wrapped at wrapW52 wrapW int53 // data is a table's or record's grid as NUON (gridData), which the54 // UI makes the grid from.55 data []byte56 grid Grid57 keys map[string]bool // a table's column names58}5960// parse reads an output for showing.61func parse(o *notebook.Output) *shown {62 switch {63 case o == nil:64 return &shown{}65 case o.Err != "":66 text := []string{o.Err}67 if o.Detail != "" {68 text = append(text, o.Detail)69 }70 return &shown{kind: outError, text: text, total: len(text)}71 case o.Unsaved:72 return &shown{kind: outUnsaved, total: 1}73 case len(o.NUON) == 0:74 return &shown{}75 }76 v, err := nuon.Parse(o.NUON)77 if err != nil {78 return &shown{kind: outBad, text: []string{"nu's output isn't NUON: " + err.Error()}, total: 1}79 }80 sh := shownOf(v)81 if sh.kind == outTable || sh.kind == outRecord {82 sh.data = gridData(v, o.NUON)83 }84 return sh85}8687// shownOf is how value v shows.88func shownOf(v nuon.Value) *shown {89 switch v.Kind {90 case nuon.Null:91 return &shown{}92 case nuon.String:93 lines := strings.Split(strings.TrimRight(v.Str, "\n"), "\n")94 return &shown{kind: outText, text: lines, total: len(lines)}95 case nuon.Record:96 if len(v.Fields) == 0 {97 return &shown{}98 }99 return &shown{kind: outRecord, total: len(v.Fields), cols: 2}100 case nuon.List:101 if len(v.List) == 0 {102 return &shown{}103 }104 if isTable(v.List) {105 keys := addKeys(nil, v.List)106 return &shown{kind: outTable, total: len(v.List), cols: len(keys), keys: keys}107 }108 sh := &shown{kind: outList, total: len(v.List)}109 for _, it := range v.List {110 sh.rows = append(sh.rows, []sheet.LiveCell{fileio.NUONCell(it)})111 }112 return sh113 }114 return &shown{kind: outValue, rows: [][]sheet.LiveCell{{fileio.NUONCell(v)}}, total: 1}115}116117// isTable reports whether a list is records, as nushell draws a table.118func isTable(list []nuon.Value) bool {119 for _, it := range list {120 if it.Kind != nuon.Record {121 return false122 }123 }124 return true125}126127// addKeys adds the columns a list of records names to keys.128func addKeys(keys map[string]bool, list []nuon.Value) map[string]bool {129 if keys == nil {130 keys = map[string]bool{}131 }132 for _, rec := range list {133 for _, f := range rec.Fields {134 keys[f.Key] = true135 }136 }137 return keys138}139140// isGrid reports whether the output is drawn as the UI's grid.141func (sh *shown) isGrid() bool { return sh.grid != nil }142143// cellText is a value as a cell shows it, whole.144func cellText(lc sheet.LiveCell, loc *locale.Locale) string {145 if lc.V.Kind == sheet.Empty {146 return ""147 }148 return strings.ReplaceAll(sheet.FormatTextIn(lc.V, lc.F, loc), "\n", " ")149}150151// more counts what's left out: "9,991 more rows".152func more(n int, what string) string {153 if n != 1 {154 what += "s"155 }156 return grouped(n) + " " + what157}158159// grouped is n with its thousands apart: 9,991.160func grouped(n int) string {161 s := strconv.Itoa(n)162 for i := len(s) - 3; i > 0; i -= 3 {163 s = s[:i] + "," + s[i:]164 }165 return s166}167168// fieldLine is a list's item, its index first as nushell shows one.169func (sh *shown) fieldLine(th *theme.Theme, loc *locale.Locale, i, width int) string {170 if i >= len(sh.rows) {171 return ""172 }173 n := len(strconv.Itoa(sh.total - 1))174 num := strconv.Itoa(i)175 key := th.Muted.Render(strings.Repeat(" ", max(n-len(num), 0))+num) + " "176 room := max(width-ansi.StringWidth(key), 1)177 return key + ansi.Truncate(cellText(sh.rows[i][0], loc), room, "…")178}179180// wrapped is the text's lines wrapped at width, kept for the width last181// asked.182func (sh *shown) wrapped(width int) []string {183 if sh.wrapW != width || sh.wrap == nil {184 sh.wrap = sh.wrap[:0]185 for i, l := range sh.text {186 if sh.kind == outError && i == 0 {187 l = "× " + l188 }189 sh.wrap = append(sh.wrap, wrapText(l, width)...)190 }191 sh.wrapW = width192 }193 return sh.wrap194}195196// wrapText breaks a line of text into lines at most width wide, at197// spaces where it can.198func wrapText(s string, width int) []string {199 if width < 1 || ansi.StringWidth(s) <= width {200 return []string{s}201 }202 return strings.Split(ansi.Wrap(s, width, " "), "\n")203}