Skip to content

feat(ui): adopt Charm v2 components for TUI primitives#110

Open
flexiondotorg wants to merge 5 commits into
mainfrom
bling
Open

feat(ui): adopt Charm v2 components for TUI primitives#110
flexiondotorg wants to merge 5 commits into
mainfrom
bling

Conversation

@flexiondotorg
Copy link
Copy Markdown
Contributor

Replace hand-rolled spinner, progress bars, metric table, and level meter with first-party Charm v2 components (bubbles/spinner, bubbles/progress, lipgloss/table, harmonica springs). Centralise colour palette to cli/styles.go with adaptive downsampling via lipgloss v2. CLI help and version now honour NO_COLOR and COLORPROFILE. Spinner, progress bars, and eased meter work as intended; progress bar now fits terminal cleanly in brand red.

All tests pass. Metric table retains all formatters and edge-case coverage; 50+ lines of manual column-width maths eliminated.

Replace hand-rolled spinner, progress bars, metric table, and level
meter with first-party Charm v2 components (bubbles/spinner,
bubbles/progress, lipgloss/table, harmonica springs). Centralise colour
palette to cli/styles.go with adaptive downsampling via lipgloss v2. CLI
help and version now honour NO_COLOR and COLORPROFILE. Spinner, progress
bars, and eased meter work as intended; progress bar now fits terminal
cleanly in brand red.

All tests pass. Metric table retains all formatters and edge-case
coverage; 50+ lines of manual column-width maths eliminated.

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 13 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: The PR replaces hand-rolled UI components with well-tested Charm v2 libraries for spinner, progress bars, metric table, and level meter, centralizes the colour palette with proper downsampling for NO_COLOR support, and includes thorough test coverage confirming correct behaviour and no...

Re-trigger cubic

- Add cyan→violet gradient via Charm WithColors + WithScaled
- Apply harmonica spring easing for smooth per-file animation
- Constrain bar width to 40 cells for alignment with VU meter
- Add accent colours to centralised palette
- Lock behaviour in progress_bar_test.go

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 5 files (changes from recent commits).

Auto-approved: This PR replaces hand-rolled TUI components (spinner, progress bar, meter, table) with well-tested Charm v2 libraries and centralises the colour palette; changes are confined to presentation logic, add thorough tests (downsampling, gradients, message routing), and pose no risk to core business...

Re-trigger cubic

…ation

- Replace hard green/orange/red zones with gradient ramp (green → yellow
  → orange → red)
- Use lipgloss.Blend1D for per-cell CIELAB colour interpolation
- Position threshold stops to preserve prior dB feel (green-dominant low
  range, warm colours compressed into hot end)
- Add ColorYellow to centralised palette
- Assert gradient quality in progress_bar_test.go (>3 distinct colours,
  vivid endpoints and midpoint)

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 3 files (changes from recent commits).

Requires human review: This PR refactors core UI and CLI components—spinner, progress bars, table rendering, color system, and output formatting—with 1270 lines changed across multiple critical files, which carries a high risk of breaking CLI output, TUI display, or color handling; the blast radius and impact are too...

Re-trigger cubic

…der and orange peak

- Lower VU meter display and audio level reporting from -60 dB floor to
  -70 dB
- Add meterLevelFloorDB constant in processor to mirror UI meterFloorDB,
  prevent drift
- Change panel border from sky blue, peak-marker pulse to shades of
  orange
- Update test assertions for new level floor and marker colours

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/ui/progress_bar_test.go">

<violation number="1" location="internal/ui/progress_bar_test.go:190">
P2: The test can pass even when no meter bar line is rendered, masking regressions.</violation>
</file>

<file name="internal/ui/views.go">

