diff options
| author | adamdottv <[email protected]> | 2025-07-02 16:08:06 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-07-02 16:08:11 -0500 |
| commit | c82a060eca41b990b4dd89cecffb874b2133af6f (patch) | |
| tree | c3bb84aee2e621da9feb18f866c88fc51da0e7c1 /packages/tui/internal/layout | |
| parent | 63e783ef795d91c745733b945247e917f1683d31 (diff) | |
| download | opencode-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.go | 123 | ||||
| -rw-r--r-- | packages/tui/internal/layout/flex_example_test.go | 41 | ||||
| -rw-r--r-- | packages/tui/internal/layout/flex_test.go | 90 |
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) + } + }) + } +} |
