split.go

  1package layout
  2
  3import (
  4	"github.com/charmbracelet/bubbles/key"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/lipgloss"
  7	"github.com/opencode-ai/opencode/internal/tui/styles"
  8)
  9
 10type SplitPaneLayout interface {
 11	tea.Model
 12	Sizeable
 13	Bindings
 14	SetLeftPanel(panel Container) tea.Cmd
 15	SetRightPanel(panel Container) tea.Cmd
 16	SetBottomPanel(panel Container) tea.Cmd
 17
 18	ClearLeftPanel() tea.Cmd
 19	ClearRightPanel() tea.Cmd
 20	ClearBottomPanel() tea.Cmd
 21}
 22
 23type splitPaneLayout struct {
 24	width         int
 25	height        int
 26	ratio         float64
 27	verticalRatio float64
 28
 29	rightPanel  Container
 30	leftPanel   Container
 31	bottomPanel Container
 32
 33	backgroundColor lipgloss.TerminalColor
 34}
 35
 36type SplitPaneOption func(*splitPaneLayout)
 37
 38func (s *splitPaneLayout) Init() tea.Cmd {
 39	var cmds []tea.Cmd
 40
 41	if s.leftPanel != nil {
 42		cmds = append(cmds, s.leftPanel.Init())
 43	}
 44
 45	if s.rightPanel != nil {
 46		cmds = append(cmds, s.rightPanel.Init())
 47	}
 48
 49	if s.bottomPanel != nil {
 50		cmds = append(cmds, s.bottomPanel.Init())
 51	}
 52
 53	return tea.Batch(cmds...)
 54}
 55
 56func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 57	var cmds []tea.Cmd
 58	switch msg := msg.(type) {
 59	case tea.WindowSizeMsg:
 60		return s, s.SetSize(msg.Width, msg.Height)
 61	}
 62
 63	if s.rightPanel != nil {
 64		u, cmd := s.rightPanel.Update(msg)
 65		s.rightPanel = u.(Container)
 66		if cmd != nil {
 67			cmds = append(cmds, cmd)
 68		}
 69	}
 70
 71	if s.leftPanel != nil {
 72		u, cmd := s.leftPanel.Update(msg)
 73		s.leftPanel = u.(Container)
 74		if cmd != nil {
 75			cmds = append(cmds, cmd)
 76		}
 77	}
 78
 79	if s.bottomPanel != nil {
 80		u, cmd := s.bottomPanel.Update(msg)
 81		s.bottomPanel = u.(Container)
 82		if cmd != nil {
 83			cmds = append(cmds, cmd)
 84		}
 85	}
 86
 87	return s, tea.Batch(cmds...)
 88}
 89
 90func (s *splitPaneLayout) View() string {
 91	var topSection string
 92
 93	if s.leftPanel != nil && s.rightPanel != nil {
 94		leftView := s.leftPanel.View()
 95		rightView := s.rightPanel.View()
 96		topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
 97	} else if s.leftPanel != nil {
 98		topSection = s.leftPanel.View()
 99	} else if s.rightPanel != nil {
100		topSection = s.rightPanel.View()
101	} else {
102		topSection = ""
103	}
104
105	var finalView string
106
107	if s.bottomPanel != nil && topSection != "" {
108		bottomView := s.bottomPanel.View()
109		finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
110	} else if s.bottomPanel != nil {
111		finalView = s.bottomPanel.View()
112	} else {
113		finalView = topSection
114	}
115
116	if s.backgroundColor != nil && finalView != "" {
117		style := lipgloss.NewStyle().
118			Width(s.width).
119			Height(s.height).
120			Background(s.backgroundColor)
121
122		return style.Render(finalView)
123	}
124
125	return finalView
126}
127
128func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
129	s.width = width
130	s.height = height
131
132	var topHeight, bottomHeight int
133	if s.bottomPanel != nil {
134		topHeight = int(float64(height) * s.verticalRatio)
135		bottomHeight = height - topHeight
136	} else {
137		topHeight = height
138		bottomHeight = 0
139	}
140
141	var leftWidth, rightWidth int
142	if s.leftPanel != nil && s.rightPanel != nil {
143		leftWidth = int(float64(width) * s.ratio)
144		rightWidth = width - leftWidth
145	} else if s.leftPanel != nil {
146		leftWidth = width
147		rightWidth = 0
148	} else if s.rightPanel != nil {
149		leftWidth = 0
150		rightWidth = width
151	}
152
153	var cmds []tea.Cmd
154	if s.leftPanel != nil {
155		cmd := s.leftPanel.SetSize(leftWidth, topHeight)
156		cmds = append(cmds, cmd)
157	}
158
159	if s.rightPanel != nil {
160		cmd := s.rightPanel.SetSize(rightWidth, topHeight)
161		cmds = append(cmds, cmd)
162	}
163
164	if s.bottomPanel != nil {
165		cmd := s.bottomPanel.SetSize(width, bottomHeight)
166		cmds = append(cmds, cmd)
167	}
168	return tea.Batch(cmds...)
169}
170
171func (s *splitPaneLayout) GetSize() (int, int) {
172	return s.width, s.height
173}
174
175func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
176	s.leftPanel = panel
177	if s.width > 0 && s.height > 0 {
178		return s.SetSize(s.width, s.height)
179	}
180	return nil
181}
182
183func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
184	s.rightPanel = panel
185	if s.width > 0 && s.height > 0 {
186		return s.SetSize(s.width, s.height)
187	}
188	return nil
189}
190
191func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
192	s.bottomPanel = panel
193	if s.width > 0 && s.height > 0 {
194		return s.SetSize(s.width, s.height)
195	}
196	return nil
197}
198
199func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
200	s.leftPanel = nil
201	if s.width > 0 && s.height > 0 {
202		return s.SetSize(s.width, s.height)
203	}
204	return nil
205}
206
207func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
208	s.rightPanel = nil
209	if s.width > 0 && s.height > 0 {
210		return s.SetSize(s.width, s.height)
211	}
212	return nil
213}
214
215func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
216	s.bottomPanel = nil
217	if s.width > 0 && s.height > 0 {
218		return s.SetSize(s.width, s.height)
219	}
220	return nil
221}
222
223func (s *splitPaneLayout) BindingKeys() []key.Binding {
224	keys := []key.Binding{}
225	if s.leftPanel != nil {
226		if b, ok := s.leftPanel.(Bindings); ok {
227			keys = append(keys, b.BindingKeys()...)
228		}
229	}
230	if s.rightPanel != nil {
231		if b, ok := s.rightPanel.(Bindings); ok {
232			keys = append(keys, b.BindingKeys()...)
233		}
234	}
235	if s.bottomPanel != nil {
236		if b, ok := s.bottomPanel.(Bindings); ok {
237			keys = append(keys, b.BindingKeys()...)
238		}
239	}
240	return keys
241}
242
243func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
244	layout := &splitPaneLayout{
245		ratio:           0.7,
246		verticalRatio:   0.9, // Default 80% for top section, 20% for bottom
247		backgroundColor: styles.Background,
248	}
249	for _, option := range options {
250		option(layout)
251	}
252	return layout
253}
254
255func WithLeftPanel(panel Container) SplitPaneOption {
256	return func(s *splitPaneLayout) {
257		s.leftPanel = panel
258	}
259}
260
261func WithRightPanel(panel Container) SplitPaneOption {
262	return func(s *splitPaneLayout) {
263		s.rightPanel = panel
264	}
265}
266
267func WithRatio(ratio float64) SplitPaneOption {
268	return func(s *splitPaneLayout) {
269		s.ratio = ratio
270	}
271}
272
273func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
274	return func(s *splitPaneLayout) {
275		s.backgroundColor = color
276	}
277}
278
279func WithBottomPanel(panel Container) SplitPaneOption {
280	return func(s *splitPaneLayout) {
281		s.bottomPanel = panel
282	}
283}
284
285func WithVerticalRatio(ratio float64) SplitPaneOption {
286	return func(s *splitPaneLayout) {
287		s.verticalRatio = ratio
288	}
289}