summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/layout
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-18 20:17:38 +0200
committerKujtim Hoxha <[email protected]>2025-04-21 13:42:27 +0200
commit333ea6ec4b2abfc2c1a9c3f6b0918ca5d296347f (patch)
treee0d456417368e8716c81ee43b82be3d6ed39c59e /internal/tui/layout
parent05d0e86f10369fd0e51a924ac88029fb92591499 (diff)
downloadopencode-333ea6ec4b2abfc2c1a9c3f6b0918ca5d296347f.tar.gz
opencode-333ea6ec4b2abfc2c1a9c3f6b0918ca5d296347f.zip
implement patch, update ui, improve rendering
Diffstat (limited to 'internal/tui/layout')
-rw-r--r--internal/tui/layout/bento.go392
-rw-r--r--internal/tui/layout/border.go121
-rw-r--r--internal/tui/layout/container.go5
-rw-r--r--internal/tui/layout/grid.go254
-rw-r--r--internal/tui/layout/layout.go6
-rw-r--r--internal/tui/layout/single.go189
-rw-r--r--internal/tui/layout/split.go37
7 files changed, 26 insertions, 978 deletions
diff --git a/internal/tui/layout/bento.go b/internal/tui/layout/bento.go
deleted file mode 100644
index c47c4e090..000000000
--- a/internal/tui/layout/bento.go
+++ /dev/null
@@ -1,392 +0,0 @@
-package layout
-
-import (
- "github.com/charmbracelet/bubbles/key"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
-)
-
-type paneID string
-
-const (
- BentoLeftPane paneID = "left"
- BentoRightTopPane paneID = "right-top"
- BentoRightBottomPane paneID = "right-bottom"
-)
-
-type BentoPanes map[paneID]tea.Model
-
-const (
- defaultLeftWidthRatio = 0.2
- defaultRightTopHeightRatio = 0.85
-
- minLeftWidth = 10
- minRightBottomHeight = 10
-)
-
-type BentoLayout interface {
- tea.Model
- Sizeable
- Bindings
-}
-
-type BentoKeyBindings struct {
- SwitchPane key.Binding
- SwitchPaneBack key.Binding
- HideCurrentPane key.Binding
- ShowAllPanes key.Binding
-}
-
-var defaultBentoKeyBindings = BentoKeyBindings{
- SwitchPane: key.NewBinding(
- key.WithKeys("tab"),
- key.WithHelp("tab", "switch pane"),
- ),
- SwitchPaneBack: key.NewBinding(
- key.WithKeys("shift+tab"),
- key.WithHelp("shift+tab", "switch pane back"),
- ),
- HideCurrentPane: key.NewBinding(
- key.WithKeys("X"),
- key.WithHelp("X", "hide current pane"),
- ),
- ShowAllPanes: key.NewBinding(
- key.WithKeys("R"),
- key.WithHelp("R", "show all panes"),
- ),
-}
-
-type bentoLayout struct {
- width int
- height int
-
- leftWidthRatio float64
- rightTopHeightRatio float64
-
- currentPane paneID
- panes map[paneID]SinglePaneLayout
- hiddenPanes map[paneID]bool
-}
-
-func (b *bentoLayout) GetSize() (int, int) {
- return b.width, b.height
-}
-
-func (b *bentoLayout) Init() tea.Cmd {
- var cmds []tea.Cmd
- for _, pane := range b.panes {
- cmd := pane.Init()
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
- return tea.Batch(cmds...)
-}
-
-func (b *bentoLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- b.SetSize(msg.Width, msg.Height)
- return b, nil
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, defaultBentoKeyBindings.SwitchPane):
- return b, b.SwitchPane(false)
- case key.Matches(msg, defaultBentoKeyBindings.SwitchPaneBack):
- return b, b.SwitchPane(true)
- case key.Matches(msg, defaultBentoKeyBindings.HideCurrentPane):
- return b, b.HidePane(b.currentPane)
- case key.Matches(msg, defaultBentoKeyBindings.ShowAllPanes):
- for id := range b.hiddenPanes {
- delete(b.hiddenPanes, id)
- }
- b.SetSize(b.width, b.height)
- return b, nil
- }
- }
-
- var cmds []tea.Cmd
- for id, pane := range b.panes {
- u, cmd := pane.Update(msg)
- b.panes[id] = u.(SinglePaneLayout)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
- return b, tea.Batch(cmds...)
-}
-
-func (b *bentoLayout) View() string {
- if b.width <= 0 || b.height <= 0 {
- return ""
- }
-
- for id, pane := range b.panes {
- if b.currentPane == id {
- pane.Focus()
- } else {
- pane.Blur()
- }
- }
-
- leftVisible := false
- rightTopVisible := false
- rightBottomVisible := false
-
- var leftPane, rightTopPane, rightBottomPane string
-
- if pane, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] {
- leftPane = pane.View()
- leftVisible = true
- }
-
- if pane, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] {
- rightTopPane = pane.View()
- rightTopVisible = true
- }
-
- if pane, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] {
- rightBottomPane = pane.View()
- rightBottomVisible = true
- }
-
- if leftVisible {
- if rightTopVisible || rightBottomVisible {
- rightSection := ""
- if rightTopVisible && rightBottomVisible {
- rightSection = lipgloss.JoinVertical(lipgloss.Top, rightTopPane, rightBottomPane)
- } else if rightTopVisible {
- rightSection = rightTopPane
- } else {
- rightSection = rightBottomPane
- }
- return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(
- lipgloss.JoinHorizontal(lipgloss.Left, leftPane, rightSection),
- )
- } else {
- return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(leftPane)
- }
- } else if rightTopVisible || rightBottomVisible {
- if rightTopVisible && rightBottomVisible {
- return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(
- lipgloss.JoinVertical(lipgloss.Top, rightTopPane, rightBottomPane),
- )
- } else if rightTopVisible {
- return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(rightTopPane)
- } else {
- return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(rightBottomPane)
- }
- }
- return ""
-}
-
-func (b *bentoLayout) SetSize(width int, height int) {
- if width < 0 || height < 0 {
- return
- }
- b.width = width
- b.height = height
-
- leftExists := false
- rightTopExists := false
- rightBottomExists := false
-
- if _, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] {
- leftExists = true
- }
- if _, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] {
- rightTopExists = true
- }
- if _, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] {
- rightBottomExists = true
- }
-
- leftWidth := 0
- rightWidth := 0
- rightTopHeight := 0
- rightBottomHeight := 0
-
- if leftExists && (rightTopExists || rightBottomExists) {
- leftWidth = int(float64(width) * b.leftWidthRatio)
- if leftWidth < minLeftWidth && width >= minLeftWidth {
- leftWidth = minLeftWidth
- }
- rightWidth = width - leftWidth
-
- if rightTopExists && rightBottomExists {
- rightTopHeight = int(float64(height) * b.rightTopHeightRatio)
- rightBottomHeight = height - rightTopHeight
-
- if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight {
- rightBottomHeight = minRightBottomHeight
- rightTopHeight = height - rightBottomHeight
- }
- } else if rightTopExists {
- rightTopHeight = height
- } else if rightBottomExists {
- rightBottomHeight = height
- }
- } else if leftExists {
- leftWidth = width
- } else if rightTopExists || rightBottomExists {
- rightWidth = width
-
- if rightTopExists && rightBottomExists {
- rightTopHeight = int(float64(height) * b.rightTopHeightRatio)
- rightBottomHeight = height - rightTopHeight
-
- if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight {
- rightBottomHeight = minRightBottomHeight
- rightTopHeight = height - rightBottomHeight
- }
- } else if rightTopExists {
- rightTopHeight = height
- } else if rightBottomExists {
- rightBottomHeight = height
- }
- }
-
- if pane, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] {
- pane.SetSize(leftWidth, height)
- }
- if pane, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] {
- pane.SetSize(rightWidth, rightTopHeight)
- }
- if pane, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] {
- pane.SetSize(rightWidth, rightBottomHeight)
- }
-}
-
-func (b *bentoLayout) HidePane(pane paneID) tea.Cmd {
- if len(b.panes)-len(b.hiddenPanes) == 1 {
- return nil
- }
- if _, ok := b.panes[pane]; ok {
- b.hiddenPanes[pane] = true
- }
- b.SetSize(b.width, b.height)
- return b.SwitchPane(false)
-}
-
-func (b *bentoLayout) SwitchPane(back bool) tea.Cmd {
- orderForward := []paneID{BentoLeftPane, BentoRightTopPane, BentoRightBottomPane}
- orderBackward := []paneID{BentoLeftPane, BentoRightBottomPane, BentoRightTopPane}
-
- order := orderForward
- if back {
- order = orderBackward
- }
-
- currentIdx := -1
- for i, id := range order {
- if id == b.currentPane {
- currentIdx = i
- break
- }
- }
-
- if currentIdx == -1 {
- for _, id := range order {
- if _, exists := b.panes[id]; exists {
- if _, hidden := b.hiddenPanes[id]; !hidden {
- b.currentPane = id
- break
- }
- }
- }
- } else {
- startIdx := currentIdx
- for {
- currentIdx = (currentIdx + 1) % len(order)
-
- nextID := order[currentIdx]
- if _, exists := b.panes[nextID]; exists {
- if _, hidden := b.hiddenPanes[nextID]; !hidden {
- b.currentPane = nextID
- break
- }
- }
-
- if currentIdx == startIdx {
- break
- }
- }
- }
-
- var cmds []tea.Cmd
- for id, pane := range b.panes {
- if _, ok := b.hiddenPanes[id]; ok {
- continue
- }
- if id == b.currentPane {
- cmds = append(cmds, pane.Focus())
- } else {
- cmds = append(cmds, pane.Blur())
- }
- }
-
- return tea.Batch(cmds...)
-}
-
-func (s *bentoLayout) BindingKeys() []key.Binding {
- bindings := KeyMapToSlice(defaultBentoKeyBindings)
- if b, ok := s.panes[s.currentPane].(Bindings); ok {
- bindings = append(bindings, b.BindingKeys()...)
- }
- return bindings
-}
-
-type BentoLayoutOption func(*bentoLayout)
-
-func NewBentoLayout(panes BentoPanes, opts ...BentoLayoutOption) BentoLayout {
- p := make(map[paneID]SinglePaneLayout, len(panes))
- for id, pane := range panes {
- if sp, ok := pane.(SinglePaneLayout); !ok {
- p[id] = NewSinglePane(
- pane,
- WithSinglePaneFocusable(true),
- WithSinglePaneBordered(true),
- )
- } else {
- p[id] = sp
- }
- }
- if len(p) == 0 {
- panic("no panes provided for BentoLayout")
- }
- layout := &bentoLayout{
- panes: p,
- hiddenPanes: make(map[paneID]bool),
- currentPane: BentoLeftPane,
- leftWidthRatio: defaultLeftWidthRatio,
- rightTopHeightRatio: defaultRightTopHeightRatio,
- }
-
- for _, opt := range opts {
- opt(layout)
- }
-
- return layout
-}
-
-func WithBentoLayoutLeftWidthRatio(ratio float64) BentoLayoutOption {
- return func(b *bentoLayout) {
- if ratio > 0 && ratio < 1 {
- b.leftWidthRatio = ratio
- }
- }
-}
-
-func WithBentoLayoutRightTopHeightRatio(ratio float64) BentoLayoutOption {
- return func(b *bentoLayout) {
- if ratio > 0 && ratio < 1 {
- b.rightTopHeightRatio = ratio
- }
- }
-}
-
-func WithBentoLayoutCurrentPane(pane paneID) BentoLayoutOption {
- return func(b *bentoLayout) {
- b.currentPane = pane
- }
-}
diff --git a/internal/tui/layout/border.go b/internal/tui/layout/border.go
deleted file mode 100644
index ea9f5e0bc..000000000
--- a/internal/tui/layout/border.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package layout
-
-import (
- "fmt"
- "strings"
-
- "github.com/charmbracelet/lipgloss"
- "github.com/kujtimiihoxha/opencode/internal/tui/styles"
-)
-
-type BorderPosition int
-
-const (
- TopLeftBorder BorderPosition = iota
- TopMiddleBorder
- TopRightBorder
- BottomLeftBorder
- BottomMiddleBorder
- BottomRightBorder
-)
-
-var (
- ActiveBorder = styles.Blue
- InactivePreviewBorder = styles.Grey
-)
-
-type BorderOptions struct {
- Active bool
- EmbeddedText map[BorderPosition]string
- ActiveColor lipgloss.TerminalColor
- InactiveColor lipgloss.TerminalColor
- ActiveBorder lipgloss.Border
- InactiveBorder lipgloss.Border
-}
-
-func Borderize(content string, opts BorderOptions) string {
- if opts.EmbeddedText == nil {
- opts.EmbeddedText = make(map[BorderPosition]string)
- }
- if opts.ActiveColor == nil {
- opts.ActiveColor = ActiveBorder
- }
- if opts.InactiveColor == nil {
- opts.InactiveColor = InactivePreviewBorder
- }
- if opts.ActiveBorder == (lipgloss.Border{}) {
- opts.ActiveBorder = lipgloss.ThickBorder()
- }
- if opts.InactiveBorder == (lipgloss.Border{}) {
- opts.InactiveBorder = lipgloss.NormalBorder()
- }
-
- var (
- thickness = map[bool]lipgloss.Border{
- true: opts.ActiveBorder,
- false: opts.InactiveBorder,
- }
- color = map[bool]lipgloss.TerminalColor{
- true: opts.ActiveColor,
- false: opts.InactiveColor,
- }
- border = thickness[opts.Active]
- style = lipgloss.NewStyle().Foreground(color[opts.Active])
- width = lipgloss.Width(content)
- )
-
- encloseInSquareBrackets := func(text string) string {
- if text != "" {
- return fmt.Sprintf("%s%s%s",
- style.Render(border.TopRight),
- text,
- style.Render(border.TopLeft),
- )
- }
- return text
- }
- buildHorizontalBorder := func(leftText, middleText, rightText, leftCorner, inbetween, rightCorner string) string {
- leftText = encloseInSquareBrackets(leftText)
- middleText = encloseInSquareBrackets(middleText)
- rightText = encloseInSquareBrackets(rightText)
- // Calculate length of border between embedded texts
- remaining := max(0, width-lipgloss.Width(leftText)-lipgloss.Width(middleText)-lipgloss.Width(rightText))
- leftBorderLen := max(0, (width/2)-lipgloss.Width(leftText)-(lipgloss.Width(middleText)/2))
- rightBorderLen := max(0, remaining-leftBorderLen)
- // Then construct border string
- s := leftText +
- style.Render(strings.Repeat(inbetween, leftBorderLen)) +
- middleText +
- style.Render(strings.Repeat(inbetween, rightBorderLen)) +
- rightText
- // Make it fit in the space available between the two corners.
- s = lipgloss.NewStyle().
- Inline(true).
- MaxWidth(width).
- Render(s)
- // Add the corners
- return style.Render(leftCorner) + s + style.Render(rightCorner)
- }
- // Stack top border, content and horizontal borders, and bottom border.
- return strings.Join([]string{
- buildHorizontalBorder(
- opts.EmbeddedText[TopLeftBorder],
- opts.EmbeddedText[TopMiddleBorder],
- opts.EmbeddedText[TopRightBorder],
- border.TopLeft,
- border.Top,
- border.TopRight,
- ),
- lipgloss.NewStyle().
- BorderForeground(color[opts.Active]).
- Border(border, false, true, false, true).Render(content),
- buildHorizontalBorder(
- opts.EmbeddedText[BottomLeftBorder],
- opts.EmbeddedText[BottomMiddleBorder],
- opts.EmbeddedText[BottomRightBorder],
- border.BottomLeft,
- border.Bottom,
- border.BottomRight,
- ),
- }, "\n")
-}
diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go
index c86d954ea..fdb9ab403 100644
--- a/internal/tui/layout/container.go
+++ b/internal/tui/layout/container.go
@@ -86,7 +86,7 @@ func (c *container) View() string {
return style.Render(c.content.View())
}
-func (c *container) SetSize(width, height int) {
+func (c *container) SetSize(width, height int) tea.Cmd {
c.width = width
c.height = height
@@ -113,8 +113,9 @@ func (c *container) SetSize(width, height int) {
// Set content size with adjusted dimensions
contentWidth := max(0, width-horizontalSpace)
contentHeight := max(0, height-verticalSpace)
- sizeable.SetSize(contentWidth, contentHeight)
+ return sizeable.SetSize(contentWidth, contentHeight)
}
+ return nil
}
func (c *container) GetSize() (int, int) {
diff --git a/internal/tui/layout/grid.go b/internal/tui/layout/grid.go
deleted file mode 100644
index 6be493e2c..000000000
--- a/internal/tui/layout/grid.go
+++ /dev/null
@@ -1,254 +0,0 @@
-package layout
-
-import (
- "github.com/charmbracelet/bubbles/key"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
-)
-
-type GridLayout interface {
- tea.Model
- Sizeable
- Bindings
- Panes() [][]tea.Model
-}
-
-type gridLayout struct {
- width int
- height int
-
- rows int
- columns int
-
- panes [][]tea.Model
-
- gap int
- bordered bool
- focusable bool
-
- currentRow int
- currentColumn int
-
- activeColor lipgloss.TerminalColor
-}
-
-type GridOption func(*gridLayout)
-
-func (g *gridLayout) Init() tea.Cmd {
- var cmds []tea.Cmd
- for i := range g.panes {
- for j := range g.panes[i] {
- if g.panes[i][j] != nil {
- cmds = append(cmds, g.panes[i][j].Init())
- }
- }
- }
- return tea.Batch(cmds...)
-}
-
-func (g *gridLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
-
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- g.SetSize(msg.Width, msg.Height)
- return g, nil
- case tea.KeyMsg:
- if key.Matches(msg, g.nextPaneBinding()) {
- return g.focusNextPane()
- }
- }
-
- // Update all panes
- for i := range g.panes {
- for j := range g.panes[i] {
- if g.panes[i][j] != nil {
- var cmd tea.Cmd
- g.panes[i][j], cmd = g.panes[i][j].Update(msg)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
- }
- }
-
- return g, tea.Batch(cmds...)
-}
-
-func (g *gridLayout) focusNextPane() (tea.Model, tea.Cmd) {
- if !g.focusable {
- return g, nil
- }
-
- var cmds []tea.Cmd
-
- // Blur current pane
- if g.currentRow < len(g.panes) && g.currentColumn < len(g.panes[g.currentRow]) {
- if currentPane, ok := g.panes[g.currentRow][g.currentColumn].(Focusable); ok {
- cmds = append(cmds, currentPane.Blur())
- }
- }
-
- // Find next valid pane
- g.currentColumn++
- if g.currentColumn >= len(g.panes[g.currentRow]) {
- g.currentColumn = 0
- g.currentRow++
- if g.currentRow >= len(g.panes) {
- g.currentRow = 0
- }
- }
-
- // Focus next pane
- if g.currentRow < len(g.panes) && g.currentColumn < len(g.panes[g.currentRow]) {
- if nextPane, ok := g.panes[g.currentRow][g.currentColumn].(Focusable); ok {
- cmds = append(cmds, nextPane.Focus())
- }
- }
-
- return g, tea.Batch(cmds...)
-}
-
-func (g *gridLayout) nextPaneBinding() key.Binding {
- return key.NewBinding(
- key.WithKeys("tab"),
- key.WithHelp("tab", "next pane"),
- )
-}
-
-func (g *gridLayout) View() string {
- if len(g.panes) == 0 {
- return ""
- }
-
- // Calculate dimensions for each cell
- cellWidth := (g.width - (g.columns-1)*g.gap) / g.columns
- cellHeight := (g.height - (g.rows-1)*g.gap) / g.rows
-
- // Render each row
- rows := make([]string, g.rows)
- for i := range g.rows {
- // Render each column in this row
- cols := make([]string, len(g.panes[i]))
- for j := range g.panes[i] {
- if g.panes[i][j] == nil {
- cols[j] = ""
- continue
- }
-
- // Set size for each pane
- if sizable, ok := g.panes[i][j].(Sizeable); ok {
- effectiveWidth, effectiveHeight := cellWidth, cellHeight
- if g.bordered {
- effectiveWidth -= 2
- effectiveHeight -= 2
- }
- sizable.SetSize(effectiveWidth, effectiveHeight)
- }
-
- // Render the pane
- content := g.panes[i][j].View()
-
- // Apply border if needed
- if g.bordered {
- isFocused := false
- if focusable, ok := g.panes[i][j].(Focusable); ok {
- isFocused = focusable.IsFocused()
- }
-
- borderText := map[BorderPosition]string{}
- if bordered, ok := g.panes[i][j].(Bordered); ok {
- borderText = bordered.BorderText()
- }
-
- content = Borderize(content, BorderOptions{
- Active: isFocused,
- EmbeddedText: borderText,
- })
- }
-
- cols[j] = content
- }
-
- // Join columns with gap
- rows[i] = lipgloss.JoinHorizontal(lipgloss.Top, cols...)
- }
-
- // Join rows with gap
- return lipgloss.JoinVertical(lipgloss.Left, rows...)
-}
-
-func (g *gridLayout) SetSize(width, height int) {
- g.width = width
- g.height = height
-}
-
-func (g *gridLayout) GetSize() (int, int) {
- return g.width, g.height
-}
-
-func (g *gridLayout) BindingKeys() []key.Binding {
- var bindings []key.Binding
- bindings = append(bindings, g.nextPaneBinding())
-
- // Collect bindings from all panes
- for i := range g.panes {
- for j := range g.panes[i] {
- if g.panes[i][j] != nil {
- if bindable, ok := g.panes[i][j].(Bindings); ok {
- bindings = append(bindings, bindable.BindingKeys()...)
- }
- }
- }
- }
-
- return bindings
-}
-
-func (g *gridLayout) Panes() [][]tea.Model {
- return g.panes
-}
-
-// NewGridLayout creates a new grid layout with the given number of rows and columns
-func NewGridLayout(rows, cols int, panes [][]tea.Model, opts ...GridOption) GridLayout {
- grid := &gridLayout{
- rows: rows,
- columns: cols,
- panes: panes,
- gap: 1,
- }
-
- for _, opt := range opts {
- opt(grid)
- }
-
- return grid
-}
-
-// WithGridGap sets the gap between cells
-func WithGridGap(gap int) GridOption {
- return func(g *gridLayout) {
- g.gap = gap
- }
-}
-
-// WithGridBordered sets whether cells should have borders
-func WithGridBordered(bordered bool) GridOption {
- return func(g *gridLayout) {
- g.bordered = bordered
- }
-}
-
-// WithGridFocusable sets whether the grid supports focus navigation
-func WithGridFocusable(focusable bool) GridOption {
- return func(g *gridLayout) {
- g.focusable = focusable
- }
-}
-
-// WithGridActiveColor sets the active border color
-func WithGridActiveColor(color lipgloss.TerminalColor) GridOption {
- return func(g *gridLayout) {
- g.activeColor = color
- }
-}
diff --git a/internal/tui/layout/layout.go b/internal/tui/layout/layout.go
index 2f17c4a0e..495a3fbc5 100644
--- a/internal/tui/layout/layout.go
+++ b/internal/tui/layout/layout.go
@@ -13,12 +13,8 @@ type Focusable interface {
IsFocused() bool
}
-type Bordered interface {
- BorderText() map[BorderPosition]string
-}
-
type Sizeable interface {
- SetSize(width, height int)
+ SetSize(width, height int) tea.Cmd
GetSize() (int, int)
}
diff --git a/internal/tui/layout/single.go b/internal/tui/layout/single.go
deleted file mode 100644
index c77fa0d78..000000000
--- a/internal/tui/layout/single.go
+++ /dev/null
@@ -1,189 +0,0 @@
-package layout
-
-import (
- "github.com/charmbracelet/bubbles/key"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
-)
-
-type SinglePaneLayout interface {
- tea.Model
- Focusable
- Sizeable
- Bindings
- Pane() tea.Model
-}
-
-type singlePaneLayout struct {
- width int
- height int
-
- focusable bool
- focused bool
-
- bordered bool
- borderText map[BorderPosition]string
-
- content tea.Model
-
- padding []int
-
- activeColor lipgloss.TerminalColor
-}
-
-type SinglePaneOption func(*singlePaneLayout)
-
-func (s *singlePaneLayout) Init() tea.Cmd {
- return s.content.Init()
-}
-
-func (s *singlePaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- s.SetSize(msg.Width, msg.Height)
- return s, nil
- }
- u, cmd := s.content.Update(msg)
- s.content = u
- return s, cmd
-}
-
-func (s *singlePaneLayout) View() string {
- style := lipgloss.NewStyle().Width(s.width).Height(s.height)
- if s.bordered {
- style = style.Width(s.width - 2).Height(s.height - 2)
- }
- if s.padding != nil {
- style = style.Padding(s.padding...)
- }
- content := style.Render(s.content.View())
- if s.bordered {
- if s.borderText == nil {
- s.borderText = map[BorderPosition]string{}
- }
- if bordered, ok := s.content.(Bordered); ok {
- s.borderText = bordered.BorderText()
- }
- return Borderize(content, BorderOptions{
- Active: s.focused,
- EmbeddedText: s.borderText,
- })
- }
- return content
-}
-
-func (s *singlePaneLayout) Blur() tea.Cmd {
- if s.focusable {
- s.focused = false
- }
- if blurable, ok := s.content.(Focusable); ok {
- return blurable.Blur()
- }
- return nil
-}
-
-func (s *singlePaneLayout) Focus() tea.Cmd {
- if s.focusable {
- s.focused = true
- }
- if focusable, ok := s.content.(Focusable); ok {
- return focusable.Focus()
- }
- return nil
-}
-
-func (s *singlePaneLayout) SetSize(width, height int) {
- s.width = width
- s.height = height
- childWidth, childHeight := s.width, s.height
- if s.bordered {
- childWidth -= 2
- childHeight -= 2
- }
- if s.padding != nil {
- if len(s.padding) == 1 {
- childWidth -= s.padding[0] * 2
- childHeight -= s.padding[0] * 2
- } else if len(s.padding) == 2 {
- childWidth -= s.padding[0] * 2
- childHeight -= s.padding[1] * 2
- } else if len(s.padding) == 3 {
- childWidth -= s.padding[0] * 2
- childHeight -= s.padding[1] + s.padding[2]
- } else if len(s.padding) == 4 {
- childWidth -= s.padding[0] + s.padding[2]
- childHeight -= s.padding[1] + s.padding[3]
- }
- }
- if s.content != nil {
- if c, ok := s.content.(Sizeable); ok {
- c.SetSize(childWidth, childHeight)
- }
- }
-}
-
-func (s *singlePaneLayout) IsFocused() bool {
- return s.focused
-}
-
-func (s *singlePaneLayout) GetSize() (int, int) {
- return s.width, s.height
-}
-
-func (s *singlePaneLayout) BindingKeys() []key.Binding {
- if b, ok := s.content.(Bindings); ok {
- return b.BindingKeys()
- }
- return []key.Binding{}
-}
-
-func (s *singlePaneLayout) Pane() tea.Model {
- return s.content
-}
-
-func NewSinglePane(content tea.Model, opts ...SinglePaneOption) SinglePaneLayout {
- layout := &singlePaneLayout{
- content: content,
- }
- for _, opt := range opts {
- opt(layout)
- }
- return layout
-}
-
-func WithSinglePaneSize(width, height int) SinglePaneOption {
- return func(opts *singlePaneLayout) {
- opts.width = width
- opts.height = height
- }
-}
-
-func WithSinglePaneFocusable(focusable bool) SinglePaneOption {
- return func(opts *singlePaneLayout) {
- opts.focusable = focusable
- }
-}
-
-func WithSinglePaneBordered(bordered bool) SinglePaneOption {
- return func(opts *singlePaneLayout) {
- opts.bordered = bordered
- }
-}
-
-func WithSinglePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption {
- return func(opts *singlePaneLayout) {
- opts.borderText = borderText
- }
-}
-
-func WithSinglePanePadding(padding ...int) SinglePaneOption {
- return func(opts *singlePaneLayout) {
- opts.padding = padding
- }
-}
-
-func WithSinglePaneActiveColor(color lipgloss.TerminalColor) SinglePaneOption {
- return func(opts *singlePaneLayout) {
- opts.activeColor = color
- }
-}
diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go
index bfb616a53..a41df6ab8 100644
--- a/internal/tui/layout/split.go
+++ b/internal/tui/layout/split.go
@@ -11,9 +11,9 @@ type SplitPaneLayout interface {
tea.Model
Sizeable
Bindings
- SetLeftPanel(panel Container)
- SetRightPanel(panel Container)
- SetBottomPanel(panel Container)
+ SetLeftPanel(panel Container) tea.Cmd
+ SetRightPanel(panel Container) tea.Cmd
+ SetBottomPanel(panel Container) tea.Cmd
}
type splitPaneLayout struct {
@@ -53,8 +53,7 @@ func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
- s.SetSize(msg.Width, msg.Height)
- return s, nil
+ return s, s.SetSize(msg.Width, msg.Height)
}
if s.rightPanel != nil {
@@ -122,7 +121,7 @@ func (s *splitPaneLayout) View() string {
return finalView
}
-func (s *splitPaneLayout) SetSize(width, height int) {
+func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
s.width = width
s.height = height
@@ -147,42 +146,50 @@ func (s *splitPaneLayout) SetSize(width, height int) {
rightWidth = width
}
+ var cmds []tea.Cmd
if s.leftPanel != nil {
- s.leftPanel.SetSize(leftWidth, topHeight)
+ cmd := s.leftPanel.SetSize(leftWidth, topHeight)
+ cmds = append(cmds, cmd)
}
if s.rightPanel != nil {
- s.rightPanel.SetSize(rightWidth, topHeight)
+ cmd := s.rightPanel.SetSize(rightWidth, topHeight)
+ cmds = append(cmds, cmd)
}
if s.bottomPanel != nil {
- s.bottomPanel.SetSize(width, bottomHeight)
+ cmd := s.bottomPanel.SetSize(width, bottomHeight)
+ cmds = append(cmds, cmd)
}
+ return tea.Batch(cmds...)
}
func (s *splitPaneLayout) GetSize() (int, int) {
return s.width, s.height
}
-func (s *splitPaneLayout) SetLeftPanel(panel Container) {
+func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
s.leftPanel = panel
if s.width > 0 && s.height > 0 {
- s.SetSize(s.width, s.height)
+ return s.SetSize(s.width, s.height)
}
+ return nil
}
-func (s *splitPaneLayout) SetRightPanel(panel Container) {
+func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
s.rightPanel = panel
if s.width > 0 && s.height > 0 {
- s.SetSize(s.width, s.height)
+ return s.SetSize(s.width, s.height)
}
+ return nil
}
-func (s *splitPaneLayout) SetBottomPanel(panel Container) {
+func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
s.bottomPanel = panel
if s.width > 0 && s.height > 0 {
- s.SetSize(s.width, s.height)
+ return s.SetSize(s.width, s.height)
}
+ return nil
}
func (s *splitPaneLayout) BindingKeys() []key.Binding {