1package repl
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/bubbles/key"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/kujtimiihoxha/termai/internal/app"
10 "github.com/kujtimiihoxha/termai/internal/llm/agent"
11 "github.com/kujtimiihoxha/termai/internal/tui/layout"
12 "github.com/kujtimiihoxha/termai/internal/tui/styles"
13 "github.com/kujtimiihoxha/termai/internal/tui/util"
14 "github.com/kujtimiihoxha/vimtea"
15)
16
17type EditorCmp interface {
18 tea.Model
19 layout.Focusable
20 layout.Sizeable
21 layout.Bordered
22 layout.Bindings
23}
24
25type editorCmp struct {
26 app *app.App
27 editor vimtea.Editor
28 editorMode vimtea.EditorMode
29 sessionID string
30 focused bool
31 width int
32 height int
33}
34
35type editorKeyMap struct {
36 SendMessage key.Binding
37 SendMessageI key.Binding
38 InsertMode key.Binding
39 NormaMode key.Binding
40 VisualMode key.Binding
41 VisualLineMode key.Binding
42}
43
44var editorKeyMapValue = editorKeyMap{
45 SendMessage: key.NewBinding(
46 key.WithKeys("enter"),
47 key.WithHelp("enter", "send message normal mode"),
48 ),
49 SendMessageI: key.NewBinding(
50 key.WithKeys("ctrl+s"),
51 key.WithHelp("ctrl+s", "send message insert mode"),
52 ),
53 InsertMode: key.NewBinding(
54 key.WithKeys("i"),
55 key.WithHelp("i", "insert mode"),
56 ),
57 NormaMode: key.NewBinding(
58 key.WithKeys("esc"),
59 key.WithHelp("esc", "normal mode"),
60 ),
61 VisualMode: key.NewBinding(
62 key.WithKeys("v"),
63 key.WithHelp("v", "visual mode"),
64 ),
65 VisualLineMode: key.NewBinding(
66 key.WithKeys("V"),
67 key.WithHelp("V", "visual line mode"),
68 ),
69}
70
71func (m *editorCmp) Init() tea.Cmd {
72 return m.editor.Init()
73}
74
75func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
76 switch msg := msg.(type) {
77 case vimtea.EditorModeMsg:
78 m.editorMode = msg.Mode
79 case SelectedSessionMsg:
80 if msg.SessionID != m.sessionID {
81 m.sessionID = msg.SessionID
82 }
83 }
84 if m.IsFocused() {
85 switch msg := msg.(type) {
86 case tea.KeyMsg:
87 switch {
88 case key.Matches(msg, editorKeyMapValue.SendMessage):
89 if m.editorMode == vimtea.ModeNormal {
90 return m, m.Send()
91 }
92 case key.Matches(msg, editorKeyMapValue.SendMessageI):
93 if m.editorMode == vimtea.ModeInsert {
94 return m, m.Send()
95 }
96 }
97 }
98 u, cmd := m.editor.Update(msg)
99 m.editor = u.(vimtea.Editor)
100 return m, cmd
101 }
102 return m, nil
103}
104
105func (m *editorCmp) Blur() tea.Cmd {
106 m.focused = false
107 return nil
108}
109
110func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
111 title := "New Message"
112 if m.focused {
113 title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
114 }
115 return map[layout.BorderPosition]string{
116 layout.BottomLeftBorder: title,
117 }
118}
119
120func (m *editorCmp) Focus() tea.Cmd {
121 m.focused = true
122 return m.editor.Tick()
123}
124
125func (m *editorCmp) GetSize() (int, int) {
126 return m.width, m.height
127}
128
129func (m *editorCmp) IsFocused() bool {
130 return m.focused
131}
132
133func (m *editorCmp) SetSize(width int, height int) {
134 m.width = width
135 m.height = height
136 m.editor.SetSize(width, height)
137}
138
139func (m *editorCmp) Send() tea.Cmd {
140 return func() tea.Msg {
141 messages, _ := m.app.Messages.List(m.sessionID)
142 if hasUnfinishedMessages(messages) {
143 return util.ReportWarn("Assistant is still working on the previous message")
144 }
145 a, _ := agent.NewCoderAgent(m.app)
146
147 content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
148 go a.Generate(m.sessionID, content)
149
150 return m.editor.Reset()
151 }
152}
153
154func (m *editorCmp) View() string {
155 return m.editor.View()
156}
157
158func (m *editorCmp) BindingKeys() []key.Binding {
159 return layout.KeyMapToSlice(editorKeyMapValue)
160}
161
162func NewEditorCmp(app *app.App) EditorCmp {
163 editor := vimtea.NewEditor(
164 vimtea.WithFileName("message.md"),
165 )
166 return &editorCmp{
167 app: app,
168 editor: editor,
169 }
170}