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 / evalview / evalview.go
evalview.go 6.4K
1// Package evalview is Data > Evaluate formula, after Excel's Evaluate2// Formula: a box over the grid showing the active cell's formula with3// the part computed next underlined and its value below. Enter puts the4// value in its place and underlines the next part, until the formula's5// value is left; → steps into a reference to another formula, ← and Esc6// step back out. The engine does the evaluating (sheet.Steps); the view7// draws it. It knows the UI only through Host.8package evalview910import (11 "strconv"12 "strings"1314 tea "charm.land/bubbletea/v2"15 "charm.land/lipgloss/v2"16 "github.com/charmbracelet/x/ansi"1718 "src.rest/cat/012/internal/formula"19 "src.rest/cat/012/internal/locale"20 "src.rest/cat/012/internal/sheet"21 "src.rest/cat/012/internal/ui/overlay"22 "src.rest/cat/012/internal/ui/theme"23)2425// Host is what the view needs of the UI.26type Host interface {27 Theme() *theme.Theme28 Size() (width, height int)29 // Close closes the open overlay.30 Close()31 // Locale writes formulas as the user types them.32 Locale() *locale.Locale33}3435// View is the open Evaluate formula box.36type View struct {37 h Host38 stack []*sheet.Steps // the formula stepped into last on top39}4041// ID identifies the view's box in mouse events.42const ID = "evaluate"4344// maxInner is the widest the box's inside gets.45const maxInner = 764647// New returns the view of st, a formula about to be stepped through.48func New(h Host, st *sheet.Steps) *View { return &View{h: h, stack: []*sheet.Steps{st}} }4950// Steps is the formula being stepped through: the one stepped into last.51func (v *View) Steps() *sheet.Steps { return v.stack[len(v.stack)-1] }5253// Depth is how many formulas are open, the first one included.54func (v *View) Depth() int { return len(v.stack) }5556func (v *View) Indicator() string { return "EVAL" }5758// Key: Enter computes the next part (at the end, restarts, or steps59// out of a formula stepped into), → steps in, ← steps out, Esc steps60// out or closes.61func (v *View) Key(k tea.KeyPressMsg) tea.Cmd {62 st := v.Steps()63 switch k.String() {64 case "enter", "space":65 switch {66 case !st.Done():67 st.Step()68 case len(v.stack) > 1:69 v.out()70 default:71 st.Restart()72 }73 case "right":74 if in := st.Into(); in != nil {75 v.stack = append(v.stack, in)76 }77 case "left":78 if len(v.stack) > 1 {79 v.out()80 }81 case "esc":82 if len(v.stack) > 1 {83 v.out()84 return nil85 }86 v.h.Close()87 }88 return nil89}9091// out steps out of the formula stepped into: the reference to it takes92// its value in the formula before.93func (v *View) out() {94 v.stack = v.stack[:len(v.stack)-1]95 v.Steps().Step()96}9798// Mouse closes the box on a click outside it.99func (v *View) Mouse(e overlay.MouseEvent) tea.Cmd {100 if e.Kind == overlay.MousePress && e.Box != ID {101 v.h.Close()102 }103 return nil104}105106// Status says what the keys do now.107func (v *View) Status() (string, string) {108 th, st := v.h.Theme(), v.Steps()109 var keys []string110 switch {111 case !st.Done():112 keys = append(keys, "Enter", "evaluate")113 case len(v.stack) > 1:114 keys = append(keys, "Enter", "step out")115 default:116 keys = append(keys, "Enter", "restart")117 }118 if st.CanStepIn() {119 keys = append(keys, "→", "step in")120 }121 if len(v.stack) > 1 {122 keys = append(keys, "←", "step out")123 }124 keys = append(keys, "Esc", "close")125 desc := "The underlined part is computed next"126 if st.Done() {127 desc = "Every part is computed"128 }129 return desc, th.KeyHints(keys...)130}131132// Layout is the box: the formula, wrapped, then the next part and its133// value, under the menu bar and centered.134func (v *View) Layout() []overlay.Box {135 th := v.h.Theme()136 width, height := v.h.Size()137 inner := min(width-2, maxInner)138 st := v.Steps()139 text := v.formulaLines(inner-2, max(height-12, 1))140 rows := make([]string, 0, len(text)+2)141 for _, l := range text {142 rows = append(rows, th.MenuBar.Render(" ")+l+th.MenuBar.Render(strings.Repeat(" ", max(inner-1-ansi.StringWidth(l), 0))))143 }144 rows = append(rows, theme.SepRow, v.nextRow(inner))145 done, total := st.Progress()146 footer := strconv.Itoa(done) + " of " + strconv.Itoa(total)147 return []overlay.Box{{ID: ID, X: (width - inner - 2) / 2, Y: overlay.MenuLine + 1, Lines: th.Frame(inner, v.title(), footer, rows)}}148}149150// title names the formulas open: Evaluate C1 › B1.151func (v *View) title() string {152 first, _ := v.stack[0].Cell()153 names := make([]string, len(v.stack))154 for i, st := range v.stack {155 s, a := st.Cell()156 names[i] = a.String()157 if s != first {158 names[i] = sheet.Qualified(s.Name(), sheet.Rect{From: a, To: a})159 }160 }161 return "Evaluate " + strings.Join(names, " › ")162}163164// nextRow is "Next B1 = 8", or once done "Value 14".165func (v *View) nextRow(inner int) string {166 th, st, loc := v.h.Theme(), v.Steps(), v.h.Locale()167 line := th.Muted.Render(" Value ") + th.Evaluated.Render(formula.Localize(st.Result(), loc))168 if expr, value, ok := st.Next(); ok {169 line = th.Muted.Render(" Next ") + th.EvalNext.Render(formula.Localize(expr, loc)) + th.Muted.Render(" = ") + th.Evaluated.Render(formula.Localize(value, loc))170 }171 line = ansi.Truncate(line, inner, "…")172 return line + th.MenuBar.Render(strings.Repeat(" ", max(inner-ansi.StringWidth(line), 0)))173}174175// run is text in one style.176type run struct {177 text string178 style *lipgloss.Style179}180181// formulaLines draws the formula as it stands in lines of w columns, at182// most max of them: those from the line the next part starts on.183func (v *View) formulaLines(w, most int) []string {184 th, loc := v.h.Theme(), v.h.Locale()185 both := th.Evaluated.Bold(true).Underline(true)186 var lines [][]run187 var cur []run188 col, nextLine := 0, -1189 for _, sg := range v.Steps().Text() {190 style := &th.MenuBar191 switch {192 case sg.Value && sg.Next:193 style = &both194 case sg.Value:195 style = &th.Evaluated196 case sg.Next:197 style = &th.EvalNext198 }199 if sg.Next && nextLine < 0 {200 nextLine = len(lines)201 }202 for _, r := range formula.Localize(sg.Text, loc) {203 rw := ansi.StringWidth(string(r))204 if col+rw > w && col > 0 {205 lines, cur, col = append(lines, cur), nil, 0206 }207 if n := len(cur); n > 0 && cur[n-1].style == style {208 cur[n-1].text += string(r)209 } else {210 cur = append(cur, run{string(r), style})211 }212 col += rw213 }214 }215 lines = append(lines, cur)216 start := 0217 if nextLine >= 0 && len(lines) > most {218 start = min(max(nextLine-1, 0), len(lines)-most)219 }220 out := make([]string, 0, min(len(lines), most))221 for _, l := range lines[start:min(len(lines), start+most)] {222 var b strings.Builder223 for _, r := range l {224 b.WriteString(r.style.Render(r.text))225 }226 out = append(out, b.String())227 }228 return out229}