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 / findbar / view.go
view.go 4.7K
1package findbar23import (4 "strconv"5 "strings"67 tea "charm.land/bubbletea/v2"8 "github.com/charmbracelet/x/ansi"910 "src.rest/cat/012/internal/ui/overlay"11)1213// The find bar as the context line draws it, and its mouse targets: the14// fields, then the option chips, with the match count on the right.1516// Pieces of the bar, left to right: each field, then the option chips.17type part struct {18 text string19 field int // -1 for chips20 toggle string // the Alt key a chip toggles21}2223func (f *Bar) parts() []part {24 th := f.h.Theme()25 label := func(name string, i int) string {26 style := th.Muted27 if f.field == i {28 style = th.Key29 }30 return style.Render(name) + th.Muted.Render(overlay.SearchPrompt)31 }32 parts := []part{{text: label("Find", 0) + f.fieldText(0), field: 0}}33 if f.replace {34 parts = append(parts, part{text: label("Replace", 1) + f.fieldText(1), field: 1})35 }36 chip := func(on bool, name, key string) part {37 style := th.Muted38 if on {39 style = th.MenuSelected40 }41 return part{text: style.Render(" " + name + " "), field: -1, toggle: key}42 }43 parts = append(parts,44 chip(f.opts.MatchCase, "Aa", "alt+c"),45 chip(f.opts.WholeCell, "Whole", "alt+w"),46 chip(f.opts.Regex, ".*", "alt+r"),47 chip(f.opts.InFormulas, "=", "alt+="))48 switch {49 case f.where == inRange:50 parts = append(parts, chip(true, "in "+f.scope.String(), "alt+s"))51 case f.where == inAll:52 parts = append(parts, chip(true, "in all sheets", "alt+s"))53 case f.scope != nil || len(f.h.Book().Visible()) > 1:54 s, _ := f.h.At()55 parts = append(parts, chip(false, "in "+s.Name(), "alt+s"))56 }57 return parts58}5960// fieldText is a field's text: the live edit buffer when focused.61func (f *Bar) fieldText(i int) string {62 if f.field == i {63 return f.h.Line().Text()64 }65 return f.fields[i]66}6768// gapAfter separates a part from the next: wider after a field, so the69// query and the chips read as separate groups.70func gapAfter(p part) string {71 if p.field >= 0 {72 return " "73 }74 return " "75}7677// spans returns each part with the x where it starts on the context line.78func (f *Bar) spans() ([]part, []int) {79 parts := f.parts()80 xs := make([]int, len(parts))81 x := 082 for i, p := range parts {83 xs[i] = x84 x += ansi.StringWidth(p.text) + len(gapAfter(p))85 }86 return parts, xs87}8889// Layout is empty: the bar is drawn on the context line.90func (f *Bar) Layout() []overlay.Box { return nil }9192// ContextLine renders the bar for the context line, with the match count93// on the right.94func (f *Bar) ContextLine() (left, right string) {95 th := f.h.Theme()96 var b strings.Builder97 parts := f.parts()98 for i, p := range parts {99 b.WriteString(p.text)100 if i < len(parts)-1 {101 b.WriteString(gapAfter(p))102 }103 }104 switch {105 case f.err != "":106 right = th.Warning.Render(f.err)107 case f.fields[0] == "":108 case len(f.matches) == 0:109 right = th.Warning.Render("No matches")110 default:111 where := ""112 if f.where == inAll {113 where = " on " + f.matches[f.cur].Sheet.Name()114 }115 right = th.Muted.Render(strconv.Itoa(f.cur+1) + " of " + strconv.Itoa(len(f.matches)) + where)116 }117 return b.String(), right118}119120// Cursor puts the terminal cursor in the focused field, whose text ends121// its part.122func (f *Bar) Cursor() (x, y int) {123 parts, xs := f.spans()124 for i, p := range parts {125 if p.field == f.field {126 return xs[i] + ansi.StringWidth(p.text) - ansi.StringWidth(f.h.Line().Tail()), overlay.ContextLine127 }128 }129 return 0, overlay.ContextLine130}131132// Mouse focuses a field or flips a chip on the bar; a click anywhere133// else closes the bar and lands where it was clicked.134func (f *Bar) Mouse(e overlay.MouseEvent) tea.Cmd {135 if e.Kind != overlay.MousePress {136 return nil137 }138 if e.Y != overlay.ContextLine {139 f.h.Leave()140 return f.h.Press(e.X, e.Y, e.Button)141 }142 parts, xs := f.spans()143 for i, p := range parts {144 if e.X >= xs[i] && e.X < xs[i]+ansi.StringWidth(p.text) {145 if p.field >= 0 {146 f.focus(p.field)147 return nil148 }149 return f.Key(overlay.KeyFor(p.toggle))150 }151 }152 return nil153}154155// Status shows the keys for moving and replacing, and a reminder of the156// option keys when there's room. Narrow screens keep the most useful keys.157func (f *Bar) Status() (string, string) {158 th := f.h.Theme()159 width, _ := f.h.Size()160 pairs := []string{"Enter", "next", "Shift+Enter", "previous", "Esc", "close"}161 if f.replace {162 pairs = []string{"Enter", "replace", "Ctrl+Enter", "all", "Tab", "field", "Esc", "close"}163 }164 desc := "Alt+C/W/R/= options"165 if f.scope != nil || len(f.h.Book().Visible()) > 1 {166 desc = "Alt+C/W/R/=/S options"167 }168 for {169 keys := th.KeyHints(pairs...)170 switch {171 case ansi.StringWidth(desc)+3+ansi.StringWidth(keys) <= width:172 return th.Muted.Render(desc), keys173 case desc != "":174 desc = ""175 case len(pairs) > 2:176 pairs = append(pairs[:len(pairs)-4], pairs[len(pairs)-2:]...) // keep Esc177 default:178 return "", keys179 }180 }181}