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/permission"
9 "github.com/kujtimiihoxha/termai/internal/pubsub"
10 "github.com/kujtimiihoxha/termai/internal/tui/components/core"
11 "github.com/kujtimiihoxha/termai/internal/tui/components/dialog"
12 "github.com/kujtimiihoxha/termai/internal/tui/components/repl"
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 "github.com/kujtimiihoxha/vimtea"
17)
18
19type keyMap struct {
20 Logs key.Binding
21 Return key.Binding
22 Back key.Binding
23 Quit key.Binding
24 Help key.Binding
25}
26
27var keys = keyMap{
28 Logs: key.NewBinding(
29 key.WithKeys("L"),
30 key.WithHelp("L", "logs"),
31 ),
32 Return: key.NewBinding(
33 key.WithKeys("esc"),
34 key.WithHelp("esc", "close"),
35 ),
36 Back: key.NewBinding(
37 key.WithKeys("backspace"),
38 key.WithHelp("backspace", "back"),
39 ),
40 Quit: key.NewBinding(
41 key.WithKeys("ctrl+c", "q"),
42 key.WithHelp("ctrl+c/q", "quit"),
43 ),
44 Help: key.NewBinding(
45 key.WithKeys("?"),
46 key.WithHelp("?", "toggle help"),
47 ),
48}
49
50var replKeyMap = key.NewBinding(
51 key.WithKeys("N"),
52 key.WithHelp("N", "new session"),
53)
54
55type appModel struct {
56 width, height int
57 currentPage page.PageID
58 previousPage page.PageID
59 pages map[page.PageID]tea.Model
60 loadedPages map[page.PageID]bool
61 status tea.Model
62 help core.HelpCmp
63 dialog core.DialogCmp
64 app *app.App
65 dialogVisible bool
66 editorMode vimtea.EditorMode
67 showHelp bool
68}
69
70func (a appModel) Init() tea.Cmd {
71 cmd := a.pages[a.currentPage].Init()
72 a.loadedPages[a.currentPage] = true
73 return cmd
74}
75
76func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77 switch msg := msg.(type) {
78 case pubsub.Event[permission.PermissionRequest]:
79 return a, dialog.NewPermissionDialogCmd(msg.Payload)
80 case pubsub.Event[util.InfoMsg]:
81 a.status, _ = a.status.Update(msg)
82 case dialog.PermissionResponseMsg:
83 switch msg.Action {
84 case dialog.PermissionAllow:
85 a.app.Permissions.Grant(msg.Permission)
86 case dialog.PermissionAllowForSession:
87 a.app.Permissions.GrantPersistant(msg.Permission)
88 case dialog.PermissionDeny:
89 a.app.Permissions.Deny(msg.Permission)
90 }
91 case vimtea.EditorModeMsg:
92 a.editorMode = msg.Mode
93 case tea.WindowSizeMsg:
94 var cmds []tea.Cmd
95 msg.Height -= 1 // Make space for the status bar
96 a.width, a.height = msg.Width, msg.Height
97
98 a.status, _ = a.status.Update(msg)
99
100 uh, _ := a.help.Update(msg)
101 a.help = uh.(core.HelpCmp)
102
103 p, cmd := a.pages[a.currentPage].Update(msg)
104 cmds = append(cmds, cmd)
105 a.pages[a.currentPage] = p
106
107 d, cmd := a.dialog.Update(msg)
108 cmds = append(cmds, cmd)
109 a.dialog = d.(core.DialogCmp)
110
111 return a, tea.Batch(cmds...)
112 case core.DialogMsg:
113 d, cmd := a.dialog.Update(msg)
114 a.dialog = d.(core.DialogCmp)
115 a.dialogVisible = true
116 return a, cmd
117 case core.DialogCloseMsg:
118 d, cmd := a.dialog.Update(msg)
119 a.dialog = d.(core.DialogCmp)
120 a.dialogVisible = false
121 return a, cmd
122 case page.PageChangeMsg:
123 return a, a.moveToPage(msg.ID)
124 case util.InfoMsg:
125 a.status, _ = a.status.Update(msg)
126 case tea.KeyMsg:
127 if a.editorMode == vimtea.ModeNormal {
128 switch {
129 case key.Matches(msg, keys.Quit):
130 return a, dialog.NewQuitDialogCmd()
131 case key.Matches(msg, keys.Back):
132 if a.previousPage != "" {
133 return a, a.moveToPage(a.previousPage)
134 }
135 case key.Matches(msg, keys.Return):
136 if a.showHelp {
137 a.ToggleHelp()
138 return a, nil
139 }
140 case key.Matches(msg, replKeyMap):
141 if a.currentPage == page.ReplPage {
142 sessions, err := a.app.Sessions.List()
143 if err != nil {
144 return a, util.CmdHandler(util.ReportError(err))
145 }
146 lastSession := sessions[0]
147 if lastSession.MessageCount == 0 {
148 return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: lastSession.ID})
149 }
150 s, err := a.app.Sessions.Create("New Session")
151 if err != nil {
152 return a, util.CmdHandler(util.ReportError(err))
153 }
154 return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
155 }
156 case key.Matches(msg, keys.Logs):
157 return a, a.moveToPage(page.LogsPage)
158 case key.Matches(msg, keys.Help):
159 a.ToggleHelp()
160 return a, nil
161 }
162 }
163 }
164
165 var cmds []tea.Cmd
166 s, cmd := a.status.Update(msg)
167 a.status = s
168 cmds = append(cmds, cmd)
169 if a.dialogVisible {
170 d, cmd := a.dialog.Update(msg)
171 a.dialog = d.(core.DialogCmp)
172 cmds = append(cmds, cmd)
173 return a, tea.Batch(cmds...)
174 }
175 p, cmd := a.pages[a.currentPage].Update(msg)
176 a.pages[a.currentPage] = p
177 cmds = append(cmds, cmd)
178 return a, tea.Batch(cmds...)
179}
180
181func (a *appModel) ToggleHelp() {
182 if a.showHelp {
183 a.showHelp = false
184 a.height += a.help.Height()
185 } else {
186 a.showHelp = true
187 a.height -= a.help.Height()
188 }
189
190 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
191 sizable.SetSize(a.width, a.height)
192 }
193}
194
195func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
196 var cmd tea.Cmd
197 if _, ok := a.loadedPages[pageID]; !ok {
198 cmd = a.pages[pageID].Init()
199 a.loadedPages[pageID] = true
200 }
201 a.previousPage = a.currentPage
202 a.currentPage = pageID
203 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
204 sizable.SetSize(a.width, a.height)
205 }
206
207 return cmd
208}
209
210func (a appModel) View() string {
211 components := []string{
212 a.pages[a.currentPage].View(),
213 }
214
215 if a.showHelp {
216 bindings := layout.KeyMapToSlice(keys)
217 if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
218 bindings = append(bindings, p.BindingKeys()...)
219 }
220 if a.dialogVisible {
221 bindings = append(bindings, a.dialog.BindingKeys()...)
222 }
223 if a.currentPage == page.ReplPage {
224 bindings = append(bindings, replKeyMap)
225 }
226 a.help.SetBindings(bindings)
227 components = append(components, a.help.View())
228 }
229
230 components = append(components, a.status.View())
231
232 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
233
234 if a.dialogVisible {
235 overlay := a.dialog.View()
236 row := lipgloss.Height(appView) / 2
237 row -= lipgloss.Height(overlay) / 2
238 col := lipgloss.Width(appView) / 2
239 col -= lipgloss.Width(overlay) / 2
240 appView = layout.PlaceOverlay(
241 col,
242 row,
243 overlay,
244 appView,
245 true,
246 )
247 }
248 return appView
249}
250
251func New(app *app.App) tea.Model {
252 // homedir, _ := os.UserHomeDir()
253 // configPath := filepath.Join(homedir, ".termai.yaml")
254 //
255 startPage := page.ReplPage
256 // if _, err := os.Stat(configPath); os.IsNotExist(err) {
257 // startPage = page.InitPage
258 // }
259
260 return &appModel{
261 currentPage: startPage,
262 loadedPages: make(map[page.PageID]bool),
263 status: core.NewStatusCmp(),
264 help: core.NewHelpCmp(),
265 dialog: core.NewDialogCmp(),
266 app: app,
267 pages: map[page.PageID]tea.Model{
268 page.LogsPage: page.NewLogsPage(),
269 page.InitPage: page.NewInitPage(),
270 page.ReplPage: page.NewReplPage(app),
271 },
272 }
273}