Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/tui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,10 @@ func (m *Model) updateMainPanel() tea.Cmd {
if content == "" {
content = "Select an item to see details."
}
// Apply adaptive visual styling: split-view for wide terminals, unified for narrow ones
// Calculate right panel width (approximately 65% of total width minus borders)
rightPanelWidth := int(float64(m.width)*(1-leftPanelWidthRatio)) - borderWidth - 2
content = renderAdaptiveDiffView(content, rightPanelWidth, m.theme)
return mainContentUpdatedMsg{content: content}
}
}
Expand Down
251 changes: 251 additions & 0 deletions internal/tui/view.go
Comment thread
shatrughantwt marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,257 @@ func stripAnsi(str string) string {
return ansiRegex.ReplaceAllString(str, "")
}

// diffLineType represents the type of a diff line.
type diffLineType int

const (
lineTypeContext diffLineType = iota
lineTypeAdded
lineTypeRemoved
lineTypeHeader
lineTypeFileHeader
lineTypeHunkHeader
)

// diffRow represents a single row in the structured diff.
type diffRow struct {
lineType diffLineType
oldLine string
newLine string
rawLine string // Original line for fallback
}

// parseDiffStructure transforms a unified diff into structured rows suitable for split-view rendering.
func parseDiffStructure(content string) []diffRow {
lines := strings.Split(content, "\n")
Comment thread
shatrughantwt marked this conversation as resolved.
var rows []diffRow

for _, line := range lines {
if len(line) == 0 {
rows = append(rows, diffRow{lineType: lineTypeContext, oldLine: "", newLine: "", rawLine: ""})
continue
}

firstChar := line[0]

// File headers
if strings.HasPrefix(line, "diff --git") ||
strings.HasPrefix(line, "index ") {
rows = append(rows, diffRow{lineType: lineTypeFileHeader, rawLine: line})
continue
}

// --- and +++ headers (but not as content)
if strings.HasPrefix(line, "---") && len(line) > 3 && line[3] == ' ' {
rows = append(rows, diffRow{lineType: lineTypeFileHeader, rawLine: line})
continue
}
if strings.HasPrefix(line, "+++") && len(line) > 3 && line[3] == ' ' {
rows = append(rows, diffRow{lineType: lineTypeFileHeader, rawLine: line})
continue
}

// Hunk headers
if strings.HasPrefix(line, "@@") {
rows = append(rows, diffRow{lineType: lineTypeHunkHeader, rawLine: line})
continue
}

// Newline marker
if strings.HasPrefix(line, "\\ No newline") {
rows = append(rows, diffRow{lineType: lineTypeFileHeader, rawLine: line})
continue
}

// Added lines (but not +++ headers)
if firstChar == '+' {
rows = append(rows, diffRow{lineType: lineTypeAdded, newLine: line, rawLine: line})
continue
}

// Removed lines (but not --- headers)
if firstChar == '-' {
rows = append(rows, diffRow{lineType: lineTypeRemoved, oldLine: line, rawLine: line})
continue
}

// Context lines (start with space)
if firstChar == ' ' {
contentWithoutSpace := line[1:]
rows = append(rows, diffRow{lineType: lineTypeContext, oldLine: contentWithoutSpace, newLine: contentWithoutSpace, rawLine: line})
continue
}

// Fallback for any other lines
rows = append(rows, diffRow{lineType: lineTypeContext, oldLine: line, newLine: line, rawLine: line})
}

return rows
}

