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 / draw.go
draw.go 7.5K
1package nbview23import (4 "strconv"5 "strings"6 "time"78 "charm.land/lipgloss/v2"9 "github.com/charmbracelet/x/ansi"1011 "src.rest/cat/012/internal/notebook"12 "src.rest/cat/012/internal/ui/lineedit"13)1415// Drawing a cell's block line by line (layout.go says where).1617// blockLine draws line k of cell i's block.18func (v *View) blockLine(i int, c notebook.Cell, b block, k int) string {19 kind, r := b.row(k)20 if kind == rowGap {21 return ""22 }23 lead := v.bar(i, kind) + v.prompt(c, kind, r) + " "24 switch kind {25 case rowTop:26 return lead + v.boxTop(i, c)27 case rowBottom:28 return lead + v.boxEdge(i, "╰", "╯", "┗", "┛")29 case rowOut:30 return lead + " " + v.outLine(c, r)31 }32 if b.box == 0 {33 return lead + " " + v.sourceLine(i, c, r)34 }35 side := v.boxStyle(i).Render(v.edges(i, "│", "┃"))36 text := v.sourceLine(i, c, r)37 text += strings.Repeat(" ", max(v.content()-ansi.StringWidth(text), 0))38 line := lead + side + " " + text + " " + side39 if r == 0 {40 line += " " + v.runButton(i, c)41 }42 return line43}4445// bar is the column left of a cell: ▌ by the active cell, blue in46// command mode and green in edit mode, and ▎ by the other cells47// selected with it. With the active cell's output selected, the bar is48// by the output alone.49func (v *View) bar(i int, kind rowKind) string {50 from, to := v.Range()51 if i < from || i > to || kind == rowGap || i == v.sel && v.onOut && kind != rowOut {52 return " "53 }54 th := v.h.Theme()55 style := th.CellBar56 if v.edit.on || i == v.sel && v.onOut && v.shown(v.h.Cells()[i]).entered() {57 style = th.CellBarEdit // a grid entered takes keys as a cell edited does58 }59 if i != v.sel {60 return style.Render("▎")61 }62 return style.Render("▌")63}6465// prompt is the prompt column: [n]: by a code cell's first line, Out[n]:66// by its output's, [*]: while it runs and [ ]: before it has.67func (v *View) prompt(c notebook.Cell, kind rowKind, r int) string {68 text := ""69 if c.Kind == notebook.Code && r == 0 {70 o := v.h.Output(c.ID)71 switch {72 case kind == rowSrc && v.h.State(c.ID).Running:73 text = "[*]:"74 case kind == rowSrc:75 text = "[" + count(o, " ") + "]:"76 case kind == rowOut && v.shown(c).kind != outError:77 text = "Out[" + count(o, " ") + "]:"78 if len(text) > promptW {79 text = "[" + count(o, " ") + "]:"80 }81 }82 }83 return v.h.Theme().CellHead.Render(strings.Repeat(" ", max(promptW-len(text), 0)) + text)84}8586// count is the output's run number, or blank for none or one read from87// the file.88func count(o *notebook.Output, blank string) string {89 if o == nil || o.Count == 0 {90 return blank91 }92 return strconv.Itoa(o.Count)93}9495// edges picks a box's line: light, or heavy for the cell being edited,96// which reads without color.97func (v *View) edges(i int, light, heavy string) string {98 if v.edit.on && i == v.sel {99 return heavy100 }101 return light102}103104// boxStyle is a box's lines: in the bar's color for the active cell.105func (v *View) boxStyle(i int) lipgloss.Style {106 th := v.h.Theme()107 switch {108 case v.edit.on && i == v.sel:109 return th.CellBarEdit110 case i == v.sel && !v.onOut:111 return th.CellBar112 }113 return th.Border114}115116// boxTop is a box's top border, with the cell's name at the left and117// how its run stands at the right.118func (v *View) boxTop(i int, c notebook.Cell) string {119 th := v.h.Theme()120 style := v.boxStyle(i)121 h := v.edges(i, "─", "━")122 w := v.content() + 2 // between the corners123 title := c.Name()124 if c.Kind == notebook.Note {125 title = "Markdown"126 }127 state, stateStyle := v.state(c)128 room := w - 2129 if state != "" {130 state = ansi.Truncate(state, max(room/2, 8), "…")131 room -= ansi.StringWidth(state) + 3132 }133 var left, right string134 used := 0135 if title != "" && room > 4 {136 title = ansi.Truncate(title, room-2, "…")137 left = style.Render(h+" ") + th.CellHead.Render(title) + style.Render(" ")138 used += ansi.StringWidth(title) + 3139 }140 if state != "" {141 right = style.Render(" ") + stateStyle.Render(state) + style.Render(" "+h)142 used += ansi.StringWidth(state) + 3143 }144 fill := style.Render(strings.Repeat(h, max(w-used, 0)))145 return style.Render(v.edges(i, "╭", "┏")) + left + fill + right + style.Render(v.edges(i, "╮", "┓"))146}147148// boxEdge is a box's bottom border.149func (v *View) boxEdge(i int, left, right, heavyLeft, heavyRight string) string {150 return v.boxStyle(i).Render(v.edges(i, left, heavyLeft) + strings.Repeat(v.edges(i, "─", "━"), v.content()+2) + v.edges(i, right, heavyRight))151}152153// runButton is ▶, which runs the cell, or ■ while it runs, which stops154// it.155func (v *View) runButton(i int, c notebook.Cell) string {156 th := v.h.Theme()157 style := th.CellHead158 if i == v.sel {159 style = v.boxStyle(i)160 }161 if v.h.State(c.ID).Running {162 return style.Render("■")163 }164 return style.Render("▶")165}166167// state is what a cell's box says of its run, and in what style: empty168// before it has run.169func (v *View) state(c notebook.Cell) (string, lipgloss.Style) {170 th := v.h.Theme()171 if c.Kind != notebook.Code {172 return "", th.CellHead173 }174 st := v.h.State(c.ID)175 o := v.h.Output(c.ID)176 switch {177 case st.Problem != "":178 return st.Problem, th.Warning179 case st.Live:180 return live(st.Rows), th.CellHead181 case st.Running:182 return running(time.Since(st.Started)), th.CellHead183 case st.Waiting:184 return "waiting", th.CellHead185 case o == nil:186 return "", th.CellHead187 case o.Failed():188 return "failed" + took(o), th.Warning189 case st.Stale:190 return "stale" + took(o), th.Stale191 case o.Count == 0:192 return "saved", th.CellHead193 }194 return "✓" + took(o), th.CellHead195}196197// outLine is line r of cell c's output: its window, or one line saying198// it's folded.199func (v *View) outLine(c notebook.Cell, r int) string {200 th := v.h.Theme()201 f := v.foldOf(c.ID)202 if f.hidden {203 return th.Muted.Render("⋯ output hidden (o shows it)")204 }205 return v.shown(c).line(th, v.h.Locale(), r, f, v.outWidth())206}207208// sourceLine is row r of a cell's source: the editor's while it's209// edited, a note's Markdown, or a pipeline highlighted.210func (v *View) sourceLine(i int, c notebook.Cell, r int) string {211 if v.edit.on && i == v.sel {212 return v.edit.line(v.h.Theme(), r, v.content(), v.spansFor(v.edit.text(), c.Kind, true))213 }214 if c.Kind == notebook.Note {215 lines := v.noteLines(c)216 if r < len(lines) {217 return lines[r]218 }219 return ""220 }221 buf := []rune(c.Source)222 rows := lineedit.Wrap(buf, v.content())223 if r >= len(rows) {224 return ""225 }226 return drawRow(v.h.Theme(), buf, rows[r], v.spansFor(c.Source, c.Kind, false), nil)227}228229// spinner turns while a cell runs.230var spinner = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}231232// running is what a running cell's box says: after a few seconds, a233// spinner and how long it has run.234func running(d time.Duration) string {235 if d < 3*time.Second {236 return "running"237 }238 return spinner[int(d/(100*time.Millisecond))%len(spinner)] + " running " + duration(d)239}240241// live is a streaming cell's state: ● live, and how many rows it has242// printed.243func live(rows int) string {244 s := "● live, " + itoa(rows) + " row"245 if rows != 1 {246 s += "s"247 }248 return s249}250251func took(o *notebook.Output) string {252 if o.Took <= 0 {253 return ""254 }255 return " " + duration(o.Took)256}257258// duration is a run's length: under a second as <1s, so a fast cell259// always reads the same, then seconds, then minutes.260func duration(d time.Duration) string {261 switch {262 case d < time.Second:263 return "<1s"264 case d < 10*time.Second:265 return strconv.FormatFloat(d.Seconds(), 'f', 1, 64) + "s"266 case d < time.Minute:267 return strconv.Itoa(int(d.Seconds())) + "s"268 }269 return strconv.Itoa(int(d.Minutes())) + "m" + pad2(int(d.Seconds())%60) + "s"270}271272func pad2(n int) string {273 if n < 10 {274 return "0" + strconv.Itoa(n)275 }276 return strconv.Itoa(n)277}