<violation number="1" location="internal/ui/views.go:226">
P3: Peak marker has an off-by-one alignment bug at 0 dB and can render outside the meter width.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +190 to +192
if strings.ContainsRune(barLine, '|') {
t.Errorf("bar line still contains in-bar peak glyph '|':\n%q", barLine)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The test can pass even when no meter bar line is rendered, masking regressions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/ui/progress_bar_test.go, line 190:

<comment>The test can pass even when no meter bar line is rendered, masking regressions.</comment>

<file context>
@@ -142,6 +174,94 @@ func TestMeterIsGradient(t *testing.T) {
+			break
+		}
+	}
+	if strings.ContainsRune(barLine, '|') {
+		t.Errorf("bar line still contains in-bar peak glyph '|':\n%q", barLine)
+	}
</file context>
Suggested change
if strings.ContainsRune(barLine, '|') {
t.Errorf("bar line still contains in-bar peak glyph '|':\n%q", barLine)
}
if barLine == "" {
t.Fatalf("no meter bar line found in output:\n%q", bar)
}
if strings.ContainsRune(barLine, '|') {
t.Errorf("bar line still contains in-bar peak glyph '|':\n%q", barLine)
}

Comment thread internal/ui/views.go
// (peak still at the silence floor), so no stray triangle sits at column 0.
if peakLevel > minDB {
b.WriteByte('\n')
b.WriteString(strings.Repeat(" ", peakPos))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Peak marker has an off-by-one alignment bug at 0 dB and can render outside the meter width.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/ui/views.go, line 226:

<comment>Peak marker has an off-by-one alignment bug at 0 dB and can render outside the meter width.</comment>

<file context>
@@ -221,9 +218,43 @@ func renderAudioLevelMeter(currentLevel, peakLevel float64) string {
+	// (peak still at the silence floor), so no stray triangle sits at column 0.
+	if peakLevel > minDB {
+		b.WriteByte('\n')
+		b.WriteString(strings.Repeat(" ", peakPos))
+		b.WriteString(lipgloss.NewStyle().Foreground(peakMarkerColor(elapsed)).Render("▲"))
+	}
</file context>
Suggested change
b.WriteString(strings.Repeat(" ", peakPos))
b.WriteString(strings.Repeat(" ", max(0, min(peakPos, width-1))))

…ector

Replace Time/ETA line with mini timeline + realtime speed badge, showing
progress visually while computing on-the-fly processing speed (×). Move
peak dB value from header to elbow connector tethering it to the peak
marker, strengthening visual hierarchy. Plumb audio Duration through
processor to UI (ProgressUpdate → ProgressMsg → FileProgress) to enable
realtime calculation.

- Add Duration field to ProgressUpdate, ProgressMsg, FileProgress
- Capture Duration from audio.Metadata in analyzer Pass 1; carry through
  all processor passes
- Redesign Time block: ⏱ HH:MM ▰▰▱▱▱▱▱▱ HH:MM  ·  ⚡ N.N× (with guards
  for stale data)
- Reposition Peak dB as elbow connector (└ value) beneath the ▲ marker,
  both pulsing orange
- Handle right-edge overflow: flip connector for peak positions > 34
- Add focused tests for timeline fill, clock projection, realtime badge
  guards, elbow alignment

Signed-off-by: Martin Wimpress <code@wimpress.io>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/ui/views.go">

<violation number="1" location="internal/ui/views.go:282">
P2: Peak label alignment uses byte length (`len`) instead of display width, which misplaces the new elbow/label layout with Unicode text.</violation>
</file>

<file name="internal/ui/progress_bar_test.go">

<violation number="1" location="internal/ui/progress_bar_test.go:280">
P2: Peak-elbow alignment test uses byte indices instead of display columns, making it unreliable with multibyte glyphs like `㏈`.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/ui/views.go
Comment on lines +282 to +294
if peakPos+len(" "+value)+1 <= width {
b.WriteString(strings.Repeat(" ", peakPos))
b.WriteString(elbowStyle.Render("└"))
b.WriteByte(' ')
b.WriteString(valueStyle.Render(value))
} else {
// value then a right-elbow ┘ ending under the peak column.
lead := max(peakPos-(len(value)+1), 0)
b.WriteString(strings.Repeat(" ", lead))
b.WriteString(valueStyle.Render(value))
b.WriteByte(' ')
b.WriteString(elbowStyle.Render("┘"))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Peak label alignment uses byte length (len) instead of display width, which misplaces the new elbow/label layout with Unicode text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/ui/views.go, line 282:

<comment>Peak label alignment uses byte length (`len`) instead of display width, which misplaces the new elbow/label layout with Unicode text.</comment>

<file context>
@@ -218,13 +261,37 @@ func renderAudioLevelMeter(currentLevel, peakLevel float64, elapsed time.Duratio
+		// Default: elbow drops to the right (└ value). When the right-elbow form
+		// would overflow the bar, flip to the left (value ┘) so the label stays
+		// within meterWidth.
+		if peakPos+len(" "+value)+1 <= width {
+			b.WriteString(strings.Repeat(" ", peakPos))
+			b.WriteString(elbowStyle.Render("└"))
</file context>
Suggested change
if peakPos+len(" "+value)+1 <= width {
b.WriteString(strings.Repeat(" ", peakPos))
b.WriteString(elbowStyle.Render("└"))
b.WriteByte(' ')
b.WriteString(valueStyle.Render(value))
} else {
// value then a right-elbow ┘ ending under the peak column.
lead := max(peakPos-(len(value)+1), 0)
b.WriteString(strings.Repeat(" ", lead))
b.WriteString(valueStyle.Render(value))
b.WriteByte(' ')
b.WriteString(elbowStyle.Render("┘"))
}
valueW := lipgloss.Width(value)
if peakPos+1+1+valueW <= width {
b.WriteString(strings.Repeat(" ", peakPos))
b.WriteString(elbowStyle.Render("└"))
b.WriteByte(' ')
b.WriteString(valueStyle.Render(value))
} else {
// value then a right-elbow ┘ ending under the peak column.
lead := max(peakPos-(valueW+1), 0)
b.WriteString(strings.Repeat(" ", lead))
b.WriteString(valueStyle.Render(value))
b.WriteByte(' ')
b.WriteString(elbowStyle.Render("┘"))
}

Comment on lines +280 to +288
triCol := strings.IndexRune(triLine, '▲')
var elbowCol int
if tc.wantLeft {
elbowCol = strings.IndexRune(elbowLine, '┘')
if !strings.HasSuffix(strings.TrimRight(elbowLine, " "), "┘") {
t.Errorf("peak=%g: left-flip elbow not ending in '┘': %q", tc.peak, elbowLine)
}
} else {
elbowCol = strings.IndexRune(elbowLine, '└')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Peak-elbow alignment test uses byte indices instead of display columns, making it unreliable with multibyte glyphs like .

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/ui/progress_bar_test.go, line 280:

<comment>Peak-elbow alignment test uses byte indices instead of display columns, making it unreliable with multibyte glyphs like `㏈`.</comment>

<file context>
@@ -226,6 +226,94 @@ func TestMeterPeakTriangleAlignsBeneathBar(t *testing.T) {
+		}
+
+		// Elbow glyph aligns at the peak column (same as the triangle).
+		triCol := strings.IndexRune(triLine, '▲')
+		var elbowCol int
+		if tc.wantLeft {
</file context>
Suggested change
triCol := strings.IndexRune(triLine, '▲')
var elbowCol int
if tc.wantLeft {
elbowCol = strings.IndexRune(elbowLine, '┘')
if !strings.HasSuffix(strings.TrimRight(elbowLine, " "), "┘") {
t.Errorf("peak=%g: left-flip elbow not ending in '┘': %q", tc.peak, elbowLine)
}
} else {
elbowCol = strings.IndexRune(elbowLine, '└')
triIdx := strings.IndexRune(triLine, '▲')
triCol := ansi.StringWidth(triLine[:triIdx])
var elbowCol int
if tc.wantLeft {
elbowIdx := strings.IndexRune(elbowLine, '┘')
elbowCol = ansi.StringWidth(elbowLine[:elbowIdx])
if !strings.HasSuffix(strings.TrimRight(elbowLine, " "), "┘") {
t.Errorf("peak=%g: left-flip elbow not ending in '┘': %q", tc.peak, elbowLine)
}
} else {
elbowIdx := strings.IndexRune(elbowLine, '└')
elbowCol = ansi.StringWidth(elbowLine[:elbowIdx])
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant