diff options
| -rw-r--r-- | internal/viinput/model.go | 75 | ||||
| -rw-r--r-- | internal/viinput/model_test.go | 32 |
2 files changed, 89 insertions, 18 deletions
diff --git a/internal/viinput/model.go b/internal/viinput/model.go index c0c80ac..b84158b 100644 --- a/internal/viinput/model.go +++ b/internal/viinput/model.go @@ -4,6 +4,7 @@ import ( "strings" tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) // Mode represents the current vi-style input state. @@ -38,6 +39,7 @@ func (m *Model) Focus() tea.Cmd { m.focused = true m.mode = ModeInsert m.pending = 0 + m.history = nil m.wantsExit = false return nil } @@ -46,6 +48,7 @@ func (m *Model) Focus() tea.Cmd { func (m *Model) Blur() { m.focused = false m.pending = 0 + m.history = nil m.wantsExit = false } @@ -54,35 +57,52 @@ func (m *Model) SetValue(value string) { m.runes = []rune(value) m.cursor = len(m.runes) m.pending = 0 + m.history = nil m.wantsExit = false } // Value returns the current buffer contents. -func (m Model) Value() string { +func (m *Model) Value() string { + if m == nil { + return "" + } + return string(m.runes) } // Mode returns the current editing mode. -func (m Model) Mode() Mode { +func (m *Model) Mode() Mode { + if m == nil { + return ModeInsert + } + return m.mode } // WantsExit reports whether normal mode requested that editing end. -func (m Model) WantsExit() bool { +func (m *Model) WantsExit() bool { + if m == nil { + return false + } + return m.wantsExit } // Update applies a key event to the model. -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if m == nil { + return Model{}, nil + } + keyMsg, ok := msg.(tea.KeyPressMsg) if !ok || !m.focused { - return m, nil + return *m, nil } if m.mode == ModeNormal && m.pending != 0 { m.handlePendingNormal(keyMsg) m.pending = 0 - return m, nil + return *m, nil } switch m.mode { @@ -91,28 +111,30 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case ModeNormal: return m.updateNormalMode(keyMsg) default: - return m, nil + return *m, nil } } // View renders the prompt, value and cursor. -func (m Model) View() string { +func (m *Model) View() string { + if m == nil { + return "" + } + var builder strings.Builder builder.WriteString(m.Prompt) - cursorRune := "█" - if m.mode == ModeInsert { - cursorRune = "▏" - } + cursorRune := cursorGlyph(m.mode) + cursorStyle := cursorRenderStyle(m.mode) cursor := clampInt(m.cursor, 0, len(m.runes)) builder.WriteString(string(m.runes[:cursor])) - builder.WriteString(cursorRune) + builder.WriteString(cursorStyle.Render(cursorRune)) builder.WriteString(string(m.runes[cursor:])) return builder.String() } -func (m Model) updateInsertMode(keyMsg tea.KeyPressMsg) (Model, tea.Cmd) { +func (m *Model) updateInsertMode(keyMsg tea.KeyPressMsg) (Model, tea.Cmd) { switch keyMsg.String() { case "esc": m.mode = ModeNormal @@ -135,14 +157,14 @@ func (m Model) updateInsertMode(keyMsg tea.KeyPressMsg) (Model, tea.Cmd) { } } - return m, nil + return *m, nil } -func (m Model) updateNormalMode(keyMsg tea.KeyPressMsg) (Model, tea.Cmd) { +func (m *Model) updateNormalMode(keyMsg tea.KeyPressMsg) (Model, tea.Cmd) { switch keyMsg.String() { case "esc": m.wantsExit = true - return m, nil + return *m, nil case "h", "left": m.cursor = clampInt(m.cursor-1, 0, len(m.runes)) case "l", "right": @@ -186,7 +208,7 @@ func (m Model) updateNormalMode(keyMsg tea.KeyPressMsg) (Model, tea.Cmd) { m.pending = 0 } - return m, nil + return *m, nil } func (m *Model) handlePendingNormal(keyMsg tea.KeyPressMsg) { @@ -216,6 +238,23 @@ func (m *Model) handlePendingNormal(keyMsg tea.KeyPressMsg) { } } +func cursorGlyph(mode Mode) string { + if mode == ModeInsert { + return "▏" + } + + return "█" +} + +func cursorRenderStyle(mode Mode) lipgloss.Style { + style := lipgloss.NewStyle() + if mode == ModeInsert { + return style.Underline(true) + } + + return style.Reverse(true) +} + func insertedText(keyMsg tea.KeyPressMsg) (string, bool) { text := keyMsg.Text if text != "" { diff --git a/internal/viinput/model_test.go b/internal/viinput/model_test.go index dbbfe4b..1035038 100644 --- a/internal/viinput/model_test.go +++ b/internal/viinput/model_test.go @@ -1,6 +1,7 @@ package viinput import ( + "strings" "testing" tea "charm.land/bubbletea/v2" @@ -288,6 +289,37 @@ func TestModelUndoRestoresPriorState(t *testing.T) { } } +func TestModelViewUsesModeSpecificCursorStyles(t *testing.T) { + t.Parallel() + + model := New() + model.Focus() + model.Prompt = "edit> " + model.SetValue("abc") + model.cursor = 1 + model.mode = ModeNormal + + got := model.View() + if !strings.HasPrefix(got, model.Prompt) { + t.Fatalf("view = %q, want prefix %q", got, model.Prompt) + } + if !strings.Contains(got, "a") || !strings.Contains(got, "bc") { + t.Fatalf("view = %q, want rendered contents", got) + } + if !strings.Contains(got, cursorGlyph(ModeNormal)) { + t.Fatalf("normal view = %q, want block cursor", got) + } + if !strings.Contains(got, "\x1b[") { + t.Fatalf("normal view = %q, want lipgloss styling", got) + } + + model.mode = ModeInsert + got = model.View() + if !strings.Contains(got, cursorGlyph(ModeInsert)) { + t.Fatalf("insert view = %q, want bar cursor", got) + } +} + func key(value string) tea.KeyPressMsg { return tea.KeyPressMsg{Code: 0, Text: value} } |
