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/tui/layout"
11 "github.com/kujtimiihoxha/termai/internal/tui/styles"
12 "github.com/kujtimiihoxha/termai/internal/tui/util"
13 "github.com/kujtimiihoxha/vimtea"
14 "golang.org/x/net/context"
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 cancelMessage context.CancelFunc
34}
35
36type editorKeyMap struct {
37 SendMessage key.Binding
38 SendMessageI key.Binding
39 CancelMessage key.Binding
40 InsertMode key.Binding
41 NormaMode key.Binding
42 VisualMode key.Binding
43 VisualLineMode key.Binding
44}
45
46var editorKeyMapValue = editorKeyMap{
47 SendMessage: key.NewBinding(
48 key.WithKeys("enter"),
49 key.WithHelp("enter", "send message normal mode"),
50 ),
51 SendMessageI: key.NewBinding(
52 key.WithKeys("ctrl+s"),
53 key.WithHelp("ctrl+s", "send message insert mode"),
54 ),
55 CancelMessage: key.NewBinding(
56 key.WithKeys("ctrl+x"),
57 key.WithHelp("ctrl+x", "cancel current message"),
58 ),
59 InsertMode: key.NewBinding(
60 key.WithKeys("i"),
61 key.WithHelp("i", "insert mode"),
62 ),
63 NormaMode: key.NewBinding(
64 key.WithKeys("esc"),
65 key.WithHelp("esc", "normal mode"),
66 ),
67 VisualMode: key.NewBinding(
68 key.WithKeys("v"),
69 key.WithHelp("v", "visual mode"),
70 ),
71 VisualLineMode: key.NewBinding(
72 key.WithKeys("V"),
73 key.WithHelp("V", "visual line mode"),
74 ),
75}
76
77func (m *editorCmp) Init() tea.Cmd {
78 return m.editor.Init()
79}
80
81func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
82 switch msg := msg.(type) {
83 case vimtea.EditorModeMsg:
84 m.editorMode = msg.Mode
85 case SelectedSessionMsg:
86 if msg.SessionID != m.sessionID {
87 m.sessionID = msg.SessionID
88 }
89 }
90 if m.IsFocused() {
91 switch msg := msg.(type) {
92 case tea.KeyMsg:
93 switch {
94 case key.Matches(msg, editorKeyMapValue.SendMessage):
95 if m.editorMode == vimtea.ModeNormal {
96 return m, m.Send()
97 }
98 case key.Matches(msg, editorKeyMapValue.SendMessageI):
99 if m.editorMode == vimtea.ModeInsert {
100 return m, m.Send()
101 }
102 case key.Matches(msg, editorKeyMapValue.CancelMessage):
103 return m, m.Cancel()
104 }
105 }
106 u, cmd := m.editor.Update(msg)
107 m.editor = u.(vimtea.Editor)
108 return m, cmd
109 }
110 return m, nil
111}
112
113func (m *editorCmp) Blur() tea.Cmd {
114 m.focused = false
115 return nil
116}
117
118func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
119 title := "New Message"
120 if m.focused {
121 title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
122 }
123 return map[layout.BorderPosition]string{
124 layout.BottomLeftBorder: title,
125 }
126}
127
128func (m *editorCmp) Focus() tea.Cmd {
129 m.focused = true
130 return m.editor.Tick()
131}
132
133func (m *editorCmp) GetSize() (int, int) {
134 return m.width, m.height
135}
136
137func (m *editorCmp) IsFocused() bool {
138 return m.focused
139}
140
141func (m *editorCmp) SetSize(width int, height int) {
142 m.width = width
143 m.height = height
144 m.editor.SetSize(width, height)
145}
146
147func (m *editorCmp) Cancel() tea.Cmd {
148 if m.cancelMessage == nil {
149 return util.ReportWarn("No message to cancel")
150 }
151
152 m.cancelMessage()
153 m.cancelMessage = nil
154 return util.ReportWarn("Message cancelled")
155}
156
157func (m *editorCmp) Send() tea.Cmd {
158 if m.cancelMessage != nil {
159 return util.ReportWarn("Assistant is still working on the previous message")
160 }
161
162 messages, err := m.app.Messages.List(context.Background(), m.sessionID)
163 if err != nil {
164 return util.ReportError(err)
165 }
166 if hasUnfinishedMessages(messages) {
167 return util.ReportWarn("Assistant is still working on the previous message")
168 }
169
170 content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
171 if len(content) == 0 {
172 return util.ReportWarn("Message is empty")
173 }
174 ctx, cancel := context.WithCancel(context.Background())
175 m.cancelMessage = cancel
176 go func() {
177 defer cancel()
178 m.app.CoderAgent.Generate(ctx, m.sessionID, content)
179 m.cancelMessage = nil
180 }()
181
182 return m.editor.Reset()
183}
184
185func (m *editorCmp) View() string {
186 return m.editor.View()
187}
188
189func (m *editorCmp) BindingKeys() []key.Binding {
190 return layout.KeyMapToSlice(editorKeyMapValue)
191}
192
193func NewEditorCmp(app *app.App) EditorCmp {
194 editor := vimtea.NewEditor(
195 vimtea.WithFileName("message.md"),
196 )
197 return &editorCmp{
198 app: app,
199 editor: editor,
200 }
201}