1package tui
2
3import (
4 "log"
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"
11 "github.com/kujtimiihoxha/termai/internal/pubsub"
12 "github.com/kujtimiihoxha/termai/internal/tui/components/core"
13 "github.com/kujtimiihoxha/termai/internal/tui/components/dialog"
14 "github.com/kujtimiihoxha/termai/internal/tui/layout"
15 "github.com/kujtimiihoxha/termai/internal/tui/page"
16 "github.com/kujtimiihoxha/termai/internal/tui/util"
17 "github.com/kujtimiihoxha/vimtea"
18)
19
20type keyMap struct {
21 Logs key.Binding
22 Return key.Binding
23 Back key.Binding
24 Quit key.Binding
25 Help key.Binding
26}
27
28var keys = keyMap{
29 Logs: key.NewBinding(
30 key.WithKeys("L"),
31 key.WithHelp("L", "logs"),
32 ),
33 Return: key.NewBinding(
34 key.WithKeys("esc"),
35 key.WithHelp("esc", "close"),
36 ),
37 Back: key.NewBinding(
38 key.WithKeys("backspace"),
39 key.WithHelp("backspace", "back"),
40 ),
41 Quit: key.NewBinding(
42 key.WithKeys("ctrl+c", "q"),
43 key.WithHelp("ctrl+c/q", "quit"),
44 ),
45 Help: key.NewBinding(
46 key.WithKeys("?"),
47 key.WithHelp("?", "toggle help"),
48 ),
49}
50
51type appModel struct {
52 width, height int
53 currentPage page.PageID
54 previousPage page.PageID
55 pages map[page.PageID]tea.Model
56 loadedPages map[page.PageID]bool
57 status tea.Model
58 help core.HelpCmp
59 dialog core.DialogCmp
60 dialogVisible bool
61 editorMode vimtea.EditorMode
62 showHelp bool
63}
64
65func (a appModel) Init() tea.Cmd {
66 cmd := a.pages[a.currentPage].Init()
67 a.loadedPages[a.currentPage] = true
68 return cmd
69}
70
71func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
72 switch msg := msg.(type) {
73 case pubsub.Event[llm.AgentEvent]:
74 log.Println("Event received")
75 log.Println(msg)
76 case vimtea.EditorModeMsg:
77 a.editorMode = msg.Mode
78 case tea.WindowSizeMsg:
79 msg.Height -= 1 // Make space for the status bar
80 a.width, a.height = msg.Width, msg.Height
81
82 a.status, _ = a.status.Update(msg)
83
84 uh, _ := a.help.Update(msg)
85 a.help = uh.(core.HelpCmp)
86
87 p, cmd := a.pages[a.currentPage].Update(msg)
88 a.pages[a.currentPage] = p
89 return a, cmd
90 case core.DialogMsg:
91 d, cmd := a.dialog.Update(msg)
92 a.dialog = d.(core.DialogCmp)
93 a.dialogVisible = true
94 return a, cmd
95 case core.DialogCloseMsg:
96 d, cmd := a.dialog.Update(msg)
97 a.dialog = d.(core.DialogCmp)
98 a.dialogVisible = false
99 return a, cmd
100 case util.InfoMsg:
101 a.status, _ = a.status.Update(msg)
102 case util.ErrorMsg:
103 a.status, _ = a.status.Update(msg)
104 case tea.KeyMsg:
105 if a.editorMode == vimtea.ModeNormal {
106 switch {
107 case key.Matches(msg, keys.Quit):
108 return a, dialog.NewQuitDialogCmd()
109 case key.Matches(msg, keys.Back):
110 if a.previousPage != "" {
111 return a, a.moveToPage(a.previousPage)
112 }
113 case key.Matches(msg, keys.Return):
114 if a.showHelp {
115 a.ToggleHelp()
116 return a, nil
117 }
118 case key.Matches(msg, keys.Logs):
119 return a, a.moveToPage(page.LogsPage)
120 case key.Matches(msg, keys.Help):
121 a.ToggleHelp()
122 return a, nil
123 }
124 }
125 }
126 if a.dialogVisible {
127 d, cmd := a.dialog.Update(msg)
128 a.dialog = d.(core.DialogCmp)
129 return a, cmd
130 }
131 p, cmd := a.pages[a.currentPage].Update(msg)
132 a.pages[a.currentPage] = p
133 return a, cmd
134}
135
136func (a *appModel) ToggleHelp() {
137 if a.showHelp {
138 a.showHelp = false
139 a.height += a.help.Height()
140 } else {
141 a.showHelp = true
142 a.height -= a.help.Height()
143 }
144
145 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
146 sizable.SetSize(a.width, a.height)
147 }
148}
149
150func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
151 var cmd tea.Cmd
152 if _, ok := a.loadedPages[pageID]; !ok {
153 cmd = a.pages[pageID].Init()
154 a.loadedPages[pageID] = true
155 }
156 a.previousPage = a.currentPage
157 a.currentPage = pageID
158 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
159 sizable.SetSize(a.width, a.height)
160 }
161
162 return cmd
163}
164
165func (a appModel) View() string {
166 components := []string{
167 a.pages[a.currentPage].View(),
168 }
169
170 if a.showHelp {
171 bindings := layout.KeyMapToSlice(keys)
172 if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
173 bindings = append(bindings, p.BindingKeys()...)
174 }
175 if a.dialogVisible {
176 bindings = append(bindings, a.dialog.BindingKeys()...)
177 }
178 a.help.SetBindings(bindings)
179 components = append(components, a.help.View())
180 }
181
182 components = append(components, a.status.View())
183
184 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
185
186 if a.dialogVisible {
187 overlay := a.dialog.View()
188 row := lipgloss.Height(appView) / 2
189 row -= lipgloss.Height(overlay) / 2
190 col := lipgloss.Width(appView) / 2
191 col -= lipgloss.Width(overlay) / 2
192 appView = layout.PlaceOverlay(
193 col,
194 row,
195 overlay,
196 appView,
197 true,
198 )
199 }
200 return appView
201}
202
203func New(app *app.App) tea.Model {
204 return &appModel{
205 currentPage: page.ReplPage,
206 loadedPages: make(map[page.PageID]bool),
207 status: core.NewStatusCmp(),
208 help: core.NewHelpCmp(),
209 dialog: core.NewDialogCmp(),
210 pages: map[page.PageID]tea.Model{
211 page.LogsPage: page.NewLogsPage(),
212 page.InitPage: page.NewInitPage(),
213 page.ReplPage: page.NewReplPage(app),
214 },
215 }
216}