1package chat
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/textarea"
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/lipgloss"
8 "github.com/kujtimiihoxha/termai/internal/tui/layout"
9 "github.com/kujtimiihoxha/termai/internal/tui/styles"
10 "github.com/kujtimiihoxha/termai/internal/tui/util"
11)
12
13type editorCmp struct {
14 textarea textarea.Model
15 agentWorking bool
16}
17
18type focusedEditorKeyMaps struct {
19 Send key.Binding
20 Blur key.Binding
21}
22
23type bluredEditorKeyMaps struct {
24 Send key.Binding
25 Focus key.Binding
26}
27
28var focusedKeyMaps = focusedEditorKeyMaps{
29 Send: key.NewBinding(
30 key.WithKeys("ctrl+s"),
31 key.WithHelp("ctrl+s", "send message"),
32 ),
33 Blur: key.NewBinding(
34 key.WithKeys("esc"),
35 key.WithHelp("esc", "blur editor"),
36 ),
37}
38
39var bluredKeyMaps = bluredEditorKeyMaps{
40 Send: key.NewBinding(
41 key.WithKeys("ctrl+s", "enter"),
42 key.WithHelp("ctrl+s/enter", "send message"),
43 ),
44 Focus: key.NewBinding(
45 key.WithKeys("i"),
46 key.WithHelp("i", "focus editor"),
47 ),
48}
49
50func (m *editorCmp) Init() tea.Cmd {
51 return textarea.Blink
52}
53
54func (m *editorCmp) send() tea.Cmd {
55 if m.agentWorking {
56 return util.ReportWarn("Agent is working, please wait...")
57 }
58
59 value := m.textarea.Value()
60 m.textarea.Reset()
61 m.textarea.Blur()
62 if value == "" {
63 return nil
64 }
65 return tea.Batch(
66 util.CmdHandler(SendMsg{
67 Text: value,
68 }),
69 util.CmdHandler(AgentWorkingMsg(true)),
70 util.CmdHandler(EditorFocusMsg(false)),
71 )
72}
73
74func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
75 var cmd tea.Cmd
76 switch msg := msg.(type) {
77 case AgentWorkingMsg:
78 m.agentWorking = bool(msg)
79 case tea.KeyMsg:
80 // if the key does not match any binding, return
81 if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
82 return m, m.send()
83 }
84 if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
85 return m, m.send()
86 }
87 if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) {
88 m.textarea.Blur()
89 return m, util.CmdHandler(EditorFocusMsg(false))
90 }
91 if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
92 m.textarea.Focus()
93 return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
94 }
95 }
96 m.textarea, cmd = m.textarea.Update(msg)
97 return m, cmd
98}
99
100func (m *editorCmp) View() string {
101 style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
102
103 return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
104}
105
106func (m *editorCmp) SetSize(width, height int) {
107 m.textarea.SetWidth(width - 3) // account for the prompt and padding right
108 m.textarea.SetHeight(height)
109}
110
111func (m *editorCmp) GetSize() (int, int) {
112 return m.textarea.Width(), m.textarea.Height()
113}
114
115func (m *editorCmp) BindingKeys() []key.Binding {
116 bindings := layout.KeyMapToSlice(m.textarea.KeyMap)
117 if m.textarea.Focused() {
118 bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
119 } else {
120 bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
121 }
122 return bindings
123}
124
125func NewEditorCmp() tea.Model {
126 ti := textarea.New()
127 ti.Prompt = " "
128 ti.ShowLineNumbers = false
129 ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
130 ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
131 ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
132 ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
133
134 ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
135 ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
136 ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
137 ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
138 ti.CharLimit = -1
139 ti.Focus()
140 return &editorCmp{
141 textarea: ti,
142 }
143}