diff --git a/internal/client/ui/core/chat/chat.go b/internal/client/ui/core/chat/chat.go index 04eedb7..a93272f 100644 --- a/internal/client/ui/core/chat/chat.go +++ b/internal/client/ui/core/chat/chat.go @@ -23,7 +23,7 @@ func New() Model { return lineNumber + " ┃ " } - vi.SetLines(viminput.Line("test"), viminput.Line("best")) + vi.SetLines([]rune("test"), []rune("best")) // ta.Cursor.SetChar() // vi.CharLimit = 280 @@ -54,8 +54,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m *Model) Focus() { m.focus = true + m.vi.Focus() } func (m *Model) Blur() { m.focus = false + m.vi.Blur() } diff --git a/internal/client/ui/viminput/viminput.go b/internal/client/ui/viminput/viminput.go index 94c5413..1c3d8a9 100644 --- a/internal/client/ui/viminput/viminput.go +++ b/internal/client/ui/viminput/viminput.go @@ -1,31 +1,38 @@ package viminput import ( + "slices" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/kyren223/eko/internal/client/ui/colors" ) +var DefaultCursorStyle = lipgloss.NewStyle().Background(colors.White).Foreground(colors.Background) + type ( - Keymap struct{} LineDecoration = func(lnum int, m Model) string ) -type Line []byte +const ( + NormalMode = iota + InsertMode + VisualMode +) type Model struct { - keymap Keymap - PlaceholderStyle lipgloss.Style PromptStyle lipgloss.Style Placeholder string LineDecoration LineDecoration - lines []Line - CursorLine int - CursorColumn int + lines [][]rune + cursorLine int + cursorColumn int + goalColumn int + mode int width int height int @@ -34,11 +41,15 @@ type Model struct { func New(width, height int) Model { return Model{ - keymap: Keymap{}, PlaceholderStyle: lipgloss.NewStyle(), PromptStyle: lipgloss.NewStyle(), Placeholder: "", LineDecoration: EmptyLineDecoration, + lines: [][]rune{}, + cursorLine: 0, + cursorColumn: 0, + goalColumn: -1, + mode: NormalMode, width: width, height: height, focus: false, @@ -50,10 +61,10 @@ func (m Model) Init() tea.Cmd { } func (m Model) View() string { - var lines []Line + var lines [][]rune if len(m.lines) == 0 { placeholder := m.PlaceholderStyle.Render(m.Placeholder) - lines = append(lines, Line(placeholder)) + lines = append(lines, []rune(placeholder)) } else { lines = m.lines } @@ -62,7 +73,18 @@ func (m Model) View() string { for i, line := range lines { lineDecoration := m.LineDecoration(i, m) builder.WriteString(lineDecoration) - builder.Write(line) + + if m.CursorLine() != i { + builder.WriteString(string(line)) + } else if m.CursorColumn() == len(m.lines[m.CursorLine()]) { + builder.WriteString(string(line)) + builder.WriteString(DefaultCursorStyle.Render(" ")) + } else { + builder.WriteString(string(line[:m.CursorColumn()])) + builder.WriteString(DefaultCursorStyle.Render(string(line[m.CursorColumn()]))) + builder.WriteString(string(line[m.CursorColumn()+1:])) + } + builder.WriteByte('\n') } @@ -77,6 +99,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { return m, nil } + switch msg := msg.(type) { + case tea.KeyMsg: + cmd := m.handleKeys(msg) + return m, cmd + } + return m, nil } @@ -104,14 +132,90 @@ func (m *Model) Blur() { m.focus = false } -func (m *Model) SetLines(lines ...Line) { +func (m *Model) SetLines(lines ...[]rune) { m.lines = lines } -func (m *Model) SetLine(lnum int, line Line) { +func (m *Model) SetLine(lnum int, line []rune) { m.lines[lnum] = line } -func (m *Model) Line(lnum int) Line { +func (m *Model) Line(lnum int) []rune { return m.lines[lnum] } + +func (m *Model) SetCursorColumn(col int) { + m.cursorColumn = col + m.goalColumn = -1 +} + +func (m *Model) SetCursorLine(line int) { + fromLength := m.CursorColumn() + toLength := len(m.lines[line]) + if fromLength > toLength && m.goalColumn == -1 { + m.goalColumn = fromLength + } + if m.goalColumn != -1 { + m.cursorColumn = min(toLength-1, m.goalColumn) + } + m.cursorLine = line +} + +func (m *Model) CursorColumn() int { + return m.cursorColumn +} + +func (m *Model) CursorLine() int { + return m.cursorLine +} + +func (m *Model) handleKeys(key tea.KeyMsg) tea.Cmd { + switch m.mode { + case NormalMode: + return m.handleNormalModeKeys(key) + case InsertMode: + return m.handleInsertModeKeys(key) + } + + return nil +} + +func (m *Model) handleNormalModeKeys(key tea.KeyMsg) tea.Cmd { + switch key.String() { + case "h": + m.SetCursorColumn(max(m.CursorColumn()-1, 0)) + case "j": + m.SetCursorLine(min(m.CursorLine()+1, len(m.lines)-1)) + case "k": + m.SetCursorLine(max(m.CursorLine()-1, 0)) + case "l": + m.SetCursorColumn(min(m.CursorColumn()+1, len(m.lines[m.CursorLine()])-1)) + case "i": + m.mode = InsertMode + case "a": + m.mode = InsertMode + m.SetCursorColumn(m.CursorColumn() + 1) + case "0": + m.SetCursorColumn(0) + } + + return nil +} + +func (m *Model) handleInsertModeKeys(key tea.KeyMsg) tea.Cmd { + if key.Type == tea.KeyEscape { + m.SetCursorColumn(max(m.CursorColumn()-1, 0)) + m.mode = NormalMode + return nil + } + + keyStr := key.String() + length := len(keyStr) + if length == 1 && 32 <= keyStr[0] && keyStr[0] <= 126 { + line := m.lines[m.CursorLine()] + m.lines[m.CursorLine()] = slices.Insert(line, m.CursorColumn(), rune(keyStr[0])) + m.SetCursorColumn(m.CursorColumn() + 1) + } + + return nil +}