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 key.Matches(msg, focusedKeyMaps.Send) {
81 return m, m.send()
82 }
83 if key.Matches(msg, bluredKeyMaps.Send) {
84 return m, m.send()
85 }
86 if key.Matches(msg, focusedKeyMaps.Blur) {
87 m.textarea.Blur()
88 return m, util.CmdHandler(EditorFocusMsg(false))
89 }
90 if key.Matches(msg, bluredKeyMaps.Focus) {
91 if !m.textarea.Focused() {
92 m.textarea.Focus()
93 return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
94 }
95 }
96 }
97 m.textarea, cmd = m.textarea.Update(msg)
98 return m, cmd
99}
100
101func (m *editorCmp) View() string {
102 style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
103
104 return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
105}
106
107func (m *editorCmp) SetSize(width, height int) {
108 m.textarea.SetWidth(width - 3) // account for the prompt and padding right
109 m.textarea.SetHeight(height)
110}
111
112func (m *editorCmp) GetSize() (int, int) {
113 return m.textarea.Width(), m.textarea.Height()
114}
115
116func (m *editorCmp) BindingKeys() []key.Binding {
117 bindings := layout.KeyMapToSlice(m.textarea.KeyMap)
118 if m.textarea.Focused() {
119 bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
120 } else {
121 bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
122 }
123 return bindings
124}
125
126func NewEditorCmp() tea.Model {
127 ti := textarea.New()
128 ti.Prompt = " "
129 ti.ShowLineNumbers = false
130 ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
131 ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
132 ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
133 ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
134
135 ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
136 ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
137 ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
138 ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
139 ti.CharLimit = -1
140 ti.Focus()
141 return &editorCmp{
142 textarea: ti,
143 }
144}