find.go was 511 lines; searching and replacing stay, and how the bar is drawn on the context line, clicked and described on the status line move to their own file.
- commit
- 9ee03491c3e32598b19f41e356ee0fcf83216d40
- parent
- 139f5c2acbdd1568ba826575e0129790a26a94fb
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 175 | ---------------------------- |
| A | internal/ | 185 | ++++++++++++++++++++++++++++++ |
@@ -4,10 +4,8 @@ import (
44 "log/slog"55 "slices"66 "strconv"7- "strings"87 98 tea "charm.land/bubbletea/v2"10- "github.com/charmbracelet/x/ansi"119 1210 "github.com/FelineStateMachine/012/internal/sheet"1311 "github.com/FelineStateMachine/012/internal/telemetry"@@ -326,179 +324,6 @@ func cellCount(n int) string {
326324 return strconv.Itoa(n) + " cells"327325 }328326 329-// Pieces of the bar, left to right: each field, then the option chips.330-type findPart struct {331- text string332- field int // -1 for chips333- toggle string // the Alt key a chip toggles334-}335-336-func (f *findBar) parts(m *Model) []findPart {337- label := func(name string, i int) string {338- style := m.th.Muted339- if f.field == i {340- style = m.th.Key341- }342- return style.Render(name) + m.th.Muted.Render(searchPrompt)343- }344- parts := []findPart{{text: label("Find", 0) + f.fieldText(m, 0), field: 0}}345- if f.replace {346- parts = append(parts, findPart{text: label("Replace", 1) + f.fieldText(m, 1), field: 1})347- }348- chip := func(on bool, name, key string) findPart {349- style := m.th.Muted350- if on {351- style = m.th.MenuSelected352- }353- return findPart{text: style.Render(" " + name + " "), field: -1, toggle: key}354- }355- parts = append(parts,356- chip(f.opts.MatchCase, "Aa", "alt+c"),357- chip(f.opts.WholeCell, "Whole", "alt+w"),358- chip(f.opts.Regex, ".*", "alt+r"),359- chip(f.opts.InFormulas, "=", "alt+="))360- switch {361- case f.where == inRange:362- parts = append(parts, chip(true, "in "+f.scope.String(), "alt+s"))363- case f.where == inAll:364- parts = append(parts, chip(true, "in all sheets", "alt+s"))365- case f.scope != nil || m.book().Len() > 1:366- parts = append(parts, chip(false, "in "+m.sheet.Name(), "alt+s"))367- }368- return parts369-}370-371-// fieldText is a field's text: the live edit buffer when focused.372-func (f *findBar) fieldText(m *Model, i int) string {373- if f.field == i {374- return m.line.text()375- }376- return f.fields[i]377-}378-379-// gapAfter separates a part from the next: wider after a field, so the380-// query and the chips read as separate groups.381-func gapAfter(p findPart) string {382- if p.field >= 0 {383- return " "384- }385- return " "386-}387-388-// spans returns each part with the x where it starts on the context line.389-func (f *findBar) spans(m *Model) ([]findPart, []int) {390- parts := f.parts(m)391- xs := make([]int, len(parts))392- x := 0393- for i, p := range parts {394- xs[i] = x395- x += ansi.StringWidth(p.text) + len(gapAfter(p))396- }397- return parts, xs398-}399-400-// line renders the bar for the context line, with the match count on the401-// right.402-func (f *findBar) line(m *Model) (left, right string) {403- var b strings.Builder404- parts := f.parts(m)405- for i, p := range parts {406- b.WriteString(p.text)407- if i < len(parts)-1 {408- b.WriteString(gapAfter(p))409- }410- }411- switch {412- case f.err != "":413- right = m.th.Warning.Render(f.err)414- case f.fields[0] == "":415- case len(f.matches) == 0:416- right = m.th.Warning.Render("No matches")417- default:418- where := ""419- if f.where == inAll {420- where = " on " + f.matches[f.cur].s.Name()421- }422- right = m.th.Muted.Render(strconv.Itoa(f.cur+1) + " of " + strconv.Itoa(len(f.matches)) + where)423- }424- return b.String(), right425-}426-427-// cursor puts the terminal cursor in the focused field, whose text ends428-// its part.429-func (f *findBar) cursor(m *Model) (x, y int) {430- parts, xs := f.spans(m)431- for i, p := range parts {432- if p.field == f.field {433- return xs[i] + ansi.StringWidth(p.text) - ansi.StringWidth(m.line.tail()), contextLine434- }435- }436- return 0, contextLine437-}438-439-// mouse focuses a field or flips a chip on the bar; a click anywhere440-// else closes the bar and lands where it was clicked.441-func (f *findBar) mouse(m *Model, e mouseEvent) tea.Cmd {442- if e.kind != mousePress {443- return nil444- }445- if e.y != contextLine {446- m.find = f447- m.closeOverlay()448- return m.handlePress(tea.Mouse{X: e.x, Y: e.y, Button: e.button})449- }450- parts, xs := f.spans(m)451- for i, p := range parts {452- if e.x >= xs[i] && e.x < xs[i]+ansi.StringWidth(p.text) {453- if p.field >= 0 {454- f.focus(m, p.field)455- return nil456- }457- return f.key(m, keyFor(p.toggle))458- }459- }460- return nil461-}462-463-// keyFor makes a key press from a keystroke like "alt+c".464-func keyFor(s string) tea.KeyPressMsg {465- k := tea.KeyPressMsg{}466- parts := strings.Split(s, "+")467- for _, p := range parts[:len(parts)-1] {468- if p == "alt" {469- k.Mod |= tea.ModAlt470- }471- }472- k.Code = rune(parts[len(parts)-1][0])473- return k474-}475-476-// status shows the keys for moving and replacing, and a reminder of the477-// option keys when there's room. Narrow screens keep the most useful keys.478-func (f *findBar) status(m *Model) (string, string) {479- pairs := []string{"Enter", "next", "Shift+Enter", "previous", "Esc", "close"}480- if f.replace {481- pairs = []string{"Enter", "replace", "Ctrl+Enter", "all", "Tab", "field", "Esc", "close"}482- }483- desc := "Alt+C/W/R/= options"484- if f.scope != nil || m.book().Len() > 1 {485- desc = "Alt+C/W/R/=/S options"486- }487- for {488- keys := m.th.KeyHints(pairs...)489- switch {490- case ansi.StringWidth(desc)+3+ansi.StringWidth(keys) <= m.width:491- return m.th.Muted.Render(desc), keys492- case desc != "":493- desc = ""494- case len(pairs) > 2:495- pairs = append(pairs[:len(pairs)-4], pairs[len(pairs)-2:]...) // keep Esc496- default:497- return "", keys498- }499- }500-}501-502327 // found reports whether a is a match of the open find bar.503328 func (m *Model) found(a sheet.Addr) bool {504329 f, ok := m.overlay.(*findBar)@@ -0,0 +1,185 @@
1+package ui2+3+import (4+ "strconv"5+ "strings"6+7+ tea "charm.land/bubbletea/v2"8+ "github.com/charmbracelet/x/ansi"9+)10+11+// The find bar as the context line draws it, and its mouse targets: the12+// fields, then the option chips, with the match count on the right.13+14+// Pieces of the bar, left to right: each field, then the option chips.15+type findPart struct {16+ text string17+ field int // -1 for chips18+ toggle string // the Alt key a chip toggles19+}20+21+func (f *findBar) parts(m *Model) []findPart {22+ label := func(name string, i int) string {23+ style := m.th.Muted24+ if f.field == i {25+ style = m.th.Key26+ }27+ return style.Render(name) + m.th.Muted.Render(searchPrompt)28+ }29+ parts := []findPart{{text: label("Find", 0) + f.fieldText(m, 0), field: 0}}30+ if f.replace {31+ parts = append(parts, findPart{text: label("Replace", 1) + f.fieldText(m, 1), field: 1})32+ }33+ chip := func(on bool, name, key string) findPart {34+ style := m.th.Muted35+ if on {36+ style = m.th.MenuSelected37+ }38+ return findPart{text: style.Render(" " + name + " "), field: -1, toggle: key}39+ }40+ parts = append(parts,41+ chip(f.opts.MatchCase, "Aa", "alt+c"),42+ chip(f.opts.WholeCell, "Whole", "alt+w"),43+ chip(f.opts.Regex, ".*", "alt+r"),44+ chip(f.opts.InFormulas, "=", "alt+="))45+ switch {46+ case f.where == inRange:47+ parts = append(parts, chip(true, "in "+f.scope.String(), "alt+s"))48+ case f.where == inAll:49+ parts = append(parts, chip(true, "in all sheets", "alt+s"))50+ case f.scope != nil || m.book().Len() > 1:51+ parts = append(parts, chip(false, "in "+m.sheet.Name(), "alt+s"))52+ }53+ return parts54+}55+56+// fieldText is a field's text: the live edit buffer when focused.57+func (f *findBar) fieldText(m *Model, i int) string {58+ if f.field == i {59+ return m.line.text()60+ }61+ return f.fields[i]62+}63+64+// gapAfter separates a part from the next: wider after a field, so the65+// query and the chips read as separate groups.66+func gapAfter(p findPart) string {67+ if p.field >= 0 {68+ return " "69+ }70+ return " "71+}72+73+// spans returns each part with the x where it starts on the context line.74+func (f *findBar) spans(m *Model) ([]findPart, []int) {75+ parts := f.parts(m)76+ xs := make([]int, len(parts))77+ x := 078+ for i, p := range parts {79+ xs[i] = x80+ x += ansi.StringWidth(p.text) + len(gapAfter(p))81+ }82+ return parts, xs83+}84+85+// line renders the bar for the context line, with the match count on the86+// right.87+func (f *findBar) line(m *Model) (left, right string) {88+ var b strings.Builder89+ parts := f.parts(m)90+ for i, p := range parts {91+ b.WriteString(p.text)92+ if i < len(parts)-1 {93+ b.WriteString(gapAfter(p))94+ }95+ }96+ switch {97+ case f.err != "":98+ right = m.th.Warning.Render(f.err)99+ case f.fields[0] == "":100+ case len(f.matches) == 0:101+ right = m.th.Warning.Render("No matches")102+ default:103+ where := ""104+ if f.where == inAll {105+ where = " on " + f.matches[f.cur].s.Name()106+ }107+ right = m.th.Muted.Render(strconv.Itoa(f.cur+1) + " of " + strconv.Itoa(len(f.matches)) + where)108+ }109+ return b.String(), right110+}111+112+// cursor puts the terminal cursor in the focused field, whose text ends113+// its part.114+func (f *findBar) cursor(m *Model) (x, y int) {115+ parts, xs := f.spans(m)116+ for i, p := range parts {117+ if p.field == f.field {118+ return xs[i] + ansi.StringWidth(p.text) - ansi.StringWidth(m.line.tail()), contextLine119+ }120+ }121+ return 0, contextLine122+}123+124+// mouse focuses a field or flips a chip on the bar; a click anywhere125+// else closes the bar and lands where it was clicked.126+func (f *findBar) mouse(m *Model, e mouseEvent) tea.Cmd {127+ if e.kind != mousePress {128+ return nil129+ }130+ if e.y != contextLine {131+ m.find = f132+ m.closeOverlay()133+ return m.handlePress(tea.Mouse{X: e.x, Y: e.y, Button: e.button})134+ }135+ parts, xs := f.spans(m)136+ for i, p := range parts {137+ if e.x >= xs[i] && e.x < xs[i]+ansi.StringWidth(p.text) {138+ if p.field >= 0 {139+ f.focus(m, p.field)140+ return nil141+ }142+ return f.key(m, keyFor(p.toggle))143+ }144+ }145+ return nil146+}147+148+// keyFor makes a key press from a keystroke like "alt+c".149+func keyFor(s string) tea.KeyPressMsg {150+ k := tea.KeyPressMsg{}151+ parts := strings.Split(s, "+")152+ for _, p := range parts[:len(parts)-1] {153+ if p == "alt" {154+ k.Mod |= tea.ModAlt155+ }156+ }157+ k.Code = rune(parts[len(parts)-1][0])158+ return k159+}160+161+// status shows the keys for moving and replacing, and a reminder of the162+// option keys when there's room. Narrow screens keep the most useful keys.163+func (f *findBar) status(m *Model) (string, string) {164+ pairs := []string{"Enter", "next", "Shift+Enter", "previous", "Esc", "close"}165+ if f.replace {166+ pairs = []string{"Enter", "replace", "Ctrl+Enter", "all", "Tab", "field", "Esc", "close"}167+ }168+ desc := "Alt+C/W/R/= options"169+ if f.scope != nil || m.book().Len() > 1 {170+ desc = "Alt+C/W/R/=/S options"171+ }172+ for {173+ keys := m.th.KeyHints(pairs...)174+ switch {175+ case ansi.StringWidth(desc)+3+ansi.StringWidth(keys) <= m.width:176+ return m.th.Muted.Render(desc), keys177+ case desc != "":178+ desc = ""179+ case len(pairs) > 2:180+ pairs = append(pairs[:len(pairs)-4], pairs[len(pairs)-2:]...) // keep Esc181+ default:182+ return "", keys183+ }184+ }185+}