cat / 012
clone

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

cat/module-path
012 / internal / ui / nbgrid.go
nbgrid.go 8.3K
1package ui23import (4 "bytes"5 "context"6 "slices"7 "strconv"89 "github.com/charmbracelet/x/ansi"1011 "src.rest/cat/012/internal/fileio"12 "src.rest/cat/012/internal/sheet"13 "src.rest/cat/012/internal/ui/nbview"14)1516// A notebook cell's table or record output is drawn and worked as 012's17// own grid. Each such output gets a Model of its own, the grid's child,18// over a workbook of one sheet holding the output as importing its NUON19// makes it (as sending it to a sheet does): the header row, frozen, then20// the rows, each value in the format of its type, the columns fitted.21// The output's window shows the child's column header, named after the22// table's columns rather than lettered, and its rows under the header,23// numbered from 1; the child draws them with the grid's own code24// (view.go), so borders, formats, the pointer, the selection, the copy25// marker and search matches look as on a sheet. While the output is26// entered, keys, the mouse and commands go to the child (nbgridin.go),27// which refuses anything that would change a value: sorting, filtering,28// finding, selecting, copying and resizing columns act on its copy, and29// the output itself never changes.3031// outGrids are the outputs drawn as grids, and the one entered.32type outGrids struct {33 in *outGrid // the grid with the keys, or nil34}3536// outGrid is an output's grid.37type outGrid struct {38 parent *Model39 id int // the cell's40 data []byte // the output as a table, NUON (nbview.Host.Grid)41 rows int // its rows, until the child counts them42 child *Model // made the first time the grid is drawn43 err string // why the output couldn't be read as a table44 cols int // the table's columns45 // win is the rows the window shows, as last drawn, which the child46 // is sized to.47 win int48 // shown are the rows showing when a filter hides some, for the49 // sheet's version shownAt; nil when every row shows.50 shown []int51 shownAt uint6452 // frame is the parent's frame the child last drew for.53 frame int54 // fit are the columns' widths as the grid fitted them, and need the55 // widths their text needs as rows arrive (nbgridstream.go).56 fit, need []int57}5859var _ nbview.Grid = (*outGrid)(nil)6061// Grid makes cell id's output a grid (nbview.Host).62func (h nbHost) Grid(id int, data []byte, rows int) nbview.Grid {63 return &outGrid{parent: h.m, id: id, data: data, rows: rows}64}6566// ready makes the child if it isn't yet, and sizes it to a window of67// rows rows width wide: reports false if the output isn't a table.68func (g *outGrid) ready(width, rows int) bool {69 if g.child == nil && g.err == "" {70 g.build()71 }72 if g.child == nil {73 return false74 }75 c := g.child76 g.widen()77 if f := g.parent.nb.frame; g.frame != f {78 g.frame = f79 c.th = g.parent.th80 clear(c.painted)81 clear(c.mergeLines)82 clear(c.shaded)83 }84 // An output shown whole draws its rows one at a time wherever they85 // are, so the child needn't be taller than a screen.86 g.win = min(max(rows, 1), maxWin)87 // The frozen header and its divider take two of the child's lines,88 // and at least two rows keep the header frozen (grid.frozen).89 c.width, c.height = width, gridTop+max(g.win, 2)+2+190 c.clampView()91 return true92}9394// build reads the output into the child's workbook.95func (g *outGrid) build() {96 res, err := fileio.ImportReader(context.Background(), "nu", bytes.NewReader(g.data), fileio.Options{})97 if err != nil {98 g.err = err.Error()99 return100 }101 s := res.Sheet102 s.Book().SetLocale(g.parent.book().LocaleTag())103 s.LoadFrozen(1, 0)104 s.LoadFitWidths()105 s.ClearHistory()106 used, _ := s.UsedRange()107 g.cols, g.rows = used.To.Col+1, used.To.Row108 for c := range g.cols {109 g.fit = append(g.fit, s.ColWidth(c))110 }111 g.need = slices.Clone(g.fit)112 c := New(s, "")113 c.out, c.named = g, true114 c.th = g.parent.th115 c.prefs = g.parent.prefs116 c.prefs.vim = false // vim's keys edit cells, and an output's are read-only117 c.term = terminal{} // no images: its sheet has no charts, and the program draws the screen118 c.cur, c.top = sheet.Addr{Row: 1}, 1119 g.child = c120}121122// table is the table on the child's sheet, its header row included.123func (g *outGrid) table() sheet.Rect {124 return sheet.Rect{To: sheet.Addr{Col: max(g.cols-1, 0), Row: max(g.rows, 1)}}125}126127// Rows is how many rows show: those the filter keeps.128func (g *outGrid) Rows() int {129 if g.child == nil {130 return g.rows131 }132 if s := g.child.sheet; s.HiddenRows() > 0 {133 return len(g.shownRows())134 }135 return g.rows136}137138// shownRows are the rows a filter keeps, worked out once for each139// version of the sheet.140func (g *outGrid) shownRows() []int {141 s := g.child.sheet142 if g.shown != nil && g.shownAt == s.Version() {143 return g.shown144 }145 g.shown = g.shown[:0]146 for r := 1; r <= g.rows; r++ {147 if !s.RowHidden(r) {148 g.shown = append(g.shown, r)149 }150 }151 g.shownAt = s.Version()152 return g.shown153}154155// row is the sheet row of the i-th row showing.156func (g *outGrid) row(i int) (int, bool) {157 if i < 0 || i >= g.Rows() {158 return 0, false159 }160 if g.child == nil || g.child.sheet.HiddenRows() == 0 {161 return i + 1, true162 }163 return g.shownRows()[i], true164}165166// index is where sheet row r is among the rows showing: the first167// showing at or after it.168func (g *outGrid) index(r int) int {169 if g.child == nil || g.child.sheet.HiddenRows() == 0 {170 return max(r-1, 0)171 }172 i, _ := slices.BinarySearch(g.shownRows(), r)173 return i174}175176// Top is the first row the window shows.177func (g *outGrid) Top() int {178 if g.child == nil {179 return 0180 }181 return min(g.index(g.child.top), max(g.Rows()-g.win, 0))182}183184// Scroll scrolls the window d rows, reporting whether it moved.185func (g *outGrid) Scroll(d int) bool {186 if g.child == nil && !g.ready(g.parent.nbGridWidth(), 10) {187 return false188 }189 top := g.Top()190 to := min(max(top+d, 0), max(g.Rows()-g.win, 0))191 if to == top {192 return false193 }194 r, _ := g.row(to)195 g.child.top = r196 return true197}198199// Line draws line i of the window: the column header, or a row.200func (g *outGrid) Line(i, from, rows, width int) string {201 if !g.ready(width, rows) {202 if i == 0 {203 return g.parent.th.Warning.Render(ansi.Truncate("The output isn't a table: "+g.err, width, "…"))204 }205 return ""206 }207 c := g.child208 if rows > g.win {209 // Shown whole: the row numbers are as wide as the last one's,210 // which the child measures from its top.211 top := c.top212 c.top, _ = g.row(max(g.Rows()-g.win, 0))213 defer func() { c.top = top }()214 }215 var line string216 if i == 0 {217 line = c.headerRow()218 } else {219 r, ok := g.row(from + i - 1)220 if !ok {221 return ""222 }223 line = c.gridRow(r)224 }225 return ansi.Truncate(line, min(width, g.right()), "")226}227228// maxWin is the most rows the child is sized to show.229const maxWin = 500230231// right is where the table's last column ends on the child's screen:232// nothing is drawn past it.233func (g *outGrid) right() int {234 c := g.child235 if g.cols <= c.left {236 return c.width237 }238 return c.colStart(g.cols)239}240241// Hidden is how many columns don't show whole at width.242func (g *outGrid) Hidden(width int) int {243 if !g.ready(width, g.win) {244 return 0245 }246 c := g.child247 shown := 0248 for col := c.left; col < g.cols && c.colStart(col)+c.sheet.ColWidth(col) <= width; col++ {249 shown++250 }251 return g.cols - shown252}253254// Entered reports whether the grid has the keys.255func (g *outGrid) Entered() bool { return g.parent.nb.out.in == g }256257// unfocused reports whether m is an output's grid without the keys,258// which draws neither its active cell nor its selection.259func (m *Model) unfocused() bool { return m.out != nil && !m.out.Entered() }260261// colLabel is column c's name in the column header, w wide: its letters,262// or on an output's grid the table's name for it, cut to fit beside263// room more columns (a filter's button).264func (g *grid) colLabel(c, w, room int) string {265 if !g.named {266 return sheet.ColName(c)267 }268 return ansi.Truncate(g.colName(c), max(w-2-room, 1), "…")269}270271// colName names column c in messages: its letters, or the table's name272// for it on an output's grid.273func (g *grid) colName(c int) string {274 if !g.named {275 return sheet.ColName(c)276 }277 return g.sheet.ShownText(sheet.Addr{Col: c})278}279280// rowLabel is row r's number: on an output's grid, counted from the281// first row under the header.282func (g *grid) rowLabel(r int) string {283 if g.named {284 return strconv.Itoa(r)285 }286 return strconv.Itoa(r + 1)287}288289// nbGridWidth is how wide the notebook shown draws outputs' grids.290func (m *Model) nbGridWidth() int {291 if v := m.nbView(); v != nil {292 return v.GridWidth()293 }294 return m.width295}