summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/layout
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-11-02 18:43:17 -0500
committerDax Raad <[email protected]>2025-11-02 18:43:33 -0500
commitf68374ad2223ddc213bdea9519ca6a699819ee0e (patch)
tree04f0fe21b8e12cd62d7274961bb0cff64f966f40 /packages/tui/internal/layout
parent5e86c9b7916f75c7ad227b80eab18c7c54fc8ffe (diff)
downloadopencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.tar.gz
opencode-f68374ad2223ddc213bdea9519ca6a699819ee0e.zip
DELETE GO BUBBLETEA CRAP HOORAY
Diffstat (limited to 'packages/tui/internal/layout')
-rw-r--r--packages/tui/internal/layout/flex.go325
-rw-r--r--packages/tui/internal/layout/layout.go32
-rw-r--r--packages/tui/internal/layout/overlay.go382
3 files changed, 0 insertions, 739 deletions
diff --git a/packages/tui/internal/layout/flex.go b/packages/tui/internal/layout/flex.go
deleted file mode 100644
index 5b10a9523..000000000
--- a/packages/tui/internal/layout/flex.go
+++ /dev/null
@@ -1,325 +0,0 @@
-package layout
-
-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
-
-const (
- Row Direction = iota
- Column
-)
-
-type Justify int
-
-const (
- JustifyStart Justify = iota
- JustifyEnd
- JustifyCenter
- JustifySpaceBetween
- JustifySpaceAround
-)
-
-type Align int
-
-const (
- AlignStart Align = iota
- AlignEnd
- AlignCenter
- AlignStretch // Only applicable in the cross-axis
-)
-
-type FlexOptions struct {
- Background *compat.AdaptiveColor
- Direction Direction
- Justify Justify
- Align Align
- Width int
- Height int
- Gap int
-}
-
-type FlexItem struct {
- View string
- FixedSize int // Fixed size in the main axis (width for Row, height for Column)
- Grow bool // If true, the item will grow to fill available space
-}
-
-// Render lays out a series of view strings based on flexbox-like rules.
-func Render(opts FlexOptions, items ...FlexItem) string {
- if len(items) == 0 {
- 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
- if opts.Direction == Column {
- mainAxisSize = opts.Height
- crossAxisSize = opts.Width
- }
-
- // Calculate total fixed size and count grow items
- totalFixedSize := 0
- growCount := 0
- for _, item := range items {
- if item.FixedSize > 0 {
- totalFixedSize += item.FixedSize
- } else if item.Grow {
- growCount++
- }
- }
-
- // 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-totalGapSize, 0)
-
- // Calculate size for each grow item
- growItemSize := 0
- if growCount > 0 && availableSpace > 0 {
- growItemSize = availableSpace / growCount
- }
-
- // Prepare sized views
- sizedViews := make([]string, len(items))
- actualSizes := make([]int, len(items))
-
- for i, item := range items {
- view := item.View
-
- // Determine the size for this item
- itemSize := 0
- if item.FixedSize > 0 {
- itemSize = item.FixedSize
- } else if item.Grow && growItemSize > 0 {
- itemSize = growItemSize
- } else {
- // No fixed size and not growing - use natural size
- if opts.Direction == Row {
- itemSize = lipgloss.Width(view)
- } else {
- itemSize = lipgloss.Height(view)
- }
- }
-
- // Apply size constraints
- if opts.Direction == Row {
- // For row direction, constrain width and handle height alignment
- if itemSize > 0 {
- view = styles.NewStyle().
- Background(*opts.Background).
- Width(itemSize).
- Height(crossAxisSize).
- Render(view)
- }
-
- // Apply cross-axis alignment
- switch opts.Align {
- case AlignCenter:
- view = lipgloss.PlaceVertical(
- crossAxisSize,
- lipgloss.Center,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignEnd:
- view = lipgloss.PlaceVertical(
- crossAxisSize,
- lipgloss.Bottom,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStart:
- 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 {
- 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,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignEnd:
- view = lipgloss.PlaceHorizontal(
- crossAxisSize,
- lipgloss.Right,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStart:
- view = lipgloss.PlaceHorizontal(
- crossAxisSize,
- lipgloss.Left,
- view,
- styles.WhitespaceStyle(*opts.Background),
- )
- case AlignStretch:
- // Already stretched by Width setting above
- }
- }
-
- sizedViews[i] = view
- if opts.Direction == Row {
- actualSizes[i] = lipgloss.Width(view)
- } else {
- actualSizes[i] = lipgloss.Height(view)
- }
- }
-
- // 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)
-
- // Calculate spacing based on justification
- var spaceBefore, spaceBetween, spaceAfter int
- switch opts.Justify {
- case JustifyStart:
- spaceAfter = remainingSpace
- case JustifyEnd:
- spaceBefore = remainingSpace
- case JustifyCenter:
- spaceBefore = remainingSpace / 2
- spaceAfter = remainingSpace - spaceBefore
- case JustifySpaceBetween:
- if len(items) > 1 {
- spaceBetween = remainingSpace / (len(items) - 1)
- } else {
- spaceAfter = remainingSpace
- }
- case JustifySpaceAround:
- if len(items) > 0 {
- spaceAround := remainingSpace / (len(items) * 2)
- spaceBefore = spaceAround
- spaceAfter = spaceAround
- spaceBetween = spaceAround * 2
- }
- }
-
- // 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 {
- space := strings.Repeat(" ", spaceBefore)
- parts = append(parts, spaceStyle.Render(space))
- } else {
- // For vertical layout, add empty lines as separate parts
- for range spaceBefore {
- parts = append(parts, "")
- }
- }
- }
-
- // Add items with spacing
- for i, view := range sizedViews {
- parts = append(parts, view)
-
- // Add space between items (not after the last one)
- 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, "")
- }
- }
- }
- }
- }
-
- // Add space after if needed
- if spaceAfter > 0 {
- if opts.Direction == Row {
- space := strings.Repeat(" ", spaceAfter)
- parts = append(parts, spaceStyle.Render(space))
- } else {
- // For vertical layout, add empty lines as separate parts
- for range spaceAfter {
- parts = append(parts, "")
- }
- }
- }
-
- // Join the parts
- if opts.Direction == Row {
- return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
- } else {
- return lipgloss.JoinVertical(lipgloss.Left, parts...)
- }
-}
-
-// Helper function to create a simple vertical layout
-func Vertical(width, height int, items ...FlexItem) string {
- return Render(FlexOptions{
- Direction: Column,
- Width: width,
- Height: height,
- Justify: JustifyStart,
- Align: AlignStretch,
- }, items...)
-}
-
-// Helper function to create a simple horizontal layout
-func Horizontal(width, height int, items ...FlexItem) string {
- return Render(FlexOptions{
- Direction: Row,
- Width: width,
- Height: height,
- Justify: JustifyStart,
- Align: AlignStretch,
- }, items...)
-}
diff --git a/packages/tui/internal/layout/layout.go b/packages/tui/internal/layout/layout.go
deleted file mode 100644
index dce27ac68..000000000
--- a/packages/tui/internal/layout/layout.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package layout
-
-import (
- tea "github.com/charmbracelet/bubbletea/v2"
-)
-
-var Current *LayoutInfo
-
-func init() {
- Current = &LayoutInfo{
- Viewport: Dimensions{Width: 80, Height: 25},
- Container: Dimensions{Width: 80, Height: 25},
- }
-}
-
-type LayoutSize string
-
-type Dimensions struct {
- Width int
- Height int
-}
-
-type LayoutInfo struct {
- Viewport Dimensions
- Container Dimensions
-}
-
-type Modal interface {
- tea.Model
- Render(background string) string
- Close() tea.Cmd
-}
diff --git a/packages/tui/internal/layout/overlay.go b/packages/tui/internal/layout/overlay.go
deleted file mode 100644
index 08016e31c..000000000
--- a/packages/tui/internal/layout/overlay.go
+++ /dev/null
@@ -1,382 +0,0 @@
-package layout
-
-import (
- "fmt"
- "regexp"
- "strings"
- "unicode/utf8"
-
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/charmbracelet/lipgloss/v2/compat"
- chAnsi "github.com/charmbracelet/x/ansi"
- "github.com/muesli/ansi"
- "github.com/muesli/reflow/truncate"
- "github.com/muesli/termenv"
- "github.com/sst/opencode/internal/util"
-)
-
-var (
- // ANSI escape sequence regex
- ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
-)
-
-// Split a string into lines, additionally returning the size of the widest line.
-func getLines(s string) (lines []string, widest int) {
- lines = strings.Split(s, "\n")
- for _, l := range lines {
- w := ansi.PrintableRuneWidth(l)
- if widest < w {
- widest = w
- }
- }
- return lines, widest
-}
-
-// overlayOptions holds configuration for overlay rendering
-type overlayOptions struct {
- whitespace *whitespace
- border bool
- borderColor *compat.AdaptiveColor
-}
-
-// OverlayOption sets options for overlay rendering
-type OverlayOption func(*overlayOptions)
-
-// PlaceOverlay places fg on top of bg.
-func PlaceOverlay(
- x, y int,
- fg, bg string,
- opts ...OverlayOption,
-) string {
- fgLines, fgWidth := getLines(fg)
- bgLines, bgWidth := getLines(bg)
- bgHeight := len(bgLines)
- fgHeight := len(fgLines)
-
- // Parse options
- options := &overlayOptions{
- whitespace: &whitespace{},
- }
- for _, opt := range opts {
- opt(options)
- }
-
- // Adjust for borders if enabled
- if options.border {
- // Add space for left and right borders
- adjustedFgWidth := fgWidth + 2
- // Adjust placement to account for borders
- x = util.Clamp(x, 0, bgWidth-adjustedFgWidth)
- y = util.Clamp(y, 0, bgHeight-fgHeight)
-
- // Pad all foreground lines to the same width for consistent borders
- for i := range fgLines {
- lineWidth := ansi.PrintableRuneWidth(fgLines[i])
- if lineWidth < fgWidth {
- fgLines[i] += strings.Repeat(" ", fgWidth-lineWidth)
- }
- }
- } else {
- if fgWidth >= bgWidth && fgHeight >= bgHeight {
- // FIXME: return fg or bg?
- return fg
- }
- // TODO: allow placement outside of the bg box?
- x = util.Clamp(x, 0, bgWidth-fgWidth)
- y = util.Clamp(y, 0, bgHeight-fgHeight)
- }
-
- var b strings.Builder
- for i, bgLine := range bgLines {
- if i > 0 {
- b.WriteByte('\n')
- }
- if i < y || i >= y+fgHeight {
- b.WriteString(bgLine)
- continue
- }
-
- pos := 0
-
- // Handle left side of the line up to the overlay
- if x > 0 {
- left := truncate.String(bgLine, uint(x))
- pos = ansi.PrintableRuneWidth(left)
- b.WriteString(left)
- if pos < x {
- b.WriteString(options.whitespace.render(x - pos))
- pos = x
- }
- }
-
- // Render the overlay content with optional borders
- if options.border {
- // Get the foreground line
- fgLine := fgLines[i-y]
- fgLineWidth := ansi.PrintableRuneWidth(fgLine)
-
- // Extract the styles at the border positions
- // We need to get the style just before the border position to preserve background
- leftStyle := ansiStyle{}
- if pos > 0 {
- leftStyle = getStyleAtPosition(bgLine, pos-1)
- } else {
- leftStyle = getStyleAtPosition(bgLine, pos)
- }
- rightStyle := getStyleAtPosition(bgLine, pos+fgLineWidth)
-
- // Left border - combine background from original with border foreground
- leftSeq := combineStyles(leftStyle, options.borderColor)
- if leftSeq != "" {
- b.WriteString(leftSeq)
- }
- b.WriteString("┃")
- if leftSeq != "" {
- b.WriteString("\x1b[0m") // Reset all styles only if we applied any
- }
- pos++
-
- // Content
- b.WriteString(fgLine)
- pos += fgLineWidth
-
- // Right border - combine background from original with border foreground
- rightSeq := combineStyles(rightStyle, options.borderColor)
- if rightSeq != "" {
- b.WriteString(rightSeq)
- }
- b.WriteString("┃")
- if rightSeq != "" {
- b.WriteString("\x1b[0m") // Reset all styles only if we applied any
- }
- pos++
- } else {
- // No border, just render the content
- fgLine := fgLines[i-y]
- b.WriteString(fgLine)
- pos += ansi.PrintableRuneWidth(fgLine)
- }
-
- // Handle right side of the line after the overlay
- right := cutLeft(bgLine, pos)
- bgWidth := ansi.PrintableRuneWidth(bgLine)
- rightWidth := ansi.PrintableRuneWidth(right)
- if rightWidth <= bgWidth-pos {
- b.WriteString(options.whitespace.render(bgWidth - rightWidth - pos))
- }
-
- b.WriteString(right)
- }
-
- return b.String()
-}
-
-// cutLeft cuts printable characters from the left.
-// This function is heavily based on muesli's ansi and truncate packages.
-func cutLeft(s string, cutWidth int) string {
- return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
-}
-
-// ansiStyle represents parsed ANSI style attributes
-type ansiStyle struct {
- fgColor string
- bgColor string
- attrs []string
-}
-
-// parseANSISequence parses an ANSI escape sequence into its components
-func parseANSISequence(seq string) ansiStyle {
- style := ansiStyle{}
-
- // Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
- if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
- return style
- }
-
- params := seq[2 : len(seq)-1]
- if params == "" {
- return style
- }
-
- parts := strings.Split(params, ";")
- i := 0
- for i < len(parts) {
- switch parts[i] {
- case "0": // Reset
- // Mark this as a reset by adding it to attrs
- style.attrs = append(style.attrs, "0")
- // Don't clear the style here, let the caller handle it
- case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
- style.attrs = append(style.attrs, parts[i])
- case "38": // Foreground color
- if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
- // 256 color mode
- style.fgColor = strings.Join(parts[i:i+3], ";")
- i += 2
- } else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
- // RGB color mode
- style.fgColor = strings.Join(parts[i:i+5], ";")
- i += 4
- }
- case "48": // Background color
- if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
- // 256 color mode
- style.bgColor = strings.Join(parts[i:i+3], ";")
- i += 2
- } else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
- // RGB color mode
- style.bgColor = strings.Join(parts[i:i+5], ";")
- i += 4
- }
- case "30", "31", "32", "33", "34", "35", "36", "37": // Standard foreground colors
- style.fgColor = parts[i]
- case "40", "41", "42", "43", "44", "45", "46", "47": // Standard background colors
- style.bgColor = parts[i]
- case "90", "91", "92", "93", "94", "95", "96", "97": // Bright foreground colors
- style.fgColor = parts[i]
- case "100", "101", "102", "103", "104", "105", "106", "107": // Bright background colors
- style.bgColor = parts[i]
- }
- i++
- }
-
- return style
-}
-
-// combineStyles creates an ANSI sequence that combines background from one style with foreground from another
-func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
- if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
- return ""
- }
-
- var parts []string
-
- // Add attributes
- parts = append(parts, bgStyle.attrs...)
-
- // Add background color from the original style
- if bgStyle.bgColor != "" {
- parts = append(parts, bgStyle.bgColor)
- }
-
- // Add foreground color if specified
- if fgColor != nil {
- // Use the adaptive color which automatically selects based on terminal background
- // The RGBA method already handles light/dark selection
- r, g, b, _ := fgColor.RGBA()
- // RGBA returns 16-bit values, we need 8-bit
- parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
- }
-
- if len(parts) == 0 {
- return ""
- }
-
- return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
-}
-
-// getStyleAtPosition extracts the active ANSI style at a given visual position
-func getStyleAtPosition(s string, targetPos int) ansiStyle {
- visualPos := 0
- currentStyle := ansiStyle{}
-
- i := 0
- for i < len(s) && visualPos <= targetPos {
- // Check if we're at an ANSI escape sequence
- if match := ansiRegex.FindStringIndex(s[i:]); match != nil && match[0] == 0 {
- // Found an ANSI sequence at current position
- seq := s[i : i+match[1]]
- parsedStyle := parseANSISequence(seq)
-
- // Check if this is a reset sequence
- if len(parsedStyle.attrs) > 0 && parsedStyle.attrs[0] == "0" {
- // Reset all styles
- currentStyle = ansiStyle{}
- } else {
- // Update current style (merge with existing)
- if parsedStyle.fgColor != "" {
- currentStyle.fgColor = parsedStyle.fgColor
- }
- if parsedStyle.bgColor != "" {
- currentStyle.bgColor = parsedStyle.bgColor
- }
- if len(parsedStyle.attrs) > 0 {
- currentStyle.attrs = parsedStyle.attrs
- }
- }
-
- i += match[1]
- } else if i < len(s) {
- // Regular character
- if visualPos == targetPos {
- return currentStyle
- }
- _, size := utf8.DecodeRuneInString(s[i:])
- i += size
- visualPos++
- }
- }
-
- return currentStyle
-}
-
-type whitespace struct {
- style termenv.Style
- chars string
-}
-
-// Render whitespaces.
-func (w whitespace) render(width int) string {
- if w.chars == "" {
- w.chars = " "
- }
-
- r := []rune(w.chars)
- j := 0
- b := strings.Builder{}
-
- // Cycle through runes and print them into the whitespace.
- for i := 0; i < width; {
- b.WriteRune(r[j])
- j++
- if j >= len(r) {
- j = 0
- }
- i += ansi.PrintableRuneWidth(string(r[j]))
- }
-
- // Fill any extra gaps white spaces. This might be necessary if any runes
- // are more than one cell wide, which could leave a one-rune gap.
- short := width - ansi.PrintableRuneWidth(b.String())
- if short > 0 {
- b.WriteString(strings.Repeat(" ", short))
- }
-
- return w.style.Styled(b.String())
-}
-
-// WhitespaceOption sets a styling rule for rendering whitespace.
-type WhitespaceOption func(*whitespace)
-
-// WithWhitespace sets whitespace options for the overlay
-func WithWhitespace(opts ...WhitespaceOption) OverlayOption {
- return func(o *overlayOptions) {
- for _, opt := range opts {
- opt(o.whitespace)
- }
- }
-}
-
-// WithOverlayBorder enables border rendering for the overlay
-func WithOverlayBorder() OverlayOption {
- return func(o *overlayOptions) {
- o.border = true
- }
-}
-
-// WithOverlayBorderColor sets the border color for the overlay
-func WithOverlayBorderColor(color compat.AdaptiveColor) OverlayOption {
- return func(o *overlayOptions) {
- o.borderColor = &color
- }
-}