The bar is the question and its choices, which run as closures over the model; the model asks through ask(question). Tested against a fake host.
- commit
- dd5cb3abc4aef9480fdcddb1b297b075f4b47491
- parent
- 9471af439d88bfe1f5f990ec1590e858a84452cb
- author
- Dami <dami.etoile@gmail.com>
- kind
- refactor risk score 1.7 of 3, Jev's estimate
| A | internal/ | 124 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 90 | ++++++++++++++++++++++ |
| M | internal/ | 86 | +++------------------ |
| M | internal/ | 5 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 2 | + |
| M | internal/ | 3 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 13 | +-- |
| M | internal/ | 3 | +- |
| M | internal/ | 4 | +- |
| M | internal/ | 7 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 2 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 3 | +- |
| M | internal/ | 4 | +- |
| M | internal/ | 3 | +- |
@@ -0,0 +1,124 @@
1+// Package choicebar is a small question asked on the context line and2+// answered with a key, the way terminal programs confirm things, e.g.3+// "You have unsaved changes. Enter Save and quit D Discard Esc Cancel".4+// It takes the keyboard like an overlay but draws no box, and knows the5+// UI only through Host.6+package choicebar7+8+import (9+ "strings"10+11+ tea "charm.land/bubbletea/v2"12+ "github.com/charmbracelet/x/ansi"13+14+ "github.com/FelineStateMachine/012/internal/ui/overlay"15+ "github.com/FelineStateMachine/012/internal/ui/theme"16+)17+18+// Host is what a choice bar needs of the UI.19+type Host interface {20+ Theme() *theme.Theme21+ // Close closes the open overlay.22+ Close()23+ // RecordAnswer tells a macro recording the key chosen, for the24+ // command that asked; Esc cancels it.25+ RecordAnswer(answer string, cancelled bool)26+}27+28+// Choice is one answer: a key as tea reports it ("enter", "d"), its29+// label, and what choosing it does, after the bar has closed.30+type Choice struct {31+ Key string32+ Label string33+ Run func() tea.Cmd34+}35+36+// Bar is the question and its choices.37+type Bar struct {38+ h Host39+ Msg string40+ Warn bool // the message is a warning, e.g. about losing work41+ Desc string // more on the status line, when the question needs it42+ Choices []Choice43+}44+45+// New returns a bar asking msg.46+func New(h Host, msg string, warn bool, desc string, choices []Choice) *Bar {47+ return &Bar{h: h, Msg: msg, Warn: warn, Desc: desc, Choices: choices}48+}49+50+func (b *Bar) Indicator() string { return "MENU" }51+func (b *Bar) Layout() []overlay.Box { return nil }52+func (b *Bar) Status() (string, string) { return b.Desc, "" }53+54+func (b *Bar) Key(k tea.KeyPressMsg) tea.Cmd {55+ key := strings.ToLower(k.String())56+ for i, ch := range b.Choices {57+ if key == ch.Key {58+ return b.Choose(i)59+ }60+ }61+ return nil62+}63+64+// Find is the index of the choice with key or label answer, ignoring65+// case, or -1.66+func (b *Bar) Find(answer string) int {67+ for i, ch := range b.Choices {68+ if strings.EqualFold(ch.Key, answer) || strings.EqualFold(ch.Label, answer) {69+ return i70+ }71+ }72+ return -173+}74+75+// Choose closes the bar and runs choice i.76+func (b *Bar) Choose(i int) tea.Cmd {77+ ch := b.Choices[i]78+ b.h.Close()79+ cmd := ch.Run()80+ b.h.RecordAnswer(ch.Key, ch.Key == "esc")81+ return cmd82+}83+84+// Mouse runs a choice when its key chip or label is clicked. A click85+// anywhere else cancels, like Esc.86+func (b *Bar) Mouse(e overlay.MouseEvent) tea.Cmd {87+ if e.Kind != overlay.MousePress {88+ return nil89+ }90+ if e.Y == overlay.ContextLine {91+ x := ansi.StringWidth(b.prefix())92+ for i, ch := range b.Choices {93+ w := ansi.StringWidth(b.item(ch))94+ if e.X >= x && e.X < x+w {95+ return b.Choose(i)96+ }97+ x += w + len(gap)98+ }99+ }100+ b.h.Close()101+ return nil102+}103+104+const gap = " "105+106+func (b *Bar) prefix() string {107+ if b.Warn {108+ return b.h.Theme().Warning.Render(b.Msg) + gap109+ }110+ return b.Msg + gap111+}112+113+func (b *Bar) item(ch Choice) string {114+ return b.h.Theme().Chip(theme.KeyLabel(ch.Key)) + " " + ch.Label115+}116+117+// ContextLine renders the bar for the context line.118+func (b *Bar) ContextLine() (string, string) {119+ items := make([]string, len(b.Choices))120+ for i, ch := range b.Choices {121+ items[i] = b.item(ch)122+ }123+ return b.prefix() + strings.Join(items, gap), ""124+}@@ -0,0 +1,90 @@
1+package choicebar2+3+import (4+ "strings"5+ "testing"6+7+ tea "charm.land/bubbletea/v2"8+ "github.com/charmbracelet/x/ansi"9+10+ "github.com/FelineStateMachine/012/internal/ui/overlay"11+ "github.com/FelineStateMachine/012/internal/ui/theme"12+)13+14+// fakeHost records what the bar did.15+type fakeHost struct {16+ th theme.Theme17+ closed int18+ answers []string19+}20+21+func (f *fakeHost) Theme() *theme.Theme { return &f.th }22+func (f *fakeHost) Close() { f.closed++ }23+24+func (f *fakeHost) RecordAnswer(answer string, cancelled bool) {25+ if cancelled {26+ answer = "(cancelled)"27+ }28+ f.answers = append(f.answers, answer)29+}30+31+// newBar asks about unsaved changes, noting the choice made in *ran.32+func newBar(ran *string) (*Bar, *fakeHost) {33+ h := &fakeHost{th: theme.New(true)}34+ run := func(what string) func() tea.Cmd { return func() tea.Cmd { *ran = what; return nil } }35+ return New(h, "You have unsaved changes.", true, "More", []Choice{36+ {Key: "enter", Label: "Save and quit", Run: run("save")},37+ {Key: "d", Label: "Discard", Run: run("discard")},38+ {Key: "esc", Label: "Cancel", Run: run("cancel")},39+ }), h40+}41+42+func TestKeysChoose(t *testing.T) {43+ var ran string44+ b, h := newBar(&ran)45+ b.Key(tea.KeyPressMsg{Code: 'x', Text: "x"})46+ if ran != "" || h.closed != 0 {47+ t.Fatalf("a key of no choice acted: %q, closed %d", ran, h.closed)48+ }49+ b.Key(tea.KeyPressMsg{Code: 'D', Text: "D", Mod: tea.ModShift})50+ if ran != "discard" || h.closed != 1 || strings.Join(h.answers, ",") != "d" {51+ t.Fatalf("D: ran %q, closed %d, answers %v", ran, h.closed, h.answers)52+ }53+ b.Key(tea.KeyPressMsg{Code: tea.KeyEscape})54+ if ran != "cancel" || h.answers[1] != "(cancelled)" {55+ t.Fatalf("Esc: ran %q, answers %v", ran, h.answers)56+ }57+}58+59+func TestFindByKeyOrLabel(t *testing.T) {60+ var ran string61+ b, _ := newBar(&ran)62+ for answer, want := range map[string]int{"enter": 0, "DISCARD": 1, "Esc": 2, "nope": -1} {63+ if got := b.Find(answer); got != want {64+ t.Errorf("Find(%q) = %d, want %d", answer, got, want)65+ }66+ }67+}68+69+func TestClickChoosesOrCancels(t *testing.T) {70+ var ran string71+ b, h := newBar(&ran)72+ line, _ := b.ContextLine()73+ plain := ansi.Strip(line)74+ if !strings.HasPrefix(plain, "You have unsaved changes. ") || !strings.Contains(plain, "Discard ") {75+ t.Fatalf("context line %q", plain)76+ }77+ x := strings.Index(plain, "Discard")78+ b.Mouse(overlay.MouseEvent{Kind: overlay.MousePress, X: x, Y: overlay.ContextLine})79+ if ran != "discard" {80+ t.Fatalf("clicking Discard ran %q", ran)81+ }82+ ran = ""83+ b.Mouse(overlay.MouseEvent{Kind: overlay.MousePress, X: 0, Y: overlay.ContextLine + 3})84+ if ran != "" || h.closed != 2 {85+ t.Fatalf("a click elsewhere should only close: ran %q, closed %d", ran, h.closed)86+ }87+ if desc, _ := b.Status(); desc != "More" {88+ t.Fatalf("status %q", desc)89+ }90+}@@ -1,21 +1,15 @@
11 package ui22 33 import (4- "strings"5-64 tea "charm.land/bubbletea/v2"7- "github.com/charmbracelet/x/ansi"85 9- "github.com/FelineStateMachine/012/internal/ui/overlay"10- "github.com/FelineStateMachine/012/internal/ui/theme"6+ "github.com/FelineStateMachine/012/internal/ui/choicebar"117 )128 13-// choiceBar is a small question asked on the context line and answered14-// with a key, the way terminal programs confirm things, e.g.9+// question is a choice bar (package choicebar) as the model asks it:10+// a small question on the context line answered with a key, e.g.1511 // "You have unsaved changes. Enter Save and quit D Discard Esc Cancel".16-// It takes the keyboard like an overlay but draws no box.17-type choiceBar struct {18- m *Model // the model it acts on12+type question struct {1913 msg string2014 warn bool // the message is a warning, e.g. about losing work2115 desc string // more on the status line, when the question needs it@@ -28,70 +22,13 @@ type choice struct {
2822 run func(m *Model) tea.Cmd2923 }3024 31-func (c *choiceBar) Indicator() string { return "MENU" }32-func (c *choiceBar) Layout() []overlay.Box { return nil }33-func (c *choiceBar) Status() (string, string) { return c.desc, "" }34-35-func (c *choiceBar) Key(k tea.KeyPressMsg) tea.Cmd {36- m := c.m37- key := strings.ToLower(k.String())38- for _, ch := range c.choices {39- if key == ch.key {40- return c.choose(m, ch)41- }42- }43- return nil44-}45-46-func (c *choiceBar) choose(m *Model, ch choice) tea.Cmd {47- m.closeOverlay()48- cmd := ch.run(m)49- m.recordAnswer(ch.key, ch.key == "esc")50- return cmd51-}52-53-// Mouse runs a choice when its key chip or label is clicked. A click54-// anywhere else cancels, like Esc.55-func (c *choiceBar) Mouse(e overlay.MouseEvent) tea.Cmd {56- m := c.m57- if e.Kind != overlay.MousePress {58- return nil59- }60- if e.Y == contextLine {61- x := ansi.StringWidth(c.prefix(m))62- for _, ch := range c.choices {63- w := ansi.StringWidth(c.item(m, ch))64- if e.X >= x && e.X < x+w {65- return c.choose(m, ch)66- }67- x += w + len(choiceGap)68- }69- }70- m.closeOverlay()71- return nil72-}73-74-const choiceGap = " "75-76-func (c *choiceBar) prefix(m *Model) string {77- if c.warn {78- return m.th.Warning.Render(c.msg) + choiceGap79- }80- return c.msg + choiceGap81-}82-83-func (c *choiceBar) item(m *Model, ch choice) string {84- return m.th.Chip(theme.KeyLabel(ch.key)) + " " + ch.label85-}86-87-// ContextLine renders the bar for the context line.88-func (c *choiceBar) ContextLine() (string, string) {89- m := c.m90- items := make([]string, len(c.choices))91- for i, ch := range c.choices {92- items[i] = c.item(m, ch)25+// ask opens a choice bar with q's question.26+func (m *Model) ask(q question) {27+ chs := make([]choicebar.Choice, len(q.choices))28+ for i, ch := range q.choices {29+ chs[i] = choicebar.Choice{Key: ch.key, Label: ch.label, Run: func() tea.Cmd { return ch.run(m) }}9330 }94- return c.prefix(m) + strings.Join(items, choiceGap), ""31+ m.openOverlay(choicebar.New(m.host(), q.msg, q.warn, q.desc, chs))9532 }9633 9734 // quit exits, asking first when there are unsaved changes. Enter saves@@ -100,8 +37,7 @@ func (m *Model) quit() tea.Cmd {
10037 if !m.changed {10138 return m.exit()10239 }103- m.openOverlay(&choiceBar{104- m: m,40+ m.ask(question{10541 msg: "You have unsaved changes.",10642 warn: true,10743 choices: []choice{@@ -143,8 +143,7 @@ func (m *Model) confirmReplace(msg string, do func(*Model) tea.Cmd, replaces boo
143143 if !replaces {144144 return do(m)145145 }146- m.openOverlay(&choiceBar{147- m: m,146+ m.ask(question{148147 msg: msg,149148 warn: true,150149 choices: []choice{@@ -184,6 +183,6 @@ func (m *Model) saveImported() tea.Cmd {
184183 m.quitAfterSave = false185184 return nil186185 }})187- m.openOverlay(&choiceBar{m: m, msg: filepath.Base(m.xfer.Source) + " was imported.", choices: choices})186+ m.ask(question{msg: filepath.Base(m.xfer.Source) + " was imported.", choices: choices})188187 return nil189188 }@@ -312,8 +312,7 @@ func globEscape(dir string) string {
312312 313313 func (m *Model) handleSaved(msg savedMsg) tea.Cmd {314314 if msg.conflict {315- m.openOverlay(&choiceBar{316- m: m,315+ m.ask(question{317316 msg: filepath.Base(msg.name) + " changed on disk since it was opened.",318317 warn: true,319318 choices: []choice{@@ -29,8 +29,7 @@ func init() {
2929 return nil3030 }},3131 &command{id: "help.about", macro: macroNever, title: "About 012", desc: "Show the version", run: func(m *Model) tea.Cmd {32- m.openOverlay(&choiceBar{33- m: m,32+ m.ask(question{3433 msg: "012 " + version() + ": Lotus 1-2-3 looks, Google Sheets keys.",3534 choices: []choice{3635 {key: "esc", label: "Close", run: func(*Model) tea.Cmd { return nil }},@@ -5,6 +5,7 @@ import (
55 66 "github.com/FelineStateMachine/012/internal/sheet"77 "github.com/FelineStateMachine/012/internal/telemetry"8+ "github.com/FelineStateMachine/012/internal/ui/choicebar"89 "github.com/FelineStateMachine/012/internal/ui/cmdline"910 "github.com/FelineStateMachine/012/internal/ui/findbar"1011 "github.com/FelineStateMachine/012/internal/ui/lineedit"@@ -28,6 +29,7 @@ var (
2829 _ findbar.Host = host{}2930 _ themepicker.Host = host{}3031 _ rules.Host = host{}32+ _ choicebar.Host = host{}3133 )3234 3335 // The in-package hosts the model implements itself.@@ -7,6 +7,7 @@ import (
77 tea "charm.land/bubbletea/v2"88 99 "github.com/FelineStateMachine/012/internal/sheet"10+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1011 )1112 1213 // rect parses a range for tests.@@ -155,7 +156,7 @@ func TestMergeCells(t *testing.T) {
155156 m.cur = addr("A1")156157 m.selectRect(rect("A1:C1"))157158 run(m, m.runCommand("format.merge_all"))158- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "keeps only the top-left value") ||159+ if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "keeps only the top-left value") ||159160 !strings.Contains(line(m, m.height-1), "clears B1") {160161 t.Fatalf("no warning: %q", line(m, contextLine))161162 }@@ -144,7 +144,8 @@ func (m *Model) merge(k sheet.MergeKind) tea.Cmd {
144144 if !loses {145145 return do(m)146146 }147- m.openOverlay(&choiceBar{m: m, msg: "Merging keeps only the top-left value.", warn: true,147+ m.ask(question{148+ msg: "Merging keeps only the top-left value.", warn: true,148149 desc: "Merging clears " + lost.String() + " and the other values; undo brings them back",149150 choices: []choice{150151 {key: "enter", label: "Merge", run: do},@@ -130,8 +130,7 @@ func (m *Model) runMacro(mc sheet.Macro) tea.Cmd {
130130 case m.macroTrusted():131131 return m.startMacro(mc)132132 }133- m.openOverlay(&choiceBar{134- m: m,133+ m.ask(question{135134 msg: "Trust this file's macros?",136135 desc: "They were made on another computer and can change the file. Trusting covers all of them from now on.",137136 warn: true,@@ -3,11 +3,11 @@ package ui
33 import (44 "errors"55 "fmt"6- "slices"76 "strconv"87 "strings"98 109 "github.com/FelineStateMachine/012/internal/sheet"10+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1111 "github.com/FelineStateMachine/012/internal/ui/picker"1212 )1313 @@ -320,17 +320,14 @@ func (h scriptHost) choose(c *command, answer *string) error {
320320 if p, ok := m.overlay.(*picker.Picker); ok && p.Answers {321321 return h.pickAnswer(c, p, answer)322322 }323- bar, ok := m.overlay.(*choiceBar)323+ bar, ok := m.overlay.(*choicebar.Bar)324324 if !ok {325325 m.closeOverlay()326326 return fmt.Errorf("%s opens a dialog; it can't run in a macro", c.title)327327 }328328 if answer != nil {329- i := slices.IndexFunc(bar.choices, func(ch choice) bool {330- return strings.EqualFold(ch.key, *answer) || strings.EqualFold(ch.label, *answer)331- })332- if i >= 0 {333- m.macros.cmds = append(m.macros.cmds, bar.choose(m, bar.choices[i]))329+ if i := bar.Find(*answer); i >= 0 {330+ m.macros.cmds = append(m.macros.cmds, bar.Choose(i))334331 if m.mode == modeError {335332 return h.failure()336333 }@@ -338,7 +335,7 @@ func (h scriptHost) choose(c *command, answer *string) error {
338335 }339336 }340337 m.closeOverlay()341- return fmt.Errorf("%s asks %q: give run(%q, answer=...) with one of its keys", c.title, bar.msg, c.id)338+ return fmt.Errorf("%s asks %q: give run(%q, answer=...) with one of its keys", c.title, bar.Msg, c.id)342339 }343340 344341 // pickAnswer answers a picker that asks a command's question with the@@ -8,6 +8,7 @@ import (
88 99 "github.com/FelineStateMachine/012/internal/macro"1010 "github.com/FelineStateMachine/012/internal/sheet"11+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1112 "github.com/FelineStateMachine/012/internal/ui/picker"1213 )1314 @@ -131,7 +132,7 @@ func (m *Model) recordingCommand(c *command) tea.Cmd {
131132 r.depth++132133 cmd := c.run(m)133134 r.depth--134- _, choosing := m.overlay.(*choiceBar)135+ _, choosing := m.overlay.(*choicebar.Bar)135136 if p, ok := m.overlay.(*picker.Picker); ok && p.Answers {136137 choosing = true137138 }@@ -101,8 +101,8 @@ func (m *Model) askProtected(r sheet.Rect, retry func(*Model) tea.Cmd) bool {
101101 if p.Desc != "" {102102 desc = p.Desc + ": shouldn't be changed by accident"103103 }104- m.openOverlay(&choiceBar{105- m: m, msg: msg, warn: true, desc: desc,104+ m.ask(question{105+ msg: msg, warn: true, desc: desc,106106 choices: []choice{107107 {key: "enter", label: "Edit anyway", run: func(m *Model) tea.Cmd {108108 m.protectOK = true@@ -7,6 +7,7 @@ import (
77 tea "charm.land/bubbletea/v2"88 99 "github.com/FelineStateMachine/012/internal/sheet"10+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1011 )1112 1213 func TestProtectRangeWarns(t *testing.T) {@@ -44,7 +45,7 @@ func TestProtectRangeWarns(t *testing.T) {
4445 // Commands ask through what they edit; outside, nothing asks.4546 press(t, m, "<up>")4647 m.runCommand("clear")47- if _, ok := m.overlay.(*choiceBar); !ok {48+ if _, ok := m.overlay.(*choicebar.Bar); !ok {4849 t.Fatal("clear didn't ask")4950 }5051 press(t, m, "<enter>")@@ -64,7 +65,7 @@ func TestProtectRangeWarns(t *testing.T) {
6465 // Pasted text into the range asks too.6566 press(t, m, "<left>", "<left>", "<down>")6667 send(m, tea.PasteMsg{Content: "1\t2"})67- if _, ok := m.overlay.(*choiceBar); !ok {68+ if _, ok := m.overlay.(*choicebar.Bar); !ok {6869 t.Fatal("paste didn't ask")6970 }7071 press(t, m, "<enter>")@@ -96,7 +97,7 @@ func TestProtectSheetAndPicker(t *testing.T) {
9697 press(t, m, "<esc>")9798 // Anything on a protected sheet asks, inserting rows included.9899 m.runCommand("insert.row_above")99- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "Sheet1 is protected.") {100+ if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "Sheet1 is protected.") {100101 t.Fatalf("insert didn't ask: %q", line(m, contextLine))101102 }102103 press(t, m, "<esc>")@@ -237,8 +237,7 @@ func (m *Model) offerRecovery(name string) {
237237 if name != "" {238238 what = filepath.Base(name)239239 }240- m.openOverlay(&choiceBar{241- m: m,240+ m.ask(question{242241 msg: "Unsaved changes to " + what + " were kept.",243242 warn: true,244243 desc: "Kept " + f.when.Format("Jan 2 15:04") + ", when a session ended unsaved. Later asks again next time",@@ -9,6 +9,7 @@ import (
99 tea "charm.land/bubbletea/v2"1010 1111 "github.com/FelineStateMachine/012/internal/confine"12+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1213 )1314 1415 // Save as onto another file that exists asks first: Enter replaces it,@@ -21,7 +22,7 @@ func TestSaveAsAsksBeforeReplacing(t *testing.T) {
2122 2223 run(m, m.openSave())2324 press(t, m, "other", "<enter>")24- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "other.012 exists.") ||25+ if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "other.012 exists.") ||2526 !strings.Contains(line(m, contextLine), "Replace") {2627 t.Fatalf("no question: overlay %T, %q", m.overlay, line(m, contextLine))2728 }@@ -13,6 +13,7 @@ import (
1313 "github.com/FelineStateMachine/012/internal/confine"1414 "github.com/FelineStateMachine/012/internal/fileio"1515 "github.com/FelineStateMachine/012/internal/sheet"16+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1617 "github.com/FelineStateMachine/012/internal/ui/transfer"1718 )1819 @@ -130,7 +131,7 @@ func TestSaveAsksWhenFileChanged(t *testing.T) {
130131 os.Chtimes("book.012", later, later)131132 132133 press(t, m, "<up>", "again", "<enter>", "<ctrl+s>")133- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "book.012 changed on disk since it was opened") {134+ if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "book.012 changed on disk since it was opened") {134135 t.Fatalf("context line %q, overlay %T", line(m, contextLine), m.overlay)135136 }136137 press(t, m, "<esc>")@@ -262,7 +262,7 @@ func (m *Model) confirmDeleteSheet() tea.Cmd {
262262 if n == 0 {263263 what = "Delete " + s.Name() + " and its charts?"264264 }265- m.openOverlay(&choiceBar{m: m, msg: what, warn: true, choices: []choice{265+ m.ask(question{msg: what, warn: true, choices: []choice{266266 {key: "enter", label: "Delete", run: del},267267 {key: "esc", label: "Cancel", run: func(*Model) tea.Cmd { return nil }},268268 }})@@ -149,8 +149,7 @@ func (m *Model) confirmImport(name string, opt fileio.Options) tea.Cmd {
149149 if !m.changed {150150 return m.startImport(name, opt, transfer.Book)151151 }152- m.openOverlay(&choiceBar{153- m: m,152+ m.ask(question{154153 msg: "Importing replaces this spreadsheet, which has unsaved changes.",155154 warn: true,156155 choices: []choice{@@ -13,6 +13,7 @@ import (
1313 1414 "github.com/FelineStateMachine/012/internal/fileio"1515 "github.com/FelineStateMachine/012/internal/sheet"16+ "github.com/FelineStateMachine/012/internal/ui/choicebar"1617 "github.com/FelineStateMachine/012/internal/ui/transfer"1718 )1819 @@ -91,7 +92,7 @@ func TestImportAsksBeforeReplacingChanges(t *testing.T) {
9192 press(t, m, "keep me", "<enter>")9293 m.runCommand("file.import")9394 press(t, m, "<enter>", "Replace spreadsheet", "<enter>")94- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "Importing replaces this spreadsheet, which has unsaved changes.") {95+ if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(line(m, contextLine), "Importing replaces this spreadsheet, which has unsaved changes.") {9596 t.Fatalf("no warning: %q", line(m, contextLine))9697 }9798 press(t, m, "<esc>")@@ -30,8 +30,8 @@ func (m *Model) askUndoCost(r sheet.Rect, retry func(*Model) tea.Cmd) bool {
3030 return false3131 }3232 protectOK := m.protectOK // agreed to before this asked33- m.openOverlay(&choiceBar{34- m: m, msg: "This can't be undone: it would take " + byteSize(cost) + " of undo history.", warn: true,33+ m.ask(question{34+ msg: "This can't be undone: it would take " + byteSize(cost) + " of undo history.", warn: true,3535 desc: "Going on runs it without undo and forgets the changes before it",3636 choices: []choice{3737 {key: "enter", label: "Go on without undo", run: func(m *Model) tea.Cmd {@@ -5,6 +5,7 @@ import (
55 "testing"66 77 "github.com/FelineStateMachine/012/internal/sheet"8+ "github.com/FelineStateMachine/012/internal/ui/choicebar"89 )910 1011 // A change whose undo step would pass the ceiling asks first: Esc backs@@ -17,7 +18,7 @@ func TestUndoCostAsks(t *testing.T) {
1718 press(t, m, "<up>", "<shift+up>", "<shift+up>", "<shift+up>", "<shift+up>", "<shift+up>", "<shift+up>")1819 m.runCommand("clear")1920 l := line(m, contextLine)20- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(l, "This can't be undone: it would take 0 MB of undo history.") {21+ if _, ok := m.overlay.(*choicebar.Bar); !ok || !strings.Contains(l, "This can't be undone: it would take 0 MB of undo history.") {2122 t.Fatalf("clear didn't ask: %q", l)2223 }2324 press(t, m, "<esc>")