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