|
| 1 | +package sidebar |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "strings" |
| 6 | + |
| 7 | + "charm.land/lipgloss/v2" |
| 8 | + |
| 9 | + "github.com/docker/cagent/pkg/tui/styles" |
| 10 | +) |
| 11 | + |
| 12 | +// CollapsedViewModel holds the computed layout decisions for collapsed mode. |
| 13 | +// This is a pure data structure - rendering is handled by separate view functions. |
| 14 | +// Computing this once avoids duplicating the layout logic between CollapsedHeight and collapsedView. |
| 15 | +type CollapsedViewModel struct { |
| 16 | + TitleWithStar string |
| 17 | + WorkingIndicator string |
| 18 | + WorkingDir string |
| 19 | + UsageSummary string |
| 20 | + |
| 21 | + // Layout decisions computed from the data |
| 22 | + TitleAndIndicatorOnOneLine bool |
| 23 | + WdAndUsageOnOneLine bool |
| 24 | + ContentWidth int |
| 25 | +} |
| 26 | + |
| 27 | +// LineCount returns the number of lines needed to render this layout. |
| 28 | +func (vm CollapsedViewModel) LineCount() int { |
| 29 | + lines := 1 // divider |
| 30 | + |
| 31 | + switch { |
| 32 | + case vm.TitleAndIndicatorOnOneLine: |
| 33 | + lines++ |
| 34 | + case vm.WorkingIndicator == "": |
| 35 | + // No working indicator but title wraps |
| 36 | + lines += linesNeeded(lipgloss.Width(vm.TitleWithStar), vm.ContentWidth) |
| 37 | + default: |
| 38 | + // Title and working indicator on separate lines, each may wrap |
| 39 | + lines += linesNeeded(lipgloss.Width(vm.TitleWithStar), vm.ContentWidth) |
| 40 | + lines += linesNeeded(lipgloss.Width(vm.WorkingIndicator), vm.ContentWidth) |
| 41 | + } |
| 42 | + |
| 43 | + if vm.WdAndUsageOnOneLine { |
| 44 | + lines++ |
| 45 | + } else { |
| 46 | + lines += linesNeeded(lipgloss.Width(vm.WorkingDir), vm.ContentWidth) |
| 47 | + if vm.UsageSummary != "" { |
| 48 | + lines += linesNeeded(lipgloss.Width(vm.UsageSummary), vm.ContentWidth) |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + return lines |
| 53 | +} |
| 54 | + |
| 55 | +// RenderCollapsedView renders the collapsed sidebar from a CollapsedViewModel. |
| 56 | +// This is a pure function that takes data and returns a string. |
| 57 | +func RenderCollapsedView(vm CollapsedViewModel) string { |
| 58 | + var lines []string |
| 59 | + |
| 60 | + // Title line(s) |
| 61 | + switch { |
| 62 | + case vm.TitleAndIndicatorOnOneLine: |
| 63 | + if vm.WorkingIndicator == "" { |
| 64 | + lines = append(lines, vm.TitleWithStar) |
| 65 | + } else { |
| 66 | + gap := vm.ContentWidth - lipgloss.Width(vm.TitleWithStar) - lipgloss.Width(vm.WorkingIndicator) |
| 67 | + lines = append(lines, fmt.Sprintf("%s%*s%s", vm.TitleWithStar, gap, "", vm.WorkingIndicator)) |
| 68 | + } |
| 69 | + case vm.WorkingIndicator == "": |
| 70 | + // No working indicator but title wraps - just output title (lipgloss will wrap) |
| 71 | + lines = append(lines, vm.TitleWithStar) |
| 72 | + default: |
| 73 | + // Title and working indicator on separate lines |
| 74 | + lines = append(lines, vm.TitleWithStar, vm.WorkingIndicator) |
| 75 | + } |
| 76 | + |
| 77 | + // Working directory + usage line(s) |
| 78 | + if vm.WdAndUsageOnOneLine { |
| 79 | + gap := vm.ContentWidth - lipgloss.Width(vm.WorkingDir) - lipgloss.Width(vm.UsageSummary) |
| 80 | + lines = append(lines, fmt.Sprintf("%s%*s%s", styles.MutedStyle.Render(vm.WorkingDir), gap, "", vm.UsageSummary)) |
| 81 | + } else { |
| 82 | + lines = append(lines, styles.MutedStyle.Render(vm.WorkingDir)) |
| 83 | + if vm.UsageSummary != "" { |
| 84 | + lines = append(lines, vm.UsageSummary) |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + return strings.Join(lines, "\n") |
| 89 | +} |
| 90 | + |
| 91 | +// linesNeeded calculates how many lines are needed to display text of given width |
| 92 | +// within a container of contentWidth. Returns at least 1 line. |
| 93 | +func linesNeeded(textWidth, contentWidth int) int { |
| 94 | + if contentWidth <= 0 || textWidth <= 0 { |
| 95 | + return 1 |
| 96 | + } |
| 97 | + return max(1, (textWidth+contentWidth-1)/contentWidth) |
| 98 | +} |
0 commit comments