tui.go

  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}