// renderSplitDiffView renders a GitHub-style split-view diff.
// columnWidth should be roughly half the viewport width minus some padding.
func renderSplitDiffView(rows []diffRow, columnWidth int, theme Theme) string {
if columnWidth < 20 {
// Column too narrow for split view
return ""
}

// Create themed styles
headerStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#d2a8ff")).
Bold(true)

// Use theme's added/removed colors for backgrounds
addedStyle := theme.GitStaged.
Width(columnWidth).
Padding(0, 1)

removedStyle := theme.GitUnstaged.
Width(columnWidth).
Padding(0, 1)

contextStyle := lipgloss.NewStyle().
Width(columnWidth).
Padding(0, 1)

emptyStyle := lipgloss.NewStyle().
Width(columnWidth).
Padding(0, 1)

var renderedRows []string

for _, row := range rows {
var left, right string

switch row.lineType {
case lineTypeFileHeader:
// File headers span full width
fullLine := headerStyle.Width(columnWidth * 2).Render(row.rawLine)
renderedRows = append(renderedRows, fullLine)

case lineTypeHunkHeader:
// Hunk headers span full width
fullLine := headerStyle.Width(columnWidth * 2).Render(row.rawLine)
renderedRows = append(renderedRows, fullLine)

case lineTypeRemoved:
// Removed line on left, empty on right
left = removedStyle.Render(strings.TrimPrefix(row.oldLine, "-"))
right = emptyStyle.Render("")
renderedRows = append(renderedRows, lipgloss.JoinHorizontal(lipgloss.Top, left, right))

case lineTypeAdded:
// Empty on left, added line on right
left = emptyStyle.Render("")
right = addedStyle.Render(strings.TrimPrefix(row.newLine, "+"))
renderedRows = append(renderedRows, lipgloss.JoinHorizontal(lipgloss.Top, left, right))

case lineTypeContext:
// Context lines on both sides
left = contextStyle.Render(row.oldLine)
right = contextStyle.Render(row.newLine)
renderedRows = append(renderedRows, lipgloss.JoinHorizontal(lipgloss.Top, left, right))

default:
// Fallback: show on both sides
left = contextStyle.Render(row.oldLine)
right = contextStyle.Render(row.newLine)
renderedRows = append(renderedRows, lipgloss.JoinHorizontal(lipgloss.Top, left, right))
}
}

return strings.Join(renderedRows, "\n")
}

// renderAdaptiveDiffView returns the appropriately formatted diff based on viewport width.
// If width >= 120, uses split-view; otherwise falls back to unified diff.
func renderAdaptiveDiffView(content string, width int, theme Theme) string {
// Quick check: if content doesn't look like a diff, return as-is
if !strings.Contains(content, "diff --git") && !strings.Contains(content, "@@") {
return content
}

// Threshold for split-view: 120 chars available
const splitViewThreshold = 120

if width >= splitViewThreshold && width > 60 {
// Use split-view mode
columnWidth := (width - 1) / 2
rows := parseDiffStructure(content)
splitView := renderSplitDiffView(rows, columnWidth, theme)
if splitView != "" {
return splitView
}
}

// Fallback to unified diff styling
return styleDiffContent(content, theme)
}

// styleDiffContent applies visual highlighting to diff lines for better readability.
// It detects and styles:
// - Diff headers (diff --git, index, ---, +++, @@) with bold magenta
// - Added lines (+) with green
// - Removed lines (-) with red
// - Context lines with normal or dimmed styling
// It also adds visual separation before hunk markers for improved scanability.
func styleDiffContent(content string, theme Theme) string {
// Quick check: if content doesn't look like a diff, return as-is
if !strings.Contains(content, "diff --git") && !strings.Contains(content, "@@") {
return content
}

lines := strings.Split(content, "\n")

// Create styles for different diff elements
headerStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#d2a8ff")).
Bold(true)

// Use theme colors for added/removed lines (aligns with git status colors)
addedStyle := theme.GitStaged // Green
removedStyle := theme.GitUnstaged // Red

var result []string
for i, line := range lines {
if len(line) == 0 {
result = append(result, line)
continue
}

firstChar := line[0]

// Add spacing before hunk markers (visual separation of hunks)
if strings.HasPrefix(line, "@@") && i > 0 && result[len(result)-1] != "" {
result = append(result, "") // Add blank line for visual separation
}

// Handle diff headers
if strings.HasPrefix(line, "diff --git") ||
strings.HasPrefix(line, "index ") ||
strings.HasPrefix(line, "---") ||
strings.HasPrefix(line, "+++") ||
strings.HasPrefix(line, "@@") {
result = append(result, headerStyle.Render(line))
} else if firstChar == '+' && !strings.HasPrefix(line, "+++") {
// Added line: apply green styling
result = append(result, addedStyle.Render(line))
} else if firstChar == '-' && !strings.HasPrefix(line, "---") {
// Removed line: apply red styling
result = append(result, removedStyle.Render(line))
} else if firstChar == '\\' {
// "\ No newline at end of file" marker - treat as metadata
result = append(result, headerStyle.Render(line))
} else {
// Context line (starts with space) - keep as-is
result = append(result, line)
}
}

return strings.Join(result, "\n")
}

// View is the main render function for the application.
func (m Model) View() string {

Expand Down