summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/layout
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-02 16:08:06 -0500
committeradamdottv <[email protected]>2025-07-02 16:08:11 -0500
commitc82a060eca41b990b4dd89cecffb874b2133af6f (patch)
treec3bb84aee2e621da9feb18f866c88fc51da0e7c1 /packages/tui/internal/layout
parent63e783ef795d91c745733b945247e917f1683d31 (diff)
downloadopencode-c82a060eca41b990b4dd89cecffb874b2133af6f.tar.gz
opencode-c82a060eca41b990b4dd89cecffb874b2133af6f.zip
feat(tui): file viewer, select messages
Diffstat (limited to 'packages/tui/internal/layout')
-rw-r--r--packages/tui/internal/layout/flex.go123
-rw-r--r--packages/tui/internal/layout/flex_example_test.go41
-rw-r--r--packages/tui/internal/layout/flex_test.go90
3 files changed, 228 insertions, 26 deletions
diff --git a/packages/tui/internal/layout/flex.go b/packages/tui/internal/layout/flex.go
index c7d9ee1bc..5b10a9523 100644
--- a/packages/tui/internal/layout/flex.go
+++ b/packages/tui/internal/layout/flex.go
@@ -4,7 +4,9 @@ import (
"strings"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/styles"
+ "github.com/sst/opencode/internal/theme"
)
type Direction int
@@ -34,11 +36,13 @@ const (
)
type FlexOptions struct {
- Direction Direction
- Justify Justify
- Align Align
- Width int
- Height int
+ Background *compat.AdaptiveColor
+ Direction Direction
+ Justify Justify
+ Align Align
+ Width int
+ Height int
+ Gap int
}
type FlexItem struct {
@@ -53,6 +57,12 @@ func Render(opts FlexOptions, items ...FlexItem) string {
return ""
}
+ t := theme.CurrentTheme()
+ if opts.Background == nil {
+ background := t.Background()
+ opts.Background = &background
+ }
+
// Calculate dimensions for each item
mainAxisSize := opts.Width
crossAxisSize := opts.Height
@@ -72,8 +82,14 @@ func Render(opts FlexOptions, items ...FlexItem) string {
}
}
+ // Account for gaps between items
+ totalGapSize := 0
+ if len(items) > 1 && opts.Gap > 0 {
+ totalGapSize = opts.Gap * (len(items) - 1)
+ }
+
// Calculate available space for grow items
- availableSpace := max(mainAxisSize-totalFixedSize, 0)
+ availableSpace := max(mainAxisSize-totalFixedSize-totalGapSize, 0)
// Calculate size for each grow item
growItemSize := 0
@@ -108,6 +124,7 @@ func Render(opts FlexOptions, items ...FlexItem) string {
// For row direction, constrain width and handle height alignment
if itemSize > 0 {
view = styles.NewStyle().
+ Background(*opts.Background).
Width(itemSize).
Height(crossAxisSize).
Render(view)
@@ -116,31 +133,65 @@ func Render(opts FlexOptions, items ...FlexItem) string {
// Apply cross-axis alignment
switch opts.Align {
case AlignCenter:
- view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Center, view)
+ view = lipgloss.PlaceVertical(
+ crossAxisSize,
+ lipgloss.Center,
+ view,
+ styles.WhitespaceStyle(*opts.Background),
+ )
case AlignEnd:
- view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Bottom, view)
+ view = lipgloss.PlaceVertical(
+ crossAxisSize,
+ lipgloss.Bottom,
+ view,
+ styles.WhitespaceStyle(*opts.Background),
+ )
case AlignStart:
- view = lipgloss.PlaceVertical(crossAxisSize, lipgloss.Top, view)
+ view = lipgloss.PlaceVertical(
+ crossAxisSize,
+ lipgloss.Top,
+ view,
+ styles.WhitespaceStyle(*opts.Background),
+ )
case AlignStretch:
// Already stretched by Height setting above
}
} else {
// For column direction, constrain height and handle width alignment
if itemSize > 0 {
- view = styles.NewStyle().
- Height(itemSize).
- Width(crossAxisSize).
- Render(view)
+ style := styles.NewStyle().
+ Background(*opts.Background).
+ Height(itemSize)
+ // Only set width for stretch alignment
+ if opts.Align == AlignStretch {
+ style = style.Width(crossAxisSize)
+ }
+ view = style.Render(view)
}
// Apply cross-axis alignment
switch opts.Align {
case AlignCenter:
- view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Center, view)
+ view = lipgloss.PlaceHorizontal(
+ crossAxisSize,
+ lipgloss.Center,
+ view,
+ styles.WhitespaceStyle(*opts.Background),
+ )
case AlignEnd:
- view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Right, view)
+ view = lipgloss.PlaceHorizontal(
+ crossAxisSize,
+ lipgloss.Right,
+ view,
+ styles.WhitespaceStyle(*opts.Background),
+ )
case AlignStart:
- view = lipgloss.PlaceHorizontal(crossAxisSize, lipgloss.Left, view)
+ view = lipgloss.PlaceHorizontal(
+ crossAxisSize,
+ lipgloss.Left,
+ view,
+ styles.WhitespaceStyle(*opts.Background),
+ )
case AlignStretch:
// Already stretched by Width setting above
}
@@ -154,11 +205,14 @@ func Render(opts FlexOptions, items ...FlexItem) string {
}
}
- // Calculate total actual size
+ // Calculate total actual size including gaps
totalActualSize := 0
for _, size := range actualSizes {
totalActualSize += size
}
+ if len(items) > 1 && opts.Gap > 0 {
+ totalActualSize += opts.Gap * (len(items) - 1)
+ }
// Apply justification
remainingSpace := max(mainAxisSize-totalActualSize, 0)
@@ -191,12 +245,17 @@ func Render(opts FlexOptions, items ...FlexItem) string {
// Build the final layout
var parts []string
+ spaceStyle := styles.NewStyle().Background(*opts.Background)
// Add space before if needed
if spaceBefore > 0 {
if opts.Direction == Row {
- parts = append(parts, strings.Repeat(" ", spaceBefore))
+ space := strings.Repeat(" ", spaceBefore)
+ parts = append(parts, spaceStyle.Render(space))
} else {
- parts = append(parts, strings.Repeat("\n", spaceBefore))
+ // For vertical layout, add empty lines as separate parts
+ for range spaceBefore {
+ parts = append(parts, "")
+ }
}
}
@@ -205,11 +264,19 @@ func Render(opts FlexOptions, items ...FlexItem) string {
parts = append(parts, view)
// Add space between items (not after the last one)
- if i < len(sizedViews)-1 && spaceBetween > 0 {
- if opts.Direction == Row {
- parts = append(parts, strings.Repeat(" ", spaceBetween))
- } else {
- parts = append(parts, strings.Repeat("\n", spaceBetween))
+ if i < len(sizedViews)-1 {
+ // Add gap first, then any additional spacing from justification
+ totalSpacing := opts.Gap + spaceBetween
+ if totalSpacing > 0 {
+ if opts.Direction == Row {
+ space := strings.Repeat(" ", totalSpacing)
+ parts = append(parts, spaceStyle.Render(space))
+ } else {
+ // For vertical layout, add empty lines as separate parts
+ for range totalSpacing {
+ parts = append(parts, "")
+ }
+ }
}
}
}
@@ -217,9 +284,13 @@ func Render(opts FlexOptions, items ...FlexItem) string {
// Add space after if needed
if spaceAfter > 0 {
if opts.Direction == Row {
- parts = append(parts, strings.Repeat(" ", spaceAfter))
+ space := strings.Repeat(" ", spaceAfter)
+ parts = append(parts, spaceStyle.Render(space))
} else {
- parts = append(parts, strings.Repeat("\n", spaceAfter))
+ // For vertical layout, add empty lines as separate parts
+ for range spaceAfter {
+ parts = append(parts, "")
+ }
}
}
diff --git a/packages/tui/internal/layout/flex_example_test.go b/packages/tui/internal/layout/flex_example_test.go
new file mode 100644
index 000000000..a03346eb7
--- /dev/null
+++ b/packages/tui/internal/layout/flex_example_test.go
@@ -0,0 +1,41 @@
+package layout_test
+
+import (
+ "fmt"
+ "github.com/sst/opencode/internal/layout"
+)
+
+func ExampleRender_withGap() {
+ // Create a horizontal layout with 3px gap between items
+ result := layout.Render(
+ layout.FlexOptions{
+ Direction: layout.Row,
+ Width: 30,
+ Height: 1,
+ Gap: 3,
+ },
+ layout.FlexItem{View: "Item1"},
+ layout.FlexItem{View: "Item2"},
+ layout.FlexItem{View: "Item3"},
+ )
+ fmt.Println(result)
+ // Output: Item1 Item2 Item3
+}
+
+func ExampleRender_withGapAndJustify() {
+ // Create a horizontal layout with gap and space-between justification
+ result := layout.Render(
+ layout.FlexOptions{
+ Direction: layout.Row,
+ Width: 30,
+ Height: 1,
+ Gap: 2,
+ Justify: layout.JustifySpaceBetween,
+ },
+ layout.FlexItem{View: "A"},
+ layout.FlexItem{View: "B"},
+ layout.FlexItem{View: "C"},
+ )
+ fmt.Println(result)
+ // Output: A B C
+}
diff --git a/packages/tui/internal/layout/flex_test.go b/packages/tui/internal/layout/flex_test.go
new file mode 100644
index 000000000..cad38dc8f
--- /dev/null
+++ b/packages/tui/internal/layout/flex_test.go
@@ -0,0 +1,90 @@
+package layout
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestFlexGap(t *testing.T) {
+ tests := []struct {
+ name string
+ opts FlexOptions
+ items []FlexItem
+ expected string
+ }{
+ {
+ name: "Row with gap",
+ opts: FlexOptions{
+ Direction: Row,
+ Width: 20,
+ Height: 1,
+ Gap: 2,
+ },
+ items: []FlexItem{
+ {View: "A"},
+ {View: "B"},
+ {View: "C"},
+ },
+ expected: "A B C",
+ },
+ {
+ name: "Column with gap",
+ opts: FlexOptions{
+ Direction: Column,
+ Width: 1,
+ Height: 5,
+ Gap: 1,
+ Align: AlignStart,
+ },
+ items: []FlexItem{
+ {View: "A", FixedSize: 1},
+ {View: "B", FixedSize: 1},
+ {View: "C", FixedSize: 1},
+ },
+ expected: "A\n \nB\n \nC",
+ },
+ {
+ name: "Row with gap and justify space between",
+ opts: FlexOptions{
+ Direction: Row,
+ Width: 15,
+ Height: 1,
+ Gap: 1,
+ Justify: JustifySpaceBetween,
+ },
+ items: []FlexItem{
+ {View: "A"},
+ {View: "B"},
+ {View: "C"},
+ },
+ expected: "A B C",
+ },
+ {
+ name: "No gap specified",
+ opts: FlexOptions{
+ Direction: Row,
+ Width: 10,
+ Height: 1,
+ },
+ items: []FlexItem{
+ {View: "A"},
+ {View: "B"},
+ {View: "C"},
+ },
+ expected: "ABC",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := Render(tt.opts, tt.items...)
+ // Trim any trailing spaces for comparison
+ result = strings.TrimRight(result, " ")
+ expected := strings.TrimRight(tt.expected, " ")
+
+ if result != expected {
+ t.Errorf("Render() = %q, want %q", result, expected)
+ }
+ })
+ }
+}