Skip to content

feat(v3/cli): replace pterm with glyph + direct ANSI — full TUI redesign - #5411

Closed
taliesin-ai wants to merge 2 commits into
wailsapp:masterfrom
taliesin-ai:agent/engineer-linux/a97ce245
Closed

taliesin-ai wants to merge 2 commits into
wailsapp:masterfrom
taliesin-ai:agent/engineer-linux/a97ce245

Conversation

@taliesin-ai

@taliesin-ai taliesin-ai commented May 11, 2026 •

Copy link
Copy Markdown
Collaborator

Remove pterm from the v3 CLI entirely and replace with a lean, bespoke terminal UI layer in v3/internal/term.

Design language

  ◆ wails v3.x.x  ─  Init Project

  ──  System

  ✓  Project created successfully.
  ✗  Build failed: exit status 1
  ⚠  Dependency missing
  ↳  Downloading AppImage tooling…

Spinner uses glyph's braille animation on TTY; degrades to a plain ↳ info line on CI/pipes.

What changed

v3/internal/term/term.go — complete rewrite:

  • Direct ANSI escape codes (no library for text rendering)
  • glyph for animated spinners (SpinnerBraille, cyan)
  • Header, Section, Table, HeaderTable, Confirm, Hyperlink
  • Spinner type with UpdateText, Success, Fail, Logger()
  • Non-TTY fallback in StartSpinner (plain info line)

v3/internal/generator/config/log.go — removed PtermLogger and DefaultPtermLogger; Logger interface unchanged

All callers updated: cmd/wails3/main.go, internal/commands/{appimage,capabilities,entitlements_setup,init,sign,signing_setup,update_cli}.go, internal/doctor/doctor.go, internal/templates/templates.go, internal/generator/analyse_test.go

v3/go.mod: added github.com/kungfusheep/glyph v0.6.0, removed github.com/pterm/pterm and its now-unused transitive deps (atomicgo.dev/{cursor,keyboard,schedule}, github.com/containerd/console, github.com/gookit/color, github.com/lithammer/fuzzysearch)

Note: go.sum was omitted from this patch to avoid checksum-file corruption during patch application. Run go mod tidy in v3/ after merging to regenerate checksums.

CC @leaanthony

Summary by CodeRabbit

  • Chores
    • Updated Go toolchain to 1.25.1
    • Replaced CLI output system with internal terminal utilities for consistent styling
    • Standardized spinners, tables, confirmation prompts, and message helpers for clearer progress and outcomes
    • Simplified CLI footer and adjusted displayed link labels and error/footer messaging
    • Unified update/install/status messaging for more concise and informative user output

Review Change Stack

Remove pterm from v3 entirely and replace with a lean, bespoke terminal
UI layer in v3/internal/term:

- Header: ◆ wails v3.x.x  ─  Command Name
- Sections: ──  Section Title
- Messages: ✓ success  ✗ error  ⚠ warning  ↳ info
- Tables: dim-keyed aligned two-column; HeaderTable with bold+separator
- Spinner: glyph braille spinner (glyph.SpinnerBraille, cyan) on TTY;
  plain info line on CI/pipe
- Confirm: simple y/N prompt without pterm dependency
- Hyperlinks: OSC 8 terminal hyperlinks
- Inline colour helpers: Cyan/Green/Red/Yellow/Blue/Magenta/Bold/Dim

All callers updated (commands/, doctor/, templates/, generator/).
config.Logger now routes through spinnerLogger in term instead of
PtermLogger; DefaultPtermLogger and PtermLogger removed from config.

Note: go.sum omitted from this patch; run `go mod tidy` in v3/ after
applying to regenerate checksums for glyph and its transitive deps.
@coderabbitai

coderabbitai Bot commented May 11, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4359981a-460b-4bcc-a998-3064dd4f42cb

📥 Commits

Reviewing files that changed from the base of the PR and between 5340f7c and 2ef15be.

📒 Files selected for processing (1)
  • v3/go.mod
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/go.mod

Walkthrough

This PR removes the external pterm dependency and replaces it with an internal term package (ANSI + glyph spinners). The new term API provides formatted output, table renderers, prompts, and a glyph-based spinner; command files, doctor, templates, tests, and go.mod are updated to use term.

