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