1package tui
2
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/opencode-ai/opencode/internal/app"
8 "github.com/opencode-ai/opencode/internal/logging"
9 "github.com/opencode-ai/opencode/internal/pubsub"
10 "github.com/opencode-ai/opencode/internal/tui/components/completions"
11 "github.com/opencode-ai/opencode/internal/tui/components/core"
12 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
13 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
14 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/page"
17 "github.com/opencode-ai/opencode/internal/tui/theme"
18 "github.com/opencode-ai/opencode/internal/tui/util"
19)
20
21type appModel struct {
22 width, height int
23 keyMap KeyMap
24
25 currentPage page.PageID
26 previousPage page.PageID
27 pages map[page.PageID]util.Model
28 loadedPages map[page.PageID]bool
29
30 status core.StatusCmp
31
32 app *app.App
33
34 dialog dialogs.DialogCmp
35 completions completions.Completions
36}
37
38func (a appModel) Init() tea.Cmd {
39 var cmds []tea.Cmd
40 cmd := a.pages[a.currentPage].Init()
41 cmds = append(cmds, cmd)
42 a.loadedPages[a.currentPage] = true
43
44 cmd = a.status.Init()
45 cmds = append(cmds, cmd)
46 return tea.Batch(cmds...)
47}
48
49func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
50 var cmds []tea.Cmd
51 var cmd tea.Cmd
52
53 switch msg := msg.(type) {
54 case tea.WindowSizeMsg:
55 return a, a.handleWindowResize(msg)
56
57 // Completions messages
58 case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg, completions.CloseCompletionsMsg:
59 u, completionCmd := a.completions.Update(msg)
60 a.completions = u.(completions.Completions)
61 return a, completionCmd
62
63 // Dialog messages
64 case dialogs.OpenDialogMsg, dialogs.CloseDialogMsg:
65 u, dialogCmd := a.dialog.Update(msg)
66 a.dialog = u.(dialogs.DialogCmp)
67 return a, dialogCmd
68 case commands.ShowArgumentsDialogMsg:
69 return a, util.CmdHandler(
70 dialogs.OpenDialogMsg{
71 Model: commands.NewCommandArgumentsDialog(
72 msg.CommandID,
73 msg.Content,
74 msg.ArgNames,
75 ),
76 },
77 )
78 // Page change messages
79 case page.PageChangeMsg:
80 return a, a.moveToPage(msg.ID)
81
82 // Status Messages
83 case util.InfoMsg, util.ClearStatusMsg:
84 s, statusCmd := a.status.Update(msg)
85 a.status = s.(core.StatusCmp)
86 cmds = append(cmds, statusCmd)
87 return a, tea.Batch(cmds...)
88
89 // Logs
90 case pubsub.Event[logging.LogMessage]:
91 // Send to the status component
92 s, statusCmd := a.status.Update(msg)
93 a.status = s.(core.StatusCmp)
94 cmds = append(cmds, statusCmd)
95
96 // If the current page is logs, update the logs view
97 if a.currentPage == page.LogsPage {
98 updated, pageCmd := a.pages[a.currentPage].Update(msg)
99 a.pages[a.currentPage] = updated.(util.Model)
100 cmds = append(cmds, pageCmd)
101 }
102 return a, tea.Batch(cmds...)
103 case tea.KeyPressMsg:
104 return a, a.handleKeyPressMsg(msg)
105 }
106 s, _ := a.status.Update(msg)
107 a.status = s.(core.StatusCmp)
108 updated, cmd := a.pages[a.currentPage].Update(msg)
109 a.pages[a.currentPage] = updated.(util.Model)
110 cmds = append(cmds, cmd)
111 return a, tea.Batch(cmds...)
112}
113
114func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
115 var cmds []tea.Cmd
116 msg.Height -= 1 // Make space for the status bar
117 a.width, a.height = msg.Width, msg.Height
118
119 // Update status bar
120 s, cmd := a.status.Update(msg)
121 a.status = s.(core.StatusCmp)
122 cmds = append(cmds, cmd)
123
124 // Update the current page
125 updated, cmd := a.pages[a.currentPage].Update(msg)
126 a.pages[a.currentPage] = updated.(util.Model)
127 cmds = append(cmds, cmd)
128
129 // Update the dialogs
130 dialog, cmd := a.dialog.Update(msg)
131 a.dialog = dialog.(dialogs.DialogCmp)
132 cmds = append(cmds, cmd)
133
134 return tea.Batch(cmds...)
135}
136
137func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
138 switch {
139 // completions
140 case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Up):
141 u, cmd := a.completions.Update(msg)
142 a.completions = u.(completions.Completions)
143 return cmd
144
145 case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Down):
146 u, cmd := a.completions.Update(msg)
147 a.completions = u.(completions.Completions)
148 return cmd
149 case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Select):
150 u, cmd := a.completions.Update(msg)
151 a.completions = u.(completions.Completions)
152 return cmd
153 case a.completions.Open() && key.Matches(msg, a.completions.KeyMap().Cancel):
154 u, cmd := a.completions.Update(msg)
155 a.completions = u.(completions.Completions)
156 return cmd
157 // dialogs
158 case key.Matches(msg, a.keyMap.Quit):
159 return util.CmdHandler(dialogs.OpenDialogMsg{
160 Model: quit.NewQuitDialog(),
161 })
162
163 case key.Matches(msg, a.keyMap.Commands):
164 return util.CmdHandler(dialogs.OpenDialogMsg{
165 Model: commands.NewCommandDialog(),
166 })
167
168 // Page navigation
169 case key.Matches(msg, a.keyMap.Logs):
170 return a.moveToPage(page.LogsPage)
171
172 default:
173 if a.dialog.HasDialogs() {
174 u, dialogCmd := a.dialog.Update(msg)
175 a.dialog = u.(dialogs.DialogCmp)
176 return dialogCmd
177 } else {
178 updated, cmd := a.pages[a.currentPage].Update(msg)
179 a.pages[a.currentPage] = updated.(util.Model)
180 return cmd
181 }
182 }
183}
184
185// RegisterCommand adds a command to the command dialog
186// func (a *appModel) RegisterCommand(cmd dialog.Command) {
187// a.commands = append(a.commands, cmd)
188// }
189
190func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
191 if a.app.CoderAgent.IsBusy() {
192 // For now we don't move to any page if the agent is busy
193 return util.ReportWarn("Agent is busy, please wait...")
194 }
195
196 var cmds []tea.Cmd
197 if _, ok := a.loadedPages[pageID]; !ok {
198 cmd := a.pages[pageID].Init()
199 cmds = append(cmds, cmd)
200 a.loadedPages[pageID] = true
201 }
202 a.previousPage = a.currentPage
203 a.currentPage = pageID
204 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
205 cmd := sizable.SetSize(a.width, a.height)
206 cmds = append(cmds, cmd)
207 }
208
209 return tea.Batch(cmds...)
210}
211
212func (a *appModel) View() tea.View {
213 pageView := a.pages[a.currentPage].View()
214 components := []string{
215 pageView.String(),
216 }
217 components = append(components, a.status.View().String())
218
219 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
220 layers := []*lipgloss.Layer{
221 lipgloss.NewLayer(appView),
222 }
223 t := theme.CurrentTheme()
224 if a.dialog.HasDialogs() {
225 layers = append(
226 layers,
227 a.dialog.GetLayers()...,
228 )
229 }
230
231 cursor := pageView.Cursor()
232 activeView := a.dialog.ActiveView()
233 if activeView != nil {
234 cursor = activeView.Cursor()
235 }
236
237 if a.completions.Open() && cursor != nil {
238 cmp := a.completions.View().String()
239 x, y := a.completions.Position()
240 layers = append(
241 layers,
242 lipgloss.NewLayer(cmp).X(x).Y(y),
243 )
244 }
245
246 canvas := lipgloss.NewCanvas(
247 layers...,
248 )
249 view := tea.NewView(canvas.Render())
250 view.SetBackgroundColor(t.Background())
251 view.SetCursor(cursor)
252 return view
253}
254
255func New(app *app.App) tea.Model {
256 startPage := page.ChatPage
257 model := &appModel{
258 currentPage: startPage,
259 app: app,
260 status: core.NewStatusCmp(app.LSPClients),
261 loadedPages: make(map[page.PageID]bool),
262 keyMap: DefaultKeyMap(),
263
264 pages: map[page.PageID]util.Model{
265 page.ChatPage: page.NewChatPage(app),
266 page.LogsPage: page.NewLogsPage(),
267 },
268
269 dialog: dialogs.NewDialogCmp(),
270 completions: completions.New(),
271 }
272
273 return model
274}