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 / nbview / panel.go
panel.go 4.4K
1package nbview23import (4 "slices"5 "strings"67 "github.com/charmbracelet/x/ansi"89 "src.rest/cat/012/internal/ui/overlay"10 "src.rest/cat/012/internal/ui/theme"11)1213// What the notebook puts on the control panel: the context line's14// hints and messages, and the completions' box.1516// KeyLabel is how a key of Keys shows: "d d" as dd, a letter as it is17// typed (G is Shift+g), others as chips show them.18func KeyLabel(k string) string {19 if a, b, ok := strings.Cut(k, " "); ok {20 return a + b21 }22 if len(k) == 1 {23 return k24 }25 return theme.KeyLabel(k)26}2728// keyFor is the key bound to command id in keys, the shortest first.29func keyFor(keys map[string]string, id string) string {30 var found []string31 for k, c := range keys {32 if c == id {33 found = append(found, k)34 }35 }36 slices.SortFunc(found, func(a, b string) int {37 if len(a) != len(b) {38 return len(a) - len(b)39 }40 return strings.Compare(a, b)41 })42 if len(found) == 0 {43 return ""44 }45 return KeyLabel(found[0])46}4748// hints are key hints for commands, leaving out those without a key.49func (v *View) hints(keys map[string]string, pairs ...string) string {50 var out []string51 for i := 0; i+1 < len(pairs); i += 2 {52 if k := keyFor(keys, pairs[i]); k != "" {53 out = append(out, k, pairs[i+1])54 }55 }56 return v.h.Theme().KeyHints(out...)57}5859// ContextLine is what the context line says in the notebook, at its60// left and its right: a problem, a key waiting for its pair, or what61// the selection is, and the keys that apply that the toolbar doesn't62// show.63func (v *View) ContextLine() (string, string) {64 th := v.h.Theme()65 switch {66 case v.full != nil:67 return v.full.ContextLine(th)68 case v.edit.on:69 right := v.hints(v.EditKeys, "nb.command_mode", "done") + " " + th.KeyHints("Tab", "complete")70 if d := v.Diagnostic(); d != "" {71 return th.Warning.Render(d), right72 }73 if h, ok := v.hovered(); ok {74 if h.Help != nil {75 right = v.hints(v.EditKeys, "nb.word_help", "help")76 }77 return hoverLine(th, h, v.width-ansi.StringWidth(right)-6), right78 }79 return "", right80 case v.pending == "d":81 return th.Hint.Render("d again deletes " + v.selectedWords()), th.KeyHints("Esc", "cancel")82 case v.pending == "0":83 return th.Hint.Render("0 again restarts: every output is cleared"), th.KeyHints("Esc", "cancel")84 }85 c, ok := v.Cell()86 if !ok {87 return "", v.hints(v.Keys, "nb.insert_below", "add a cell")88 }89 if from, to := v.Range(); to > from {90 return th.Hint.Render(v.selectedWords() + " selected"), v.hints(v.Keys, "nb.delete", "delete", "nb.move_up", "move up", "nb.move_down", "down")91 }92 st := v.h.State(c.ID)93 if st.Problem != "" {94 return th.Warning.Render(st.Problem), ""95 }96 if o := v.h.Output(c.ID); o.Failed() && !v.onOut {97 return th.Warning.Render("Failed: " + o.Err), v.hints(v.Keys, "nb.edit", "edit")98 }99 if v.onOut {100 enter := "full-screen"101 if v.shown(c).isGrid() {102 enter = "work in it"103 }104 return th.Muted.Render(v.outputSummary(c)), v.hints(v.Keys, "nb.edit", enter, "nb.toggle_output", "hide",105 "nb.toggle_whole", "whole", "nb.send", "to a sheet")106 }107 name, text := v.Head()108 return th.Muted.Render(name + ": " + text), v.hints(v.Keys, "nb.edit", "edit") + " " + th.KeyHints("Shift+↑↓", "select")109}110111// selectedWords names the selected cells: "the cell", "3 cells".112func (v *View) selectedWords() string {113 if from, to := v.Range(); to > from {114 return itoa(to-from+1) + " cells"115 }116 return "the cell"117}118119// Rule is the context line drawn at width: left, then a rule, then120// right, the rule closing off the toolbar above the cells.121func (v *View) Rule(left, right string, width int) string {122 th := v.h.Theme()123 lw, rw := ansi.StringWidth(left), ansi.StringWidth(right)124 if lw+rw+4 > width {125 right, rw = "", 0126 }127 if left != "" {128 left = " " + ansi.Truncate(left, width-2, "…") + " "129 lw = ansi.StringWidth(left)130 }131 if right != "" {132 right = " " + right + " "133 rw += 2134 }135 return left + th.Border.Render(strings.Repeat("─", max(width-lw-rw, 0))) + right136}137138// first line is at screen row top.139func (v *View) Boxes(top int) []overlay.Box {140 if !v.edit.on || len(v.edit.comp) == 0 {141 return nil142 }143 lines := v.completionBox(v.h.Theme())144 x, y, ok := v.Cursor()145 if !ok || len(lines) == 0 {146 return nil147 }148 caret := top + y149 y = caret + 1150 if y+len(lines) > top+v.height {151 y = max(caret-len(lines), 0) // no room below: above the caret152 }153 x = max(min(x, v.width-ansi.StringWidth(lines[0])), 0)154 return []overlay.Box{{ID: "nbcomplete", X: x, Y: y, Lines: lines}}155}