summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-13 09:19:51 -0500
committeradamdottv <[email protected]>2025-06-13 09:19:51 -0500
commit38667682a7c89145e81ad12860f51ac9f554f87e (patch)
treed0340d52853a2035191833b2bb531e7137fc55c5
parentd7d5fc39fb6a4a1656664f471b064118d3a14d79 (diff)
downloadopencode-38667682a7c89145e81ad12860f51ac9f554f87e.tar.gz
opencode-38667682a7c89145e81ad12860f51ac9f554f87e.zip
wip: refactoring tui
-rw-r--r--packages/tui/internal/components/chat/message.go89
-rw-r--r--packages/tui/internal/components/chat/messages.go2
-rw-r--r--packages/tui/internal/components/diff/diff.go212
-rw-r--r--packages/tui/internal/layout/layout.go8
-rw-r--r--packages/tui/internal/tui/tui.go10
5 files changed, 244 insertions, 77 deletions
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 82e749afe..11a291235 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -55,6 +55,10 @@ type blockRenderer struct {
fullWidth bool
paddingTop int
paddingBottom int
+ paddingLeft int
+ paddingRight int
+ marginTop int
+ marginBottom int
}
type renderingOption func(*blockRenderer)
@@ -77,6 +81,30 @@ func WithBorderColor(color compat.AdaptiveColor) renderingOption {
}
}
+func WithMarginTop(padding int) renderingOption {
+ return func(c *blockRenderer) {
+ c.marginTop = padding
+ }
+}
+
+func WithMarginBottom(padding int) renderingOption {
+ return func(c *blockRenderer) {
+ c.marginBottom = padding
+ }
+}
+
+func WithPaddingLeft(padding int) renderingOption {
+ return func(c *blockRenderer) {
+ c.paddingLeft = padding
+ }
+}
+
+func WithPaddingRight(padding int) renderingOption {
+ return func(c *blockRenderer) {
+ c.paddingRight = padding
+ }
+}
+
func WithPaddingTop(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingTop = padding
@@ -92,17 +120,23 @@ func WithPaddingBottom(padding int) renderingOption {
func renderContentBlock(content string, options ...renderingOption) string {
t := theme.CurrentTheme()
renderer := &blockRenderer{
- fullWidth: false,
+ fullWidth: false,
+ paddingTop: 1,
+ paddingBottom: 1,
+ paddingLeft: 2,
+ paddingRight: 2,
}
for _, option := range options {
option(renderer)
}
style := styles.BaseStyle().
- PaddingTop(1).
- PaddingBottom(1).
- PaddingLeft(2).
- PaddingRight(2).
+ MarginTop(renderer.marginTop).
+ MarginBottom(renderer.marginBottom).
+ PaddingTop(renderer.paddingTop).
+ PaddingBottom(renderer.paddingBottom).
+ PaddingLeft(renderer.paddingLeft).
+ PaddingRight(renderer.paddingRight).
Background(t.BackgroundSubtle()).
Foreground(t.TextMuted()).
BorderStyle(lipgloss.ThickBorder())
@@ -142,12 +176,6 @@ func renderContentBlock(content string, options ...renderingOption) string {
style = style.Width(layout.Current.Container.Width)
}
content = style.Render(content)
- if renderer.paddingTop > 0 {
- content = strings.Repeat("\n", renderer.paddingTop) + content
- }
- if renderer.paddingBottom > 0 {
- content = content + strings.Repeat("\n", renderer.paddingBottom)
- }
content = lipgloss.PlaceHorizontal(
layout.Current.Container.Width,
align,
@@ -165,12 +193,11 @@ func renderText(message client.MessageInfo, text string, author string) string {
t := theme.CurrentTheme()
width := layout.Current.Container.Width
padding := 0
- switch layout.Current.Size {
- case layout.LayoutSizeSmall:
+ if layout.Current.Viewport.Width < 80 {
padding = 5
- case layout.LayoutSizeNormal:
+ } else if layout.Current.Viewport.Width < 120 {
padding = 10
- case layout.LayoutSizeLarge:
+ } else {
padding = 15
}
@@ -270,7 +297,7 @@ func renderToolInvocation(
error = styles.BaseStyle().
Foreground(t.Error()).
Render(m.(string))
- error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
}
}
@@ -301,8 +328,24 @@ func renderToolInvocation(
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
if d, ok := metadata.Get("diff"); ok {
patch := d.(string)
- diffWidth := min(layout.Current.Viewport.Width, 120)
- formattedDiff, _ := diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
+ var formattedDiff string
+ if layout.Current.Viewport.Width < 80 {
+ formattedDiff, _ = diff.FormatUnifiedDiff(
+ filename,
+ patch,
+ diff.WithWidth(layout.Current.Container.Width-2),
+ )
+ } else {
+ diffWidth := min(layout.Current.Viewport.Width, 120)
+ formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
+ }
+ formattedDiff = strings.TrimSpace(formattedDiff)
+ formattedDiff = lipgloss.NewStyle().
+ BorderStyle(lipgloss.ThickBorder()).
+ BorderForeground(t.BackgroundSubtle()).
+ BorderLeft(true).
+ BorderRight(true).
+ Render(formattedDiff)
body = strings.TrimSpace(formattedDiff)
body = lipgloss.Place(
layout.Current.Viewport.Width,
@@ -326,7 +369,7 @@ func renderToolInvocation(
stdout := stdout.(string)
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
- body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
}
case "opencode_webfetch":
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
@@ -335,7 +378,7 @@ func renderToolInvocation(
if format == "html" || format == "markdown" {
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
}
- body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
case "opencode_todowrite":
title = fmt.Sprintf("Planning... %s", elapsed)
@@ -355,13 +398,13 @@ func renderToolInvocation(
}
}
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
- body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
}
default:
toolName := renderToolName(toolCall.ToolName)
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
body = truncateHeight(body, 10)
- body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ body = renderContentBlock(body, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
}
content := style.Render(title)
@@ -435,7 +478,7 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
content = toMarkdown(content, width, t.BackgroundSubtle())
- return renderContentBlock(content, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ return renderContentBlock(content, WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
}
func renderToolAction(name string) string {
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 81f2d2a38..4beaea2d8 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -214,7 +214,7 @@ func (m *messagesComponent) renderView() {
case client.UnknownError:
clientError := errorValue.(client.UnknownError)
error = clientError.Data.Message
- error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+ error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
blocks = append(blocks, error)
previousBlockType = errorBlock
}
diff --git a/packages/tui/internal/components/diff/diff.go b/packages/tui/internal/components/diff/diff.go
index 4551cf736..2f655554e 100644
--- a/packages/tui/internal/components/diff/diff.go
+++ b/packages/tui/internal/components/diff/diff.go
@@ -105,6 +105,40 @@ func WithTotalWidth(width int) SideBySideOption {
}
// -------------------------------------------------------------------------
+// Unified Configuration
+// -------------------------------------------------------------------------
+
+// UnifiedConfig configures the rendering of unified diffs
+type UnifiedConfig struct {
+ Width int
+}
+
+// UnifiedOption modifies a UnifiedConfig
+type UnifiedOption func(*UnifiedConfig)
+
+// NewUnifiedConfig creates a UnifiedConfig with default values
+func NewUnifiedConfig(opts ...UnifiedOption) UnifiedConfig {
+ config := UnifiedConfig{
+ Width: 80, // Default width for unified view
+ }
+
+ for _, opt := range opts {
+ opt(&config)
+ }
+
+ return config
+}
+
+// WithWidth sets the width for unified view
+func WithWidth(width int) UnifiedOption {
+ return func(u *UnifiedConfig) {
+ if width > 0 {
+ u.Width = width
+ }
+ }
+}
+
+// -------------------------------------------------------------------------
// Diff Parsing
// -------------------------------------------------------------------------
@@ -642,6 +676,101 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
return sb.String()
}
+// renderLinePrefix renders the line number and marker prefix for a diff line
+func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string {
+ // Style the marker based on line type
+ var styledMarker string
+ switch dl.Kind {
+ case LineRemoved:
+ styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker)
+ case LineAdded:
+ styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
+ case LineContext:
+ styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker)
+ default:
+ styledMarker = marker
+ }
+
+ return lineNumberStyle.Render(lineNum + " " + styledMarker)
+}
+
+// renderLineContent renders the content of a diff line with syntax and intra-line highlighting
+func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string {
+ // Apply syntax highlighting
+ content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
+
+ // Apply intra-line highlighting if needed
+ if len(dl.Segments) > 0 && (dl.Kind == LineRemoved || dl.Kind == LineAdded) {
+ content = applyHighlighting(content, dl.Segments, dl.Kind, highlightColor)
+ }
+
+ // Add a padding space for added/removed lines
+ if dl.Kind == LineRemoved || dl.Kind == LineAdded {
+ content = bgStyle.Render(" ") + content
+ }
+
+ // Create the final line and truncate if needed
+ return bgStyle.MaxHeight(1).Width(width).Render(
+ ansi.Truncate(
+ content,
+ width,
+ lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
+ ),
+ )
+}
+
+// renderUnifiedLine renders a single line in unified diff format
+func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) string {
+ removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
+
+ // Determine line style and marker based on line type
+ var marker string
+ var bgStyle lipgloss.Style
+ var lineNum string
+ var highlightColor compat.AdaptiveColor
+
+ switch dl.Kind {
+ case LineRemoved:
+ marker = "-"
+ bgStyle = removedLineStyle
+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
+ highlightColor = t.DiffHighlightRemoved()
+ if dl.OldLineNo > 0 {
+ lineNum = fmt.Sprintf("%6d ", dl.OldLineNo)
+ } else {
+ lineNum = " "
+ }
+ case LineAdded:
+ marker = "+"
+ bgStyle = addedLineStyle
+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
+ highlightColor = t.DiffHighlightAdded()
+ if dl.NewLineNo > 0 {
+ lineNum = fmt.Sprintf(" %7d", dl.NewLineNo)
+ } else {
+ lineNum = " "
+ }
+ case LineContext:
+ marker = " "
+ bgStyle = contextLineStyle
+ if dl.OldLineNo > 0 && dl.NewLineNo > 0 {
+ lineNum = fmt.Sprintf("%6d %6d", dl.OldLineNo, dl.NewLineNo)
+ } else {
+ lineNum = " "
+ }
+ }
+
+ // Create the line prefix
+ prefix := renderLinePrefix(dl, lineNum, marker, lineNumberStyle, t)
+
+ // Render the content
+ prefixWidth := ansi.StringWidth(prefix)
+ contentWidth := width - prefixWidth
+ content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
+
+ return prefix + content
+}
+
// renderDiffColumnLine is a helper function that handles the common logic for rendering diff columns
func renderDiffColumnLine(
fileName string,
@@ -661,7 +790,6 @@ func renderDiffColumnLine(
var marker string
var bgStyle lipgloss.Style
var lineNum string
- var highlightType LineType
var highlightColor compat.AdaptiveColor
if isLeftColumn {
@@ -671,7 +799,6 @@ func renderDiffColumnLine(
marker = "-"
bgStyle = removedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
- highlightType = LineRemoved
highlightColor = t.DiffHighlightRemoved()
case LineAdded:
marker = "?"
@@ -692,7 +819,6 @@ func renderDiffColumnLine(
marker = "+"
bgStyle = addedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
- highlightType = LineAdded
highlightColor = t.DiffHighlightAdded()
case LineRemoved:
marker = "?"
@@ -708,44 +834,24 @@ func renderDiffColumnLine(
}
}
- // Style the marker based on line type
- var styledMarker string
- switch dl.Kind {
- case LineRemoved:
- styledMarker = removedLineStyle.Foreground(t.DiffRemoved()).Render(marker)
- case LineAdded:
- styledMarker = addedLineStyle.Foreground(t.DiffAdded()).Render(marker)
- case LineContext:
- styledMarker = contextLineStyle.Foreground(t.TextMuted()).Render(marker)
- default:
- styledMarker = marker
- }
-
// Create the line prefix
- prefix := lineNumberStyle.Render(lineNum + " " + styledMarker)
+ prefix := renderLinePrefix(*dl, lineNum, marker, lineNumberStyle, t)
- // Apply syntax highlighting
- content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
+ // Determine if we should render content
+ shouldRenderContent := (dl.Kind == LineRemoved && isLeftColumn) ||
+ (dl.Kind == LineAdded && !isLeftColumn) ||
+ dl.Kind == LineContext
- // Apply intra-line highlighting if needed
- if (dl.Kind == LineRemoved && isLeftColumn || dl.Kind == LineAdded && !isLeftColumn) && len(dl.Segments) > 0 {
- content = applyHighlighting(content, dl.Segments, highlightType, highlightColor)
+ if !shouldRenderContent {
+ return bgStyle.Width(colWidth).Render("")
}
- // Add a padding space for added/removed lines
- if (dl.Kind == LineRemoved && isLeftColumn) || (dl.Kind == LineAdded && !isLeftColumn) {
- content = bgStyle.Render(" ") + content
- }
+ // Render the content
+ prefixWidth := ansi.StringWidth(prefix)
+ contentWidth := colWidth - prefixWidth
+ content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
- // Create the final line and truncate if needed
- lineText := prefix + content
- return bgStyle.MaxHeight(1).Width(colWidth).Render(
- ansi.Truncate(
- lineText,
- colWidth,
- lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
- ),
- )
+ return prefix + content
}
// renderLeftColumn formats the left side of a side-by-side diff
@@ -762,6 +868,27 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
// Public API
// -------------------------------------------------------------------------
+// RenderUnifiedHunk formats a hunk for unified display
+func RenderUnifiedHunk(fileName string, h Hunk, opts ...UnifiedOption) string {
+ // Apply options to create the configuration
+ config := NewUnifiedConfig(opts...)
+
+ // Make a copy of the hunk so we don't modify the original
+ hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
+ copy(hunkCopy.Lines, h.Lines)
+
+ // Highlight changes within lines
+ HighlightIntralineChanges(&hunkCopy)
+
+ var sb strings.Builder
+ for _, line := range hunkCopy.Lines {
+ sb.WriteString(renderUnifiedLine(fileName, line, config.Width, theme.CurrentTheme()))
+ sb.WriteString("\n")
+ }
+
+ return sb.String()
+}
+
// RenderSideBySideHunk formats a hunk for side-by-side display
func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
// Apply options to create the configuration
@@ -792,6 +919,21 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
return sb.String()
}
+// FormatUnifiedDiff creates a unified formatted view of a diff
+func FormatUnifiedDiff(filename string, diffText string, opts ...UnifiedOption) (string, error) {
+ diffResult, err := ParseUnifiedDiff(diffText)
+ if err != nil {
+ return "", err
+ }
+
+ var sb strings.Builder
+ for _, h := range diffResult.Hunks {
+ sb.WriteString(RenderUnifiedHunk(filename, h, opts...))
+ }
+
+ return sb.String(), nil
+}
+
// FormatDiff creates a side-by-side formatted view of a diff
func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (string, error) {
// t := theme.CurrentTheme()
diff --git a/packages/tui/internal/layout/layout.go b/packages/tui/internal/layout/layout.go
index b49913bde..b3afc5cc7 100644
--- a/packages/tui/internal/layout/layout.go
+++ b/packages/tui/internal/layout/layout.go
@@ -11,7 +11,6 @@ var Current *LayoutInfo
func init() {
Current = &LayoutInfo{
- Size: LayoutSizeNormal,
Viewport: Dimensions{Width: 80, Height: 25},
Container: Dimensions{Width: 80, Height: 25},
}
@@ -19,19 +18,12 @@ func init() {
type LayoutSize string
-const (
- LayoutSizeSmall LayoutSize = "small"
- LayoutSizeNormal LayoutSize = "normal"
- LayoutSizeLarge LayoutSize = "large"
-)
-
type Dimensions struct {
Width int
Height int
}
type LayoutInfo struct {
- Size LayoutSize
Viewport Dimensions
Container Dimensions
}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index dfeb30fc4..3e4909e6b 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -181,18 +181,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height
- size := layout.LayoutSizeNormal
- if a.width < 40 {
- size = layout.LayoutSizeSmall
- } else if a.width < 80 {
- size = layout.LayoutSizeNormal
- } else {
- size = layout.LayoutSizeLarge
- }
-
// TODO: move away from global state
layout.Current = &layout.LayoutInfo{
- Size: size,
Viewport: layout.Dimensions{
Width: a.width,
Height: a.height,