1package tui
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
7 "github.com/kujtimiihoxha/termai/internal/app"
8 "github.com/kujtimiihoxha/termai/internal/logging"
9 "github.com/kujtimiihoxha/termai/internal/permission"
10 "github.com/kujtimiihoxha/termai/internal/pubsub"
11 "github.com/kujtimiihoxha/termai/internal/tui/components/core"
12 "github.com/kujtimiihoxha/termai/internal/tui/components/dialog"
13 "github.com/kujtimiihoxha/termai/internal/tui/layout"
14 "github.com/kujtimiihoxha/termai/internal/tui/page"
15 "github.com/kujtimiihoxha/termai/internal/tui/util"
16)
17
18type keyMap struct {
19 Logs key.Binding
20 Quit key.Binding
21 Help key.Binding
22}
23
24var keys = keyMap{
25 Logs: key.NewBinding(
26 key.WithKeys("ctrl+l"),
27 key.WithHelp("ctrl+L", "logs"),
28 ),
29
30 Quit: key.NewBinding(
31 key.WithKeys("ctrl+c"),
32 key.WithHelp("ctrl+c", "quit"),
33 ),
34 Help: key.NewBinding(
35 key.WithKeys("ctrl+_"),
36 key.WithHelp("ctrl+?", "toggle help"),
37 ),
38}
39
40var returnKey = key.NewBinding(
41 key.WithKeys("esc"),
42 key.WithHelp("esc", "close"),
43)
44
45var logsKeyReturnKey = key.NewBinding(
46 key.WithKeys("backspace"),
47 key.WithHelp("backspace", "go back"),
48)
49
50type appModel struct {
51 width, height int
52 currentPage page.PageID
53 previousPage page.PageID
54 pages map[page.PageID]tea.Model
55 loadedPages map[page.PageID]bool
56 status tea.Model
57 app *app.App
58
59 showPermissions bool
60 permissions dialog.PermissionDialogCmp
61
62 showHelp bool
63 help dialog.HelpCmp
64
65 showQuit bool
66 quit dialog.QuitDialog
67}
68
69func (a appModel) Init() tea.Cmd {
70 var cmds []tea.Cmd
71 cmd := a.pages[a.currentPage].Init()
72 a.loadedPages[a.currentPage] = true
73 cmds = append(cmds, cmd)
74 cmd = a.status.Init()
75 cmds = append(cmds, cmd)
76 cmd = a.quit.Init()
77 cmds = append(cmds, cmd)
78 cmd = a.help.Init()
79 cmds = append(cmds, cmd)
80 return tea.Batch(cmds...)
81}
82
83func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
84 var cmds []tea.Cmd
85 var cmd tea.Cmd
86 switch msg := msg.(type) {
87 case tea.WindowSizeMsg:
88 msg.Height -= 1 // Make space for the status bar
89 a.width, a.height = msg.Width, msg.Height
90
91 a.status, _ = a.status.Update(msg)
92 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
93 cmds = append(cmds, cmd)
94
95 prm, permCmd := a.permissions.Update(msg)
96 a.permissions = prm.(dialog.PermissionDialogCmp)
97 cmds = append(cmds, permCmd)
98
99 help, helpCmd := a.help.Update(msg)
100 a.help = help.(dialog.HelpCmp)
101 cmds = append(cmds, helpCmd)
102
103 return a, tea.Batch(cmds...)
104
105 // Status
106 case util.InfoMsg:
107 a.status, cmd = a.status.Update(msg)
108 cmds = append(cmds, cmd)
109 return a, tea.Batch(cmds...)
110 case pubsub.Event[logging.LogMessage]:
111 if msg.Payload.Persist {
112 switch msg.Payload.Level {
113 case "error":
114 a.status, cmd = a.status.Update(util.InfoMsg{
115 Type: util.InfoTypeError,
116 Msg: msg.Payload.Message,
117 TTL: msg.Payload.PersistTime,
118 })
119 case "info":
120 a.status, cmd = a.status.Update(util.InfoMsg{
121 Type: util.InfoTypeInfo,
122 Msg: msg.Payload.Message,
123 TTL: msg.Payload.PersistTime,
124 })
125 case "warn":
126 a.status, cmd = a.status.Update(util.InfoMsg{
127 Type: util.InfoTypeWarn,
128 Msg: msg.Payload.Message,
129 TTL: msg.Payload.PersistTime,
130 })
131
132 default:
133 a.status, cmd = a.status.Update(util.InfoMsg{
134 Type: util.InfoTypeInfo,
135 Msg: msg.Payload.Message,
136 TTL: msg.Payload.PersistTime,
137 })
138 }
139 cmds = append(cmds, cmd)
140 }
141 case util.ClearStatusMsg:
142 a.status, _ = a.status.Update(msg)
143
144 // Permission
145 case pubsub.Event[permission.PermissionRequest]:
146 a.showPermissions = true
147 a.permissions.SetPermissions(msg.Payload)
148 return a, nil
149 case dialog.PermissionResponseMsg:
150 switch msg.Action {
151 case dialog.PermissionAllow:
152 a.app.Permissions.Grant(msg.Permission)
153 case dialog.PermissionAllowForSession:
154 a.app.Permissions.GrantPersistant(msg.Permission)
155 case dialog.PermissionDeny:
156 a.app.Permissions.Deny(msg.Permission)
157 }
158 a.showPermissions = false
159 return a, nil
160
161 case page.PageChangeMsg:
162 return a, a.moveToPage(msg.ID)
163
164 case dialog.CloseQuitMsg:
165 a.showQuit = false
166 return a, nil
167
168 case tea.KeyMsg:
169 switch {
170 case key.Matches(msg, keys.Quit):
171 a.showQuit = !a.showQuit
172 if a.showHelp {
173 a.showHelp = false
174 }
175 return a, nil
176 case key.Matches(msg, logsKeyReturnKey):
177 if a.currentPage == page.LogsPage {
178 return a, a.moveToPage(page.ChatPage)
179 }
180 case key.Matches(msg, returnKey):
181 if a.showQuit {
182 a.showQuit = !a.showQuit
183 return a, nil
184 }
185 if a.showHelp {
186 a.showHelp = !a.showHelp
187 return a, nil
188 }
189 case key.Matches(msg, keys.Logs):
190 return a, a.moveToPage(page.LogsPage)
191 case key.Matches(msg, keys.Help):
192 if a.showQuit {
193 return a, nil
194 }
195 a.showHelp = !a.showHelp
196 return a, nil
197 }
198 }
199
200 if a.showQuit {
201 q, quitCmd := a.quit.Update(msg)
202 a.quit = q.(dialog.QuitDialog)
203 cmds = append(cmds, quitCmd)
204 // Only block key messages send all other messages down
205 if _, ok := msg.(tea.KeyMsg); ok {
206 return a, tea.Batch(cmds...)
207 }
208 }
209 if a.showPermissions {
210 d, permissionsCmd := a.permissions.Update(msg)
211 a.permissions = d.(dialog.PermissionDialogCmp)
212 cmds = append(cmds, permissionsCmd)
213 // Only block key messages send all other messages down
214 if _, ok := msg.(tea.KeyMsg); ok {
215 return a, tea.Batch(cmds...)
216 }
217 }
218 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
219 cmds = append(cmds, cmd)
220 return a, tea.Batch(cmds...)
221}
222
223func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
224 var cmd tea.Cmd
225 if _, ok := a.loadedPages[pageID]; !ok {
226 cmd = a.pages[pageID].Init()
227 a.loadedPages[pageID] = true
228 }
229 a.previousPage = a.currentPage
230 a.currentPage = pageID
231 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
232 sizable.SetSize(a.width, a.height)
233 }
234
235 return cmd
236}
237
238func (a appModel) View() string {
239 components := []string{
240 a.pages[a.currentPage].View(),
241 }
242
243 components = append(components, a.status.View())
244
245 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
246
247 if a.showPermissions {
248 overlay := a.permissions.View()
249 row := lipgloss.Height(appView) / 2
250 row -= lipgloss.Height(overlay) / 2
251 col := lipgloss.Width(appView) / 2
252 col -= lipgloss.Width(overlay) / 2
253 appView = layout.PlaceOverlay(
254 col,
255 row,
256 overlay,
257 appView,
258 true,
259 )
260 }
261
262 if a.showHelp {
263 bindings := layout.KeyMapToSlice(keys)
264 if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
265 bindings = append(bindings, p.BindingKeys()...)
266 }
267 if a.showPermissions {
268 bindings = append(bindings, a.permissions.BindingKeys()...)
269 }
270 if a.currentPage == page.LogsPage {
271 bindings = append(bindings, logsKeyReturnKey)
272 }
273
274 a.help.SetBindings(bindings)
275
276 overlay := a.help.View()
277 row := lipgloss.Height(appView) / 2
278 row -= lipgloss.Height(overlay) / 2
279 col := lipgloss.Width(appView) / 2
280 col -= lipgloss.Width(overlay) / 2
281 appView = layout.PlaceOverlay(
282 col,
283 row,
284 overlay,
285 appView,
286 true,
287 )
288 }
289
290 if a.showQuit {
291 overlay := a.quit.View()
292 row := lipgloss.Height(appView) / 2
293 row -= lipgloss.Height(overlay) / 2
294 col := lipgloss.Width(appView) / 2
295 col -= lipgloss.Width(overlay) / 2
296 appView = layout.PlaceOverlay(
297 col,
298 row,
299 overlay,
300 appView,
301 true,
302 )
303 }
304
305 return appView
306}
307
308func New(app *app.App) tea.Model {
309 startPage := page.ChatPage
310 return &appModel{
311 currentPage: startPage,
312 loadedPages: make(map[page.PageID]bool),
313 status: core.NewStatusCmp(app.LSPClients),
314 help: dialog.NewHelpCmp(),
315 quit: dialog.NewQuitCmp(),
316 permissions: dialog.NewPermissionDialogCmp(),
317 app: app,
318 pages: map[page.PageID]tea.Model{
319 page.ChatPage: page.NewChatPage(app),
320 page.LogsPage: page.NewLogsPage(),
321 },
322 }
323}