A prompt's keys, answer and drawing go through eight methods of the model; its callbacks are closures over it. Prompts stay in package ui, being one of the model's modes: ranges are pointed at with the grid's movement and drawn by the grid.
- commit
- 21cc23c5e760f2a7d00aec67c485ec4980e58ea9
- parent
- 633fc57739ac4f50acf3031fe6c205d3734fc7a6
- author
- Dami <dami.etoile@gmail.com>
- kind
- refactor risk score 1.6 of 3, Jev's estimate
| M | internal/ | 2 | +- |
| M | internal/ | 4 | +- |
| M | internal/ | 8 | +- |
| M | internal/ | 5 | +- |
| M | internal/ | 6 | +- |
| M | internal/ | 2 | +- |
| M | internal/ | 133 | +++++++++++++++++++----------- |
| M | internal/ | 8 | +- |
@@ -89,7 +89,7 @@ func (e *chartEditor) askAxisEnd(m *Model, label, question, initial string, end
8989 e.reopen(m)9090 return nil9191 })92- m.prompt.onCancel = e.reopen92+ m.prompt.onCancel = func() { e.reopen(m) }9393 }9494 9595 func (e *chartEditor) axisStatus(m *Model) string {@@ -136,7 +136,7 @@ func (e *chartEditor) editRange(m *Model) {
136136 e.reopen(m)137137 return nil138138 })139- m.prompt.onCancel = func(m *Model) {139+ m.prompt.onCancel = func() {140140 m.clearSelection()141141 e.reopen(m)142142 }@@ -150,7 +150,7 @@ func (e *chartEditor) editTitle(m *Model) {
150150 e.reopen(m)151151 return nil152152 })153- m.prompt.onCancel = e.reopen153+ m.prompt.onCancel = func() { e.reopen(m) }154154 }155155 156156 // editorPart is a piece of the editor bar: a type chip or a toggle.@@ -22,7 +22,7 @@ func (m *Model) openWidth() tea.Cmd {
2222 for c := r.From.Col; c <= r.To.Col; c++ {2323 orig[c] = m.sheet.ColWidth(c)2424 }25- restore := func(m *Model) {25+ restore := func() {2626 for c, w := range orig {2727 m.sheet.SetColWidth(c, w)2828 }@@ -32,12 +32,12 @@ func (m *Model) openWidth() tea.Cmd {
3232 label: "Column width (1-240):",3333 indicator: "WIDTH",3434 fresh: true,35- resize: (*Model).setWidths,35+ resize: m.setWidths,3636 maxSize: 240,37- onText: func(m *Model, text string) tea.Cmd {37+ onText: func(text string) tea.Cmd {3838 w, err := strconv.Atoi(text)3939 if err != nil || w < 1 || w > 240 {40- restore(m)40+ restore()4141 m.fail("Column width must be between 1 and 240")4242 return nil4343 }@@ -47,6 +47,7 @@ var (
4747 _ pivotHost = (*Model)(nil)4848 _ macrosHost = (*Model)(nil)4949 _ namesHost = (*Model)(nil)50+ _ promptHost = (*Model)(nil)5051 )5152 5253 // Components that stay in package ui are handed interfaces of their own@@ -76,7 +77,7 @@ func (m *Model) pointRange(label string, done func(sheet.Rect), cancel func()) {
7677 done(r)7778 return nil7879 })79- m.prompt.onCancel = func(*Model) { cancel() }80+ m.prompt.onCancel = cancel8081 }8182 8283 // askText asks for text on the context line, starting from initial; done@@ -86,7 +87,7 @@ func (m *Model) askText(label, initial string, done func(string), cancel func())
8687 done(text)8788 return nil8889 })89- m.prompt.onCancel = func(*Model) { cancel() }90+ m.prompt.onCancel = cancel9091 }9192 9293 // What every component with a text field needs.@@ -200,11 +200,11 @@ func (m *Model) stopRecording() tea.Cmd {
200200 return nil201201 })202202 m.prompt.indicator = "NAME"203- m.prompt.onCancel = stillRecording203+ m.prompt.onCancel = m.stillRecording204204 return nil205205 }206206 207-func stillRecording(m *Model) { m.note = "Still recording" }207+func (m *Model) stillRecording() { m.note = "Still recording" }208208 209209 // checkNewMacroName reports why name can't name a new macro.210210 func checkNewMacroName(w *sheet.Workbook, name string) error {@@ -229,7 +229,7 @@ func (m *Model) askShortcut(name string) {
229229 return nil230230 })231231 m.prompt.indicator = "KEY"232- m.prompt.onCancel = stillRecording232+ m.prompt.onCancel = m.stillRecording233233 }234234 235235 func isShortcutDigit(s string) bool { return len(s) == 1 && s[0] >= '0' && s[0] <= '9' }@@ -155,7 +155,7 @@ const keyCheckTimeout = 15 * time.Second
155155 // and checks it with one small test call (jev.Check).156156 func (m *Model) askKey() tea.Cmd {157157 m.openPrompt(&prompt{kind: promptText, label: "TypeSafe API key:", indicator: "KEY", secret: true,158- onText: func(m *Model, key string) tea.Cmd {158+ onText: func(key string) tea.Cmd {159159 if key == "" {160160 m.note = "No key entered; nothing changed"161161 return nil@@ -8,6 +8,8 @@ import (
88 tea "charm.land/bubbletea/v2"99 1010 "github.com/FelineStateMachine/012/internal/sheet"11+ "github.com/FelineStateMachine/012/internal/ui/lineedit"12+ "github.com/FelineStateMachine/012/internal/ui/theme"1113 )1214 1315 // Prompts are questions on the context line, answered by typing (a file@@ -34,18 +36,49 @@ type prompt struct {
3436 secret bool // the answer is shown masked, e.g. an API key3537 breaks bool // Shift+Enter and Alt+Enter type a line break, shown as noteBreak3638 typing bool // range prompts: the user is typing instead of pointing37- onText func(m *Model, text string) tea.Cmd38- onRange func(m *Model, r sheet.Rect) tea.Cmd39- onCancel func(m *Model)39+ onText func(text string) tea.Cmd40+ onRange func(r sheet.Rect) tea.Cmd41+ onCancel func()4042 // resize previews a size prompt's answer, from 1 to maxSize.41- resize func(m *Model, n int)43+ resize func(n int)4244 maxSize int4345 files []string // the file list shown by File Open4446 }4547 48+// promptHost is what a prompt acts on. The model implements it. Prompts49+// stay in package ui because a prompt is one of the model's modes rather50+// than an overlay: a range is pointed at with the grid's own movement51+// and drawn by the grid, and the answer goes to a callback over the52+// model.53+type promptHost interface {54+ styles() *theme.Theme55+ // editLine is the shared edit line the answer is typed in.56+ editLine() *lineedit.Line57+ // pointer is the range being pointed at, which the grid draws.58+ pointer() *pointer59+ // pointMoveKey moves or stretches the pointer as the grid's keys60+ // do, and reports whether key was one of them.61+ pointMoveKey(key string) bool62+ closePrompt()63+ fail(msg string)64+ // recordAnswer tells a macro recording the answer to the question65+ // of the command that asked.66+ recordAnswer(answer string, cancelled bool)67+ // cancelQuit stops Quit waiting on the save a cancelled prompt was68+ // part of: cancelling Save as cancels Save and quit.69+ cancelQuit()70+}71+72+func (m *Model) editLine() *lineedit.Line { return &m.line }73+func (m *Model) pointer() *pointer { return &m.point }74+func (m *Model) cancelQuit() { m.quitAfterSave = false }75+76+// pointing reports whether a range prompt is taking the arrows.77+func (p *prompt) pointing() bool { return p.kind == promptRange && !p.typing }78+4679 // pointing reports whether a range prompt is taking the arrows.4780 func (m *Model) pointing() bool {48- return m.mode == modePrompt && m.prompt.kind == promptRange && !m.prompt.typing81+ return m.mode == modePrompt && m.prompt.pointing()4982 }5083 5184 func (m *Model) openPrompt(p *prompt, initial string) {@@ -58,13 +91,15 @@ func (m *Model) openPrompt(p *prompt, initial string) {
5891 5992 // openText asks for text, starting from initial, which typing replaces.6093 func (m *Model) openText(label, initial string, onText func(*Model, string) tea.Cmd) {61- m.openPrompt(&prompt{kind: promptText, label: label, indicator: "EDIT", fresh: true, onText: onText}, initial)94+ done := func(text string) tea.Cmd { return onText(m, text) }95+ m.openPrompt(&prompt{kind: promptText, label: label, indicator: "EDIT", fresh: true, onText: done}, initial)6296 }6397 6498 // openRange asks for a range, starting from the current selection.6599 func (m *Model) openRange(label string, onRange func(*Model, sheet.Rect) tea.Cmd) {66100 r := m.selection()67- m.openPrompt(&prompt{kind: promptRange, label: label, indicator: "POINT", onRange: onRange}, "")101+ done := func(r sheet.Rect) tea.Cmd { return onRange(m, r) }102+ m.openPrompt(&prompt{kind: promptRange, label: label, indicator: "POINT", onRange: done}, "")68103 m.point = pointer{anchor: r.From, at: r.To, anchored: r.From != r.To}69104 }70105 @@ -74,7 +109,7 @@ func (m *Model) closePrompt() {
74109 }75110 76111 // key handles a key while the prompt is open.77-func (p *prompt) key(m *Model, k tea.KeyPressMsg) tea.Cmd {112+func (p *prompt) key(m promptHost, k tea.KeyPressMsg) tea.Cmd {78113 switch key := k.String(); {79114 case key == "esc":80115 p.cancel(m)@@ -82,8 +117,8 @@ func (p *prompt) key(m *Model, k tea.KeyPressMsg) tea.Cmd {
82117 return p.accept(m)83118 case p.breaks && (key == "shift+enter" || key == "alt+enter"):84119 p.fresh = false85- m.line.Insert(noteBreak)86- case m.pointing():120+ m.editLine().Insert(noteBreak)121+ case p.pointing():87122 p.pointKey(m, k)88123 case p.kind == promptWidth && (key == "left" || key == "right"):89124 p.stepWidth(m, key)@@ -94,31 +129,31 @@ func (p *prompt) key(m *Model, k tea.KeyPressMsg) tea.Cmd {
94129 }95130 96131 // cancel backs out: of a range being stretched, or of the prompt.97-func (p *prompt) cancel(m *Model) {98- if m.pointing() && m.point.anchored {99- m.point.anchored = false132+func (p *prompt) cancel(m promptHost) {133+ if pt := m.pointer(); p.pointing() && pt.anchored {134+ pt.anchored = false100135 return101136 }102- m.quitAfterSave = false // cancelling Save as cancels Save and quit137+ m.cancelQuit()103138 // Closed first, so onCancel can return to what opened the prompt, as104139 // the chart editor does.105140 m.closePrompt()106141 if p.onCancel != nil {107- p.onCancel(m)142+ p.onCancel()108143 }109144 m.recordAnswer("", true)110145 }111146 112147 // accept closes the prompt and acts on the answer.113-func (p *prompt) accept(m *Model) tea.Cmd {114- text := strings.TrimSpace(m.line.Text())148+func (p *prompt) accept(m promptHost) tea.Cmd {149+ text := strings.TrimSpace(m.editLine().Text())115150 m.closePrompt()116151 if p.kind != promptRange {117- cmd := p.onText(m, text)152+ cmd := p.onText(text)118153 m.recordAnswer(text, false)119154 return cmd120155 }121- r := m.point.rect()156+ r := m.pointer().rect()122157 if p.typing {123158 var ok bool124159 if r, ok = sheet.ParseRange(text); !ok {@@ -126,78 +161,82 @@ func (p *prompt) accept(m *Model) tea.Cmd {
126161 return nil127162 }128163 }129- cmd := p.onRange(m, r)164+ cmd := p.onRange(r)130165 m.recordAnswer(r.String(), false)131166 return cmd132167 }133168 134169 // pointKey moves or stretches the range being pointed at; typing switches135170 // to typing the range instead.136-func (p *prompt) pointKey(m *Model, k tea.KeyPressMsg) {171+func (p *prompt) pointKey(m promptHost, k tea.KeyPressMsg) {137172 key := k.String()138- switch {173+ switch pt := m.pointer(); {139174 case m.pointMoveKey(key):140175 case key == ":":141- m.point.anchor, m.point.anchored = m.point.at, true176+ pt.anchor, pt.anchored = pt.at, true142177 case typed(k) != "":143178 p.typing = true144- m.line.Insert(typed(k))179+ m.editLine().Insert(typed(k))145180 }146181 }147182 148183 // stepWidth changes a width by one, previewing it live.149-func (p *prompt) stepWidth(m *Model, key string) {150- w, _ := strconv.Atoi(m.line.Text())184+func (p *prompt) stepWidth(m promptHost, key string) {185+ line := m.editLine()186+ w, _ := strconv.Atoi(line.Text())151187 if key == "left" {152188 w--153189 } else {154190 w++155191 }156192 w = clamp(w, 1, p.maxSize)157- m.line.Clear()193+ line.Clear()158194 p.fresh = false159- m.line.Insert(strconv.Itoa(w))160- p.resize(m, w)195+ line.Insert(strconv.Itoa(w))196+ p.resize(w)161197 }162198 163199 // typeKey edits the answer; the first key typed replaces the default.164-func (p *prompt) typeKey(m *Model, k tea.KeyPressMsg) {200+func (p *prompt) typeKey(m promptHost, k tea.KeyPressMsg) {201+ line := m.editLine()165202 if p.fresh && typed(k) != "" {166- m.line.Clear()203+ line.Clear()167204 }168205 p.fresh = false169206 if p.kind == promptWidth && !isDigits(typed(k)) {170207 return171208 }172- m.line.Key(k)209+ line.Key(k)173210 }174211 175212 // paste types pasted text into the answer.176-func (p *prompt) paste(m *Model, text string) {213+func (p *prompt) paste(m promptHost, text string) {214+ line := m.editLine()177215 if p.fresh {178- m.line.Clear()216+ line.Clear()179217 p.fresh = false180218 }181- m.line.Insert(text)219+ line.Insert(text)182220 }183221 184222 // line is the prompt as the context line shows it, e.g. "Save as:185223 // budget.012", and the keys or choices that go with it.186-func (p *prompt) line(m *Model) (left, right string) {224+func (p *prompt) line(m promptHost) (left, right string) {225+ th, text := m.styles(), m.editLine().Text()187226 switch {188- case m.pointing():189- return p.prefix() + m.th.Selection.Render(m.point.text()),190- m.th.KeyHints("Arrows", "move", "Shift+arrows", "extend", "Enter", "apply", "Esc", "cancel")227+ case p.pointing():228+ return p.prefix() + th.Selection.Render(m.pointer().text()),229+ th.KeyHints("Arrows", "move", "Shift+arrows", "extend", "Enter", "apply", "Esc", "cancel")191230 case len(p.files) > 0:192- return p.prefix() + m.line.Text(), m.th.Muted.Render(strings.Join(p.files, " "))231+ return p.prefix() + text, th.Muted.Render(strings.Join(p.files, " "))193232 case p.kind == promptWidth:194- return p.prefix() + m.line.Text(), m.th.KeyHints("Left/Right", "adjust", "Enter", "apply", "Esc", "cancel")233+ return p.prefix() + text, th.KeyHints("Left/Right", "adjust", "Enter", "apply", "Esc", "cancel")195234 case p.secret:196- return p.prefix() + mask(len(m.line.Buf)), m.th.KeyHints("Enter", "save", "Esc", "cancel")235+ return p.prefix() + mask(len(m.editLine().Buf)), th.KeyHints("Enter", "save", "Esc", "cancel")197236 case p.breaks:198- return p.prefix() + m.line.Text(), m.th.KeyHints("Enter", "save", "Shift+Enter", "new line", "Esc", "cancel")237+ return p.prefix() + text, th.KeyHints("Enter", "save", "Shift+Enter", "new line", "Esc", "cancel")199238 }200- return p.prefix() + m.line.Text(), m.th.KeyHints("Enter", "apply", "Esc", "cancel")239+ return p.prefix() + text, th.KeyHints("Enter", "apply", "Esc", "cancel")201240 }202241 203242 func (p *prompt) prefix() string {@@ -208,11 +247,11 @@ func (p *prompt) prefix() string {
208247 func mask(n int) string { return strings.Repeat("•", n) }209248 210249 // head is the answer before the caret, as shown.211-func (p *prompt) head(m *Model) string {250+func (p *prompt) head(m promptHost) string {212251 if p.secret {213- return mask(m.line.Pos)252+ return mask(m.editLine().Pos)214253 }215- return m.line.Head()254+ return m.editLine().Head()216255 }217256 218257 func isDigits(s string) bool {@@ -37,7 +37,7 @@ func (m *Model) openHeight() tea.Cmd {
3737 orig[row] = h3838 }3939 }40- restore := func(m *Model) {40+ restore := func() {4141 m.sheet.Batch(sheet.Change{Label: "row height", Focus: r}, func() error {4242 m.sheet.SetRowHeight(r.From.Row, r.To.Row, 0)4343 for row, h := range orig {@@ -52,12 +52,12 @@ func (m *Model) openHeight() tea.Cmd {
5252 label: "Row height (1-" + strconv.Itoa(sheet.MaxRowHeight) + " lines):",5353 indicator: "HEIGHT",5454 fresh: true,55- resize: func(m *Model, h int) { m.sheet.SetRowHeight(r.From.Row, r.To.Row, h) },55+ resize: func(h int) { m.sheet.SetRowHeight(r.From.Row, r.To.Row, h) },5656 maxSize: sheet.MaxRowHeight,57- onText: func(m *Model, text string) tea.Cmd {57+ onText: func(text string) tea.Cmd {5858 h, err := strconv.Atoi(text)5959 if err != nil || h < 1 || h > sheet.MaxRowHeight {60- restore(m)60+ restore()6161 m.fail("Row height must be between 1 and " + strconv.Itoa(sheet.MaxRowHeight) + " lines")6262 return nil6363 }