cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Prompts: a narrow promptHost

Dami committed 21cc23c
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
8 files changed, +104 -64
Minternal/ui/chartaxes.go2+-
Minternal/ui/charteditor.go4+-
Minternal/ui/columns.go8+-
Minternal/ui/hosts.go5+-
Minternal/ui/macro.go6+-
Minternal/ui/prefs.go2+-
Minternal/ui/prompt.go133+++++++++++++++++++-----------
Minternal/ui/rowheights.go8+-
M internal/ui/chartaxes.go +1 -1
M internal/ui/charteditor.go +2 -2
M internal/ui/columns.go +4 -4
M internal/ui/hosts.go +3 -2
M internal/ui/macro.go +3 -3
M internal/ui/prefs.go +1 -1
M internal/ui/prompt.go +86 -47
@@ -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 {
M internal/ui/rowheights.go +4 -4