Changes

Migrate from pterm to internal term library

Layer / File(s) Summary
Internal term API implementation
v3/internal/term/term.go
New ANSI-based output with toggles for color/output/debug, formatted helpers (Successf, Errorf, Printf), Table/HeaderTable, Confirm prompt, inline color helpers, and an overhauled Spinner wrapping glyph with UpdateText/Success/Fail.
Go module and dependency updates
v3/go.mod
Bumped to Go 1.25.1 and adjusted indirect dependencies: added glyph/fzf/riffkey; removed pterm and several atomicgo packages.
Config/logging layer adaptation
v3/internal/generator/config/log.go
Removed PtermLogger and DefaultPtermLogger, leaving Logger interface and NullLogger.
Command UI updates to use term API
v3/internal/commands/sign.go, v3/internal/commands/signing_setup.go, v3/internal/commands/appimage.go, v3/internal/commands/capabilities.go, v3/internal/commands/entitlements_setup.go, v3/internal/commands/init.go, v3/internal/commands/update_cli.go, v3/internal/templates/templates.go
Replaced pterm calls with term equivalents (Section/HeaderTable/Info/Success/Warning/Confirm/StartSpinner/StopSpinner) across signing, AppImage, init, update, templates, and related flows.
Top-level integration and doctor
v3/cmd/wails3/main.go, v3/internal/doctor/doctor.go
Main uses term.Error on app.Run failures; doctor.Run uses term spinners and term.Table-driven output for system, build, dependencies, and diagnostics.
Test updates for term logging
v3/internal/generator/analyse_test.go
Replaced pterm logging with term.Warning in tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wailsapp/wails#4023: Related terminal/spinner API updates and GenerateBindings integration with the internal term package.

Suggested reviewers

  • leaanthony

Poem

🐰 I swapped bright pterm lights for tidy ANSI cheer,
glyph spinners hum and tables now appear.
Commands speak with color, prompts ask with grace,
a rabbit hops along — a cleaner CLI place.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides comprehensive context including design language, detailed file-level changes, and dependency updates. However, it is missing critical elements from the template: no linked issue number, no type-of-change checkbox selection, no testing details, and no checklist completion. Add 'Fixes #<issue_number>', complete the type-of-change and testing checkboxes, include test configuration output from 'wails doctor', and verify all checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 39.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(v3/cli): replace pterm with glyph + direct ANSI — full TUI redesign' clearly and specifically summarizes the main change: replacing the pterm dependency with a custom terminal UI layer using glyph and ANSI codes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.1)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
v3/internal/term/term.go (1)

390-393: 💤 Low value

Hyperlink embeds ANSI codes regardless of color settings.

The Hyperlink function directly embeds ansiUnder and ansiReset in the OSC 8 sequence. When colourEnabled is false, the hyperlink will still contain underline/reset codes, which may produce unexpected output in environments where ANSI codes should be stripped.

Consider whether the underline styling should respect the colourEnabled flag, similar to the col() helper.

