Skip to content

feat: refactor and modularize TUI for enhanced themes and flow rendering#11

Merged
appleboy merged 1 commit intomainfrom
tui
Feb 28, 2026
Merged

feat: refactor and modularize TUI for enhanced themes and flow rendering#11
appleboy merged 1 commit intomainfrom
tui

Conversation

@appleboy
Copy link
Member

  • Upgrade from Charmbracelet BubbleTea/Bubbles/Lipgloss v1 to v2 and refactor imports accordingly
  • Introduce modular TUI configuration structure with support for themes, animations, and QR code toggling
  • Implement a unified flow renderer for rich terminal display without a full Bubble Tea program
  • Add a dedicated help view component for dynamic keybinding/help display in the TUI
  • Replace ad-hoc progress bar with Bubbles v2 progress component and add animation support
  • Replace static help text with toggleable, context-aware help panel
  • Enhance error presentation with a new ErrorView component including rich suggestions
  • Mask access and refresh tokens throughout all views and outputs for improved security
  • Add QR code rendering utility for device flow URLs with Unicode and ASCII fallback
  • Provide flexible flow step tracking and visualization in BubbleTeaManager
  • Improve device code flow UX with clearer device info presentation and spinner animation
  • Refactor token info display to show masked tokens, file location, and additional metadata
  • Remove formatPercentage test since logic moved into Bubbles progress implementation
  • Add platform detection and terminal size utilities for responsive layouts
  • Introduce comprehensive tests for progress bar and info box view rendering
  • Improve token expiration and refresh flow feedback with step visualization
  • Add unified flow step and status struct for better state representation and display
  • Add granular update methods for BubbleTeaManager to handle each flow scenario interactively
  • Integrate new components and views for improved modularity and future extensibility

- Upgrade from Charmbracelet BubbleTea/Bubbles/Lipgloss v1 to v2 and refactor imports accordingly
- Introduce modular TUI configuration structure with support for themes, animations, and QR code toggling
- Implement a unified flow renderer for rich terminal display without a full Bubble Tea program
- Add a dedicated help view component for dynamic keybinding/help display in the TUI
- Replace ad-hoc progress bar with Bubbles v2 progress component and add animation support
- Replace static help text with toggleable, context-aware help panel
- Enhance error presentation with a new ErrorView component including rich suggestions
- Mask access and refresh tokens throughout all views and outputs for improved security
- Add QR code rendering utility for device flow URLs with Unicode and ASCII fallback
- Provide flexible flow step tracking and visualization in BubbleTeaManager
- Improve device code flow UX with clearer device info presentation and spinner animation
- Refactor token info display to show masked tokens, file location, and additional metadata
- Remove formatPercentage test since logic moved into Bubbles progress implementation
- Add platform detection and terminal size utilities for responsive layouts
- Introduce comprehensive tests for progress bar and info box view rendering
- Improve token expiration and refresh flow feedback with step visualization
- Add unified flow step and status struct for better state representation and display
- Add granular update methods for BubbleTeaManager to handle each flow scenario interactively
- Integrate new components and views for improved modularity and future extensibility

Signed-off-by: appleboy <appleboy.tw@gmail.com>
Copilot AI review requested due to automatic review settings February 28, 2026 08:43
@appleboy appleboy merged commit c30ecfe into main Feb 28, 2026
18 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the CLI’s terminal UI to the Charmbracelet v2 ecosystem and introduces a unified, modular rendering approach for OAuth flows with improved theming, help, progress, error display, and token masking.

