diff options
| author | Dax Raad <[email protected]> | 2025-11-02 18:43:17 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-11-02 18:43:33 -0500 |
| commit | f68374ad2223ddc213bdea9519ca6a699819ee0e (patch) | |
| tree | 04f0fe21b8e12cd62d7274961bb0cff64f966f40 /packages/tui/internal/layout | |
| parent | 5e86c9b7916f75c7ad227b80eab18c7c54fc8ffe (diff) | |
| download | opencode-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.go | 325 | ||||
| -rw-r--r-- | packages/tui/internal/layout/layout.go | 32 | ||||
| -rw-r--r-- | packages/tui/internal/layout/overlay.go | 382 |
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 - } -} |