♻️ Proposed fix to respect colourEnabled
 func Hyperlink(url, text string) string {
+	if !colourEnabled {
+		return text
+	}
 	return fmt.Sprintf("\x1b]8;;%s\x1b\\%s%s%s\x1b]8;;\x1b\\",
 		url, ansiUnder, text, ansiReset)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/term/term.go` around lines 390 - 393, The Hyperlink function
always injects ansiUnder and ansiReset into the OSC 8 sequence even when colour
output is disabled; change Hyperlink to respect the existing colourEnabled
behavior (like the col() helper) by conditionally using the underline/reset
sequences only when colourEnabled is true (or by calling col(ansiUnder) /
col(ansiReset) or equivalent), so that when colourEnabled is false the hyperlink
contains no ANSI underline/reset codes; update the function that constructs the
OSC 8 string (Hyperlink) to reference ansiUnder, ansiReset and colourEnabled
accordingly.
v3/internal/doctor/doctor.go (1)

36-42: ⚡ Quick win

Consider using spinner.Success() for consistency.

The current pattern uses term.StopSpinner(spinner) both in the defer (line 40) and after successful completion (line 90). While functional, this differs from the pattern in update_cli.go which uses spinner.Success(message) and spinner.Fail(message) to clearly indicate the outcome.

Using spinner.Success("System scan complete") on line 90 would provide better user feedback and align with the spinner lifecycle pattern used elsewhere in the codebase.

♻️ Refactor to use spinner.Success()
 	spinner := term.StartSpinner("Scanning system - Please wait (this may take a long time)…")
 
 	defer func() {
 		if err != nil {
-			term.StopSpinner(spinner)
+			spinner.Fail("System scan failed")
 		}
 	}()
 
 	...
 
-	term.StopSpinner(spinner)
+	spinner.Success("System scan complete")

Also applies to: 62-63, 90-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/doctor/doctor.go` around lines 36 - 42, Replace the manual
term.StopSpinner(spinner) calls with the spinner lifecycle helpers: after
starting the spinner via term.StartSpinner (assigned to variable spinner), call
spinner.Fail("message") in the error defer path and spinner.Success("System scan
complete") on successful completion where term.StopSpinner(spinner) is currently
used; update the defer to call spinner.Fail(...) when err != nil and change the
success branch (currently calling term.StopSpinner(spinner)) to
spinner.Success("System scan complete") so the doctor.go spinner usage matches
update_cli.go's pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/internal/commands/appimage.go`:
- Around line 31-32: The spinner is unconditionally stopped via defer
term.StopSpinner(spinner); instead, update the generateAppImage call site to
capture its error and call spinner.Fail("...") when generateAppImage returns an
error and spinner.Success("...") on success (using the same spinner variable
returned from term.StartSpinner), then stop the spinner appropriately (either
rely on spinner.Success/Fail to finalize or call term.StopSpinner(spinner) after
those calls); remove the unconditional defer and follow the pattern used in
update_cli.go so the final spinner state reflects success or failure.

In `@v3/internal/term/term.go`:
- Around line 21-25: The package-level booleans colourEnabled, outputEnabled,
and debugEnabled are unprotected and cause data races; introduce a sync.RWMutex
(e.g., stateMu) and use stateMu.RLock()/RUnlock() for all reads (col(), the
message functions that check output/debug, spinnerLogger.Debugf()) and
stateMu.Lock()/Unlock() for all writes (the toggle functions that set
colourEnabled/outputEnabled/debugEnabled); update all referenced
functions/locations (col, the various message printing helpers,
spinnerLogger.Debugf, and the toggle functions) to use the mutex to ensure safe
concurrent access.

---

Nitpick comments:
In `@v3/internal/doctor/doctor.go`:
- Around line 36-42: Replace the manual term.StopSpinner(spinner) calls with the
spinner lifecycle helpers: after starting the spinner via term.StartSpinner
(assigned to variable spinner), call spinner.Fail("message") in the error defer
path and spinner.Success("System scan complete") on successful completion where
term.StopSpinner(spinner) is currently used; update the defer to call
spinner.Fail(...) when err != nil and change the success branch (currently
calling term.StopSpinner(spinner)) to spinner.Success("System scan complete") so
the doctor.go spinner usage matches update_cli.go's pattern.

In `@v3/internal/term/term.go`:
- Around line 390-393: The Hyperlink function always injects ansiUnder and
ansiReset into the OSC 8 sequence even when colour output is disabled; change
Hyperlink to respect the existing colourEnabled behavior (like the col() helper)
by conditionally using the underline/reset sequences only when colourEnabled is
true (or by calling col(ansiUnder) / col(ansiReset) or equivalent), so that when
colourEnabled is false the hyperlink contains no ANSI underline/reset codes;
update the function that constructs the OSC 8 string (Hyperlink) to reference
ansiUnder, ansiReset and colourEnabled accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ee67795e-44bc-48fa-8344-2444cab64592

📥 Commits

Reviewing files that changed from the base of the PR and between bfe3e65 and 5340f7c.

📒 Files selected for processing (14)
  • v3/cmd/wails3/main.go
  • v3/go.mod
  • v3/internal/commands/appimage.go
  • v3/internal/commands/capabilities.go
  • v3/internal/commands/entitlements_setup.go
  • v3/internal/commands/init.go
  • v3/internal/commands/sign.go
  • v3/internal/commands/signing_setup.go
  • v3/internal/commands/update_cli.go
  • v3/internal/doctor/doctor.go
  • v3/internal/generator/analyse_test.go
  • v3/internal/generator/config/log.go
  • v3/internal/templates/templates.go
  • v3/internal/term/term.go

Comment on lines +31 to +32
spinner := term.StartSpinner("Generating AppImage…")
defer term.StopSpinner(spinner)

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use spinner.Success() and spinner.Fail() for proper status indication.

The defer unconditionally calls term.StopSpinner(spinner) regardless of success or failure. Based on the pattern in update_cli.go (lines 160-165), the spinner should signal its final state:

  • Call spinner.Fail(message) when generateAppImage returns an error
  • Call spinner.Success(message) when the operation completes successfully

This provides better UX by showing users whether the AppImage generation succeeded or failed.

♻️ Refactor to use spinner success/fail methods
-	spinner := term.StartSpinner("Generating AppImage…")
-	defer term.StopSpinner(spinner)
+	spinner := term.StartSpinner("Generating AppImage…")

	if options.Binary == "" {
		return fmt.Errorf("binary not provided")
	}
	...
	
	term.Header("AppImage Generator")

-	return generateAppImage(options)
+	err = generateAppImage(options)
+	if err != nil {
+		spinner.Fail("AppImage generation failed")
+		return err
+	}
+	spinner.Success("AppImage generated successfully")
+	return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/commands/appimage.go` around lines 31 - 32, The spinner is
unconditionally stopped via defer term.StopSpinner(spinner); instead, update the
generateAppImage call site to capture its error and call spinner.Fail("...")
when generateAppImage returns an error and spinner.Success("...") on success
(using the same spinner variable returned from term.StartSpinner), then stop the
spinner appropriately (either rely on spinner.Success/Fail to finalize or call
term.StopSpinner(spinner) after those calls); remove the unconditional defer and
follow the pattern used in update_cli.go so the final spinner state reflects
success or failure.

Comment thread v3/internal/term/term.go
Comment on lines +21 to +25
var (
colourEnabled = true
outputEnabled = true
debugEnabled = false
)

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.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: Global state lacks synchronization for concurrent access.

The global variables colourEnabled, outputEnabled, and debugEnabled are accessed and modified without synchronization. These variables are:

  • Read by col() (line 43), all message functions (lines 70+, 98+, 122+), and spinnerLogger.Debugf() (line 379)
  • Modified by toggle functions (lines 58-62)

If any callers use the term package from multiple goroutines (e.g., concurrent spinners, logger from different contexts), this will cause data races and violate Go's memory model.

🔒 Proposed fix: Add mutex protection
 var (
+	stateMu       sync.RWMutex
 	colourEnabled = true
 	outputEnabled = true
 	debugEnabled  = false
 )

Then wrap all reads with stateMu.RLock()/RUnlock() and all writes with stateMu.Lock()/Unlock(). For example:

 func col(code, text string) string {
+	stateMu.RLock()
+	enabled := colourEnabled
+	stateMu.RUnlock()
-	if !colourEnabled {
+	if !enabled {
 		return text
 	}
 	return code + text + ansiReset
 }

Apply similar patterns to all toggle functions and readers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var (
colourEnabled = true
outputEnabled = true
debugEnabled = false
)
var (
stateMu sync.RWMutex
colourEnabled = true
outputEnabled = true
debugEnabled = false
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/internal/term/term.go` around lines 21 - 25, The package-level booleans
colourEnabled, outputEnabled, and debugEnabled are unprotected and cause data
races; introduce a sync.RWMutex (e.g., stateMu) and use
stateMu.RLock()/RUnlock() for all reads (col(), the message functions that check
output/debug, spinnerLogger.Debugf()) and stateMu.Lock()/Unlock() for all writes
(the toggle functions that set colourEnabled/outputEnabled/debugEnabled); update
all referenced functions/locations (col, the various message printing helpers,
spinnerLogger.Debugf, and the toggle functions) to use the mutex to ensure safe
concurrent access.

@taliesin-ai

Copy link
Copy Markdown
Collaborator Author

Superseded by #5412 which adds the 2-row glyph spinner with secondary info line routing.


Taliesin is an AI agent. CC @leaanthony

@leaanthony leaanthony closed this May 11, 2026
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