Changes:

  • Upgrades Bubble Tea/Bubbles/Lipgloss imports to v2 (charm.land/*/v2) and refactors models/views accordingly.
  • Adds a unified flow renderer (FlowRenderer + UnifiedFlowModel) to render rich flow output without running a full Bubble Tea program.
  • Introduces new modular TUI components (HelpView, ErrorView, QR code rendering, v2 progress bar) and masks tokens across views/outputs.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tui/unified_flow_view.go New unified flow model + rendering helpers (header/warnings/steps/token info).
tui/token_view.go New scrollable token details viewport with masked token display.
tui/styles.go Switches to lipgloss v2 import; adds maskTokenPreview.
tui/simple_manager.go Masks tokens in simple output; adds token-file notice; adds maskTokenSimple.
tui/flow_renderer.go New non-BubbleTea renderer with step spinner animation and device/token boxes.
tui/device_view.go Uses lipgloss v2; switches to HelpView and enhanced ErrorView; masks token preview.
tui/device_model.go Migrates to Bubble Tea v2 message types; adds HelpView/config plumbing; View() now returns tea.View.
tui/config.go New env-driven TUI config (animations, QR codes, theme preset, unicode).
tui/components/timer.go Updates lipgloss import to v2.
tui/components/step_indicator.go Updates lipgloss import to v2.
tui/components/qrcode.go New Unicode + ASCII QR code rendering utility.
tui/components/progress_bar.go Replaces custom bar with Bubbles v2 progress component + animation support.
tui/components/info_box.go Updates lipgloss import to v2.
tui/components/help_view.go New help/keybinding component wrapping Bubbles help/key.
tui/components/error_view.go New rich error presentation with suggestions/retry hint.
tui/components/components_test.go Updates tests to accommodate new progress bar implementation; removes percentage formatter test.
tui/bubbletea_manager.go Refactors manager to use FlowRenderer with step tracking; adds terminal sizing helper.
tui/browser_view.go Uses lipgloss v2; switches to HelpView and enhanced ErrorView; masks token preview.
tui/browser_model.go Migrates to Bubble Tea v2 message types; integrates HelpView; forwards msgs to progress bar.
go.mod Switches to charm.land/*/v2 deps; adds go-qrcode and related indirect deps.
go.sum Updates dependency checksums for new v2 and QR code dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 47 to +61
func (p *ProgressBar) View() string {
filledStyle := lipgloss.NewStyle().
Background(lipgloss.Color("#7D56F4")).
Foreground(lipgloss.Color("#FFFFFF"))

emptyStyle := lipgloss.NewStyle().
Background(lipgloss.Color("#3E3E3E")).
Foreground(lipgloss.Color("#888888"))

filled := int(float64(p.Width) * p.Progress)
empty := p.Width - filled

bar := filledStyle.Render(strings.Repeat("▓", filled)) +
emptyStyle.Render(strings.Repeat("░", empty))
bar := p.model.View()

// Add percentage display
percentage := lipgloss.NewStyle().
Foreground(lipgloss.Color("#888888")).
Render(" " + formatPercentage(p.Progress))
Render(p.model.ViewAs(p.model.Percent()))

return bar + percentage
return bar + " " + percentage
}

// ViewCompact renders a compact version of the progress bar
func (p *ProgressBar) ViewCompact() string {
return formatPercentage(p.Progress)
return p.model.ViewAs(p.model.Percent())
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The percentage display is rendered using p.model.ViewAs(p.model.Percent()), which (per the progress API naming) renders another progress bar, not a percentage string. This likely results in the bar being printed twice and ViewCompact() returning a bar instead of something like 50%. Consider formatting the percent explicitly from p.model.Percent() (or using the progress component’s percentage formatting option if available) and keep ViewAs only for rendering the bar at a given percent in tests/debugging.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +53
Quit: key.NewBinding(
key.WithKeys("q", "esc"),
key.WithHelp("q/esc", "quit"),
),
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "scroll up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "scroll down"),
),
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

Quit is advertised as q/esc in the default key map, but the BrowserModel/DeviceModel Update handlers currently only quit on Ctrl+C (and TokenViewModel quits on q). This makes the on-screen help misleading. Either add q/esc handling to the relevant models or update the key map/help text to match the actual supported keys per view.

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +333
// formatDuration formats duration in a human-readable way
func formatDuration(d time.Duration) string {
d = d.Round(time.Second)
h := int(d.Hours())
m := int(d.Minutes()) % 60
s := int(d.Seconds()) % 60

if h > 0 {
return fmt.Sprintf("%dh%dm%ds", h, m, s)
}
if m > 0 {
return fmt.Sprintf("%dm%ds", m, s)
}
return fmt.Sprintf("%ds", s)
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

formatDuration doesn’t handle negative durations. When a token is already expired, time.Until(ExpiresAt) will be negative and this will render values like -3s, which is confusing compared to the existing formatDurationHuman logic that returns expired. Consider clamping at 0 or returning an "expired" label when d < 0.

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +227
// Move to position after header
fmt.Printf("\033[%d;0H", headerLines)
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

ANSI cursor positioning sequences are 1-based. Using \033[%d;0H relies on terminals treating column 0 as 1, which isn’t guaranteed and can cause cursor placement glitches. Consider using column 1 (e.g., ;1H) consistently for portability.

Suggested change
// Move to position after header
fmt.Printf("\033[%d;0H", headerLines)
// Move to position after header (column indices are 1-based)
fmt.Printf("\033[%d;1H", headerLines)

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +167
// maskTokenPreview masks token for preview display (shows first 8 and last 4 chars)
func maskTokenPreview(token string) string {
if len(token) <= 16 {
if len(token) <= 8 {
return token[:min(len(token), 4)] + "..."
}
return token[:4] + "..." + token[len(token)-4:]
}
return token[:8] + "..." + token[len(token)-4:]
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

Token masking logic is now implemented in multiple places (maskTokenPreview, maskToken in token_view.go, and maskTokenSimple in simple_manager.go) with identical behavior. This duplication makes it easy for masking behavior to diverge over time. Consider consolidating on a single helper in the tui package and reusing it everywhere tokens are displayed.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +240
if len(token) <= 16 {
if len(token) <= 8 {
return token[:min(len(token), 4)] + "..."
}
return token[:4] + "..." + token[len(token)-4:]
}
return token[:8] + "..." + token[len(token)-4:]
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

ShowTokenInfo duplicates token file path resolution and token masking logic that already exists elsewhere in the tui package (e.g., getTokenFilePath and maskTokenPreview). Reusing the shared helpers would reduce drift and keep token masking/file-path behavior consistent across simple and interactive outputs.

Suggested change
if len(token) <= 16 {
if len(token) <= 8 {
return token[:min(len(token), 4)] + "..."
}
return token[:4] + "..." + token[len(token)-4:]
}
return token[:8] + "..." + token[len(token)-4:]
// Delegate to shared masking helper to keep behavior consistent
return maskTokenPreview(token)

Copilot uses AI. Check for mistakes.
Comment on lines 87 to 121
func TestProgressBar(t *testing.T) {
bar := NewProgressBar(40)

if bar.Progress != 0.0 {
t.Errorf("Expected initial progress to be 0.0, got %f", bar.Progress)
}

if bar.Width != 40 {
t.Errorf("Expected width to be 40, got %d", bar.Width)
// Test view renders with initial progress
view := bar.View()
if view == "" {
t.Error("View should not be empty")
}

// Test setting progress
// Test setting progress - verify it doesn't panic
bar.SetProgress(0.5)
if bar.Progress != 0.5 {
t.Errorf("Expected progress to be 0.5, got %f", bar.Progress)
view = bar.View()
if view == "" {
t.Error("View should not be empty after setting progress")
}

// Test clamping to 0-1 range
// Test clamping to 0-1 range - verify it doesn't panic
bar.SetProgress(-0.1)
if bar.Progress != 0.0 {
t.Errorf("Expected progress to be clamped to 0.0, got %f", bar.Progress)
view = bar.View()
if view == "" {
t.Error("View should not be empty after setting negative progress")
}

bar.SetProgress(1.5)
if bar.Progress != 1.0 {
t.Errorf("Expected progress to be clamped to 1.0, got %f", bar.Progress)
view = bar.View()
if view == "" {
t.Error("View should not be empty after setting progress > 1.0")
}

// Test view renders
view := bar.View()
// Test width change
bar.SetWidth(60)
view = bar.View()
if view == "" {
t.Error("View should not be empty")
t.Error("View should not be empty after changing width")
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The updated ProgressBar tests only assert that View() is non-empty, which won’t catch regressions like incorrect percentage rendering or width handling. Since the progress bar implementation changed significantly, consider adding assertions that the rendered output contains the expected percentage (e.g., 0%, 50%, 100%) and reacts to SetWidth() as intended.

Copilot uses AI. Check for mistakes.
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.

2 participants