1package nbview23import (4 "strings"56 "src.rest/cat/012/internal/notebook"7 "src.rest/cat/012/internal/nuon"8 "src.rest/cat/012/internal/ui/theme"9)1011// A table (a list of records) or a record is drawn by the UI as its own12// grid, which the output's window shows: the grid's column header, named13// after the table's columns, then as many of its rows as the window14// holds. The UI owns everything about the grid (its cells, formats,15// widths, the active cell, the selection, sorting and filtering); the16// notebook places it, says how many rows show, and scrolls it.1718// Grid is an output drawn as the UI's grid.19type Grid interface {20 // Rows is how many rows the grid has showing: the rows a filter21 // keeps, the header not counted.22 Rows() int23 // Top is the first row the window shows, counted among those.24 Top() int25 // Scroll scrolls the window d rows, reporting whether it moved.26 Scroll(d int) bool27 // Line draws line i of a window of rows rows at width, showing the28 // rows from row from on: the column header for 0, then the rows.29 Line(i, from, rows, width int) string30 // Hidden is how many of the grid's columns don't show at width.31 Hidden(width int) int32 // Entered reports whether the grid has the keys: its active cell33 // and selection show, and arrows move them.34 Entered() bool35 // Append adds more, a NUON table of n rows, under the grid's rows,36 // as a stream's rows arrive; data is the whole output now.37 Append(more []byte, n int, data []byte)38}3940// gridData is what the UI's grid of an output holds, as NUON: a table as41// it is, and a record as a table of its fields, a column of their names42// and one of their values.43func gridData(v nuon.Value, data []byte) []byte {44 if v.Kind != nuon.Record {45 return data46 }47 list := nuon.Value{Kind: nuon.List}48 for _, f := range v.Fields {49 list.List = append(list.List, nuon.Value{Kind: nuon.Record, Fields: []nuon.Field{50 {Key: "field", Value: nuon.StringValue(f.Key)}, {Key: "value", Value: f.Value}}})51 }52 return nuon.Append(nil, list)53}5455// entered reports whether the output's grid has the keys.56func (sh *shown) entered() bool { return sh.grid != nil && sh.grid.Entered() }5758// gridRows is how many of a grid's rows show, and from which: all of59// them from the first when the output shows whole (unless it's entered,60// when it scrolls in its window), or a window's worth from where it's61// scrolled.62func (sh *shown) gridRows(whole bool) (from, n int) {63 total := sh.grid.Rows()64 if whole && !sh.entered() {65 return 0, total66 }67 return sh.grid.Top(), min(total, window)68}6970// gridLine draws line i of a grid's window.71func (sh *shown) gridLine(th *theme.Theme, i int, f fold, width int) string {72 from, n := sh.gridRows(f.whole)73 switch {74 case i <= n:75 return sh.grid.Line(i, from, max(n, 1), width)76 case n == 0 && i == 1:77 return th.Muted.Render("no rows: the filter hides them all")78 }79 return sh.gridFooter(th, from, n, width)80}8182// gridFooter says what a grid's window leaves out.83func (sh *shown) gridFooter(th *theme.Theme, from, n, width int) string {84 total := sh.grid.Rows()85 unit := "row"86 if sh.kind == outRecord {87 unit = "field"88 }89 var parts []string90 switch {91 case n >= total:92 case from == 0:93 parts = append(parts, more(total-n, "more "+unit))94 default:95 parts = append(parts, unit+"s "+grouped(from+1)+" to "+grouped(from+n)+" of "+grouped(total))96 }97 if c := sh.grid.Hidden(width); c > 0 {98 parts = append(parts, more(c, "more column"))99 }100 var hints []string101 if n < total && !sh.entered() {102 hints = append(hints, "O shows all")103 }104 if !sh.entered() {105 hints = append(hints, "Enter works in it")106 }107 line := "… " + strings.Join(parts, ", ")108 if len(hints) > 0 {109 line += " (" + strings.Join(hints, ", ") + ")"110 }111 return th.Muted.Render(line)112}113114// SelectedGrid is the selected output's grid, if it's selected and drawn115// as one.116func (v *View) SelectedGrid() Grid {117 c, ok := v.Cell()118 if !ok || !v.onOut {119 return nil120 }121 return v.shown(c).grid122}123124// Window is how many rows an output's window shows.125const Window = window126127// Shows reports whether grid g is the selected output's, or shown128// full-screen.129func (v *View) Shows(g Grid) bool {130 if v.full != nil {131 return v.full.Grid() == g132 }133 return g != nil && v.SelectedGrid() == g134}135136// SelectGrid selects the output grid g is drawn in.137func (v *View) SelectGrid(g Grid) {138 for i, c := range v.h.Cells() {139 if sh := v.shown(c); g != nil && sh.grid == g {140 v.StopEdit()141 v.Select(i, true)142 return143 }144 }145}146147// GridAt is the grid at column x of line y of the body, with where that148// is in the grid: gy 0 its column header, then its rows as the window149// shows them.150func (v *View) GridAt(x, y int) (g Grid, gx, gy int, ok bool) {151 if v.full != nil {152 if g = v.full.Grid(); g == nil || y < 0 || y >= v.height {153 return nil, 0, 0, false154 }155 return g, x, y, true156 }157 h, ok := v.hitAt(y)158 if !ok || h.kind != rowOut || x < textX {159 return nil, 0, 0, false160 }161 c := v.h.Cells()[h.cell]162 f := v.foldOf(c.ID)163 sh := v.shown(c)164 if f.hidden || !sh.isGrid() {165 return nil, 0, 0, false166 }167 if _, n := sh.gridRows(f.whole); h.r > n {168 return nil, 0, 0, false169 }170 return sh.grid, x - textX, h.r, true171}172173// GridOrigin is where grid g's column header is drawn in the body: its174// first column and line, which may be off screen.175func (v *View) GridOrigin(g Grid) (x, y int, ok bool) {176 if v.full != nil {177 return 0, 0, v.full.Grid() == g178 }179 for i, c := range v.h.Cells() {180 if sh := v.shown(c); g != nil && sh.grid == g {181 b := v.blockOf(i, c)182 return textX, v.startOf(i) + 2*b.box + b.src - v.top, true183 }184 }185 return 0, 0, false186}187188// GridWidth is how wide an output's grid is drawn: the output's width,189// or the body's full-screen.190func (v *View) GridWidth() int {191 if v.full != nil {192 return v.width193 }194 return v.outWidth()195}196197// FollowOutput scrolls so the selected output's window shows, as far as198// it fits: its grid's active cell moved, or the grid changed its height.199func (v *View) FollowOutput() {200 if v.full == nil && v.onOut {201 v.follow()202 }203}204205// maxCarried is the most rows a stream's grid holds, carried from output206// to output: the rows a stream keeps, twice over, so a grid past them207// is read again from the output about once for every StreamKeep rows.208const maxCarried = 20000209210// Carry shows o, cell id's output, as old showed: o is old with the211// rows a stream printed since, more (a NUON list), so they go under the212// rows of old's grid, whose widths, pointer, selection, sort and filter213// stay, rather than o being read whole. It reports false, and o is read214// afresh when it shows, when old isn't showing as a table's grid, more215// isn't a table, or the grid would hold more than maxCarried rows.216func (v *View) Carry(id int, old, o *notebook.Output, more []byte) bool {217 prev := v.outs[old]218 if prev == nil || prev.kind != outTable || prev.grid == nil || v.h.Output(id) != o {219 return false220 }221 val, err := nuon.Parse(more)222 if err != nil || val.Kind != nuon.List || !isTable(val.List) || prev.total+len(val.List) > maxCarried {223 return false224 }225 keys := addKeys(prev.keys, val.List)226 sh := &shown{kind: outTable, total: prev.total + len(val.List), cols: len(keys), keys: keys, data: o.NUON, grid: prev.grid}227 if len(val.List) > 0 {228 prev.grid.Append(more, len(val.List), o.NUON)229 }230 delete(v.outs, old)231 v.outs[o] = sh232 if v.full != nil && v.full.sh == prev {233 v.full.sh = sh234 }235 return true236}