Skip to content

feat(v2/cli): replace pterm/ctc/gommon with unified glyph TUI + spinners#5403

Draft
taliesin-ai wants to merge 1 commit into
masterfrom
agent/engineer-linux/a97ce245
Draft

feat(v2/cli): replace pterm/ctc/gommon with unified glyph TUI + spinners#5403
taliesin-ai wants to merge 1 commit into
masterfrom
agent/engineer-linux/a97ce245

Conversation

@taliesin-ai
Copy link
Copy Markdown
Collaborator

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

Closes #3374

Summary

Replaces the Wails v2 CLI's five overlapping TUI libraries — pterm, wzshiming/ctc, labstack/gommon/color, gookit/color (indirect) — with a single coherent system:

  • New v2/internal/tui package: direct ANSI escape codes for styled text + glyph's NewInlineApp() for animated spinners in interactive terminals. Auto-detects non-TTY (CI, pipes) via os.Stdout.Stat() and falls back to plain text. Respects NO_COLOR, TERM=dumb, and the existing -nocolor flag.
  • Animated spinners on build, update, init (template install + go mod tidy), and generate commands via tui.WithSpinner().
  • Unified color helpers: tui.Green(), tui.Red(), tui.Section(), tui.Table(), etc. — replacing the scattered per-command colour calls.
  • v2/internal/colour simplified: now delegates to tui for full backward compatibility with callers outside cmd/wails.
  • glamour kept for markdown rendering (release notes).
  • glyph v0.6.0 added as direct dependency (Linux/macOS; Windows support listed as roadmap in glyph's README — no regression since Windows didn't have spinners before either).

Testing

  • go build ./cmd/wails — exits 0 on Linux (Ubuntu 24.04, Go 1.23.4 / 1.26.2)
  • go vet ./cmd/wails — exits 0
  • All CLI commands retain identical flags and output structure; only the animation layer is new

CC @leaanthony

Summary by CodeRabbit

  • Refactor

    • Enhanced terminal output formatting with improved progress indicators and table rendering across CLI commands.
    • Improved color and terminal styling consistency throughout the command-line interface.
  • Chores

    • Updated Go toolchain from 1.25.0 to 1.25.1 and refreshed dependencies to latest versions.

Review Change Stack

Consolidates the CLI's five overlapping TUI libraries (pterm, wzshiming/ctc,
labstack/gommon/color, gookit/color, charmbracelet/glamour) down to a single
coherent system: a new internal/tui package for all styled output plus glyph
for animated spinners on interactive terminals.

- Add v2/internal/tui: ANSI-direct color helpers, NO_COLOR/non-TTY detection,
  and WithSpinner() that wraps glyph's NewInlineApp() for build/init/update/
  generate operations
- Rewrite v2/internal/colour/colour.go to delegate to tui (keeps API compat)
- Remove pterm, wzshiming/ctc, labstack/gommon/color imports from all CLI cmds
- Add github.com/kungfusheep/glyph v0.6.0 as direct dependency
- glamour kept for markdown rendering (release notes)

Co-authored-by: multica-agent <github@multica.ai>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Walkthrough

This PR comprehensively refactors the Wails CLI from pterm-based rendering to an internal tui package, adding spinner animations via the glyph library and standardizing color/styling handling with NO_COLOR support. The old internal/colour package becomes a backward-compatibility wrapper.

Changes

CLI Terminal UI Migration to TUI Package

Layer / File(s) Summary
TUI Package Foundation
v2/internal/tui/tui.go
New terminal UI package exports color helpers, styled message functions (Fatal, Error, Success, Info, Warning), Section/BulletPoint/Table rendering, and WithSpinner for animated progress feedback; ColourEnabled flag respects NO_COLOR environment variable and TTY detection.
Module Dependencies
v2/go.mod
Go version bumped to 1.25.1; adds github.com/kungfusheep/glyph and github.com/kungfusheep/riffkey for spinner support; removes github.com/wzshiming/ctc; upgrades golang.org/x/* packages and go-runewidth.
Backward Compatibility Wrapper
v2/internal/colour/colour.go
Refactored to delegate all color functions to tui equivalents; retains public API and ColourEnabled flag; Col() signature simplified to accept text only; Rainbow() now uses tui color sequence.
CLI Main Entry Point
v2/cmd/wails/main.go
banner(), fatal(), printBulletPoint(), printFooter() rewritten to use tui helpers; version subcommand prints with fmt.Println; app.Run() error path uses tui.Error().
Internal Utilities
v2/cmd/wails/internal/logutils/color-logs.go, v2/pkg/clilogger/clilogger.go
LogGreen, LogRed, LogDarkYellow and CLILogger.Fatal switched from internal/colour to internal/tui for colored output.
Simple Commands: dev, show
v2/cmd/wails/dev.go, v2/cmd/wails/show.go
Color disabling routes through tui.SetNoColour(); dev adjusts quiet-mode banner logic; show prints release notes via fmt.Println().
Spinner Commands: build, generate, update
v2/cmd/wails/build.go, v2/cmd/wails/generate.go, v2/cmd/wails/update.go
Compilation, bindings generation, and installation operations wrapped in tui.WithSpinner(); pterm spinners/error output replaced with tui equivalents; build/generate options display refactored to tui.Section + tui.Table.
Complex Commands: doctor, init
v2/cmd/wails/doctor.go, v2/cmd/wails/init.go
System scanning, dependency analysis, and template initialization refactored to use tui.Section, tui.Table/HeaderTable, and tui.WithSpinner for multi-step workflows; tabular output now rendered via tui instead of pterm box/table formatting.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (CLI)
    participant Cmd as Command Handler
    participant TUI as tui Package
    participant Op as Operation
    
    User->>Cmd: Run build/generate/update
    Cmd->>TUI: WithSpinner("message", fn)
    TUI->>TUI: Initialize spinner animation
    TUI->>Op: Execute fn()
    Op-->>TUI: Success or Error
    alt Operation Succeeds
        TUI->>TUI: Show checkmark
        TUI-->>Cmd: Return nil
        Cmd->>User: Continue/Done
    else Operation Fails
        TUI->>TUI: Show ✗ with error
        TUI-->>Cmd: Return error
        Cmd->>TUI: tui.Error(message)
        Cmd->>User: Exit with error
    end
Loading
sequenceDiagram
    participant Doc as Doctor Command
    participant TUI as tui Package
    participant Shell as Shell/System
    
    Doc->>TUI: Section("Wails Doctor")
    Doc->>TUI: WithSpinner("Scanning system", fn)
    TUI->>Shell: Gather system info
    Shell-->>TUI: Info collected
    Doc->>TUI: Section("Wails Info")
    Doc->>TUI: Table(build info rows)
    Doc->>TUI: Section("Dependencies")
    Doc->>TUI: HeaderTable(dep rows)
    Doc->>TUI: Section("Diagnosis")
    alt All required deps present
        Doc->>TUI: Success("Ready to go!")
    else Missing deps
        Doc->>TUI: Warning("Missing dependencies")
    end
    Doc->>TUI: Println("Installation instructions")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

size:XXL, lgtm

Suggested reviewers

  • leaanthony
  • ndianabasi

Poem

🐰 Terminal colors dance anew,
From pterm's ways to shiny tui,
With spinners spinning, tables bright,
And NO_COLOR respecting right,
The CLI's dressed in better clothes,
A refactor that majestically flows!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue #3374 requests macOS app activation policy runtime control, but this PR implements CLI TUI refactoring. The PR objectives and code changes have no relation to the linked issue requirements. Verify the correct issue is linked. This PR appears to address CLI TUI consolidation, not macOS app activation policy features. Link the appropriate issue or clarify the relationship.
Docstring Coverage ⚠️ Warning Docstring coverage is 44.00% 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 accurately describes the main change: replacing multiple TUI libraries (pterm/ctc/gommon) with unified glyph-based TUI package.
Description check ✅ Passed The PR description provides a clear summary of changes, issue reference, testing notes, and context about the new tui package and spinner implementation.
Out of Scope Changes check ✅ Passed The PR scope is well-focused on CLI TUI library consolidation with comprehensive changes across cmd/wails files, internal/tui, internal/colour, and go.mod updates. All changes directly support the stated objective.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/engineer-linux/a97ce245

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
v2/go.mod (1)

23-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

go mod tidy must be run — direct dependencies do not match imports, and manifest is out of order.

The repository still imports pterm in v2/pkg/commands/build/base.go and v2/pkg/commands/build/build.go, yet the PR description claims this dependency was replaced. Either the claimed migration was not completed, or the description is inaccurate.

Additionally, labstack/gommon remains in go.mod (line 23) but has no corresponding imports in v2/, indicating the file is out of sync with actual usage.

The out-of-order placement of github.com/kungfusheep/glyph v0.6.0 (line 41, which should appear alphabetically after github.com/goreleaser/goreleaser) is a clear symptom that go mod tidy was not run after editing the manifest.

Run go mod tidy to resolve the manifest, then clarify whether the PR description accurately reflects the intended dependency changes.

🤖 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 `@v2/go.mod` at line 23, The go.mod is out of sync with imports (unused
labstack/gommon entry, pterm still imported in v2/pkg/commands/build/base.go and
v2/pkg/commands/build/build.go) and entries are not ordered; run "go mod tidy"
in the v2 module to regenerate the manifest and sort dependencies, verify and
remove any unused entries (e.g., github.com/labstack/gommon) and ensure pterm
was actually removed or update the PR description if pterm is still used; after
tidying, run `go list -m all` or `grep` those packages in v2/ to confirm the
dependency graph matches the code.
v2/cmd/wails/dev.go (1)

12-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quiet mode may leak output via logutils calls that bypass the muted logger.

Previously, pterm.DisableOutput() silenced all output globally. Now --quiet only mutes the clilogger and skips the banner. Code in dev.Application that uses logutils.LogRed(), logutils.LogDarkYellow(), etc., calls println(tui.Red(...)) directly, which bypasses the muted logger and respects only the ColourEnabled flag (controlled by --nocolor, not --quiet). If --quiet is expected to fully silence dev server bootstrap output, these call sites should either route through the (muted) logger or check a separate quiet flag in tui/logutils.

🤖 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 `@v2/cmd/wails/dev.go` around lines 12 - 33, Quiet mode currently only mutes
clilogger in devApplication but dev.Application still emits output via
logutils/tui (e.g., logutils.LogRed(), logutils.LogDarkYellow(),
println(tui.Red(...))) which bypasses the muted logger; update the bootstrap so
dev.Application's startup messages respect --quiet by either routing those
messages through the created logger (use logger.Info/Error/Warn instead of
direct logutils/tui printlns) or by adding and checking a quiet flag in
tui/logutils (e.g., tui.IsQuiet or logutils.Muted) before printing; change calls
in dev.Application (and any helper that calls tui.Red/println) to use the logger
or check the new quiet flag so --quiet fully silences output.
v2/cmd/wails/doctor.go (1)

197-210: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

externalPackages is collected but unused — either render or remove the collection.

externalPackages is populated at line 198 for dependencies marked External (packages the system package manager cannot install, such as npm or NSIS), but the slice is never used and is explicitly discarded at line 210 with _ = externalPackages. This creates dead code that accumulates unused data.

Either render an External Packages section in the Dependencies output (similar to how optional dependencies are flagged with *), or remove the collection logic entirely if external packages are not meant to be surfaced to the user.

🤖 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 `@v2/cmd/wails/doctor.go` around lines 197 - 210, externalPackages is populated
when dependency.External is true but never used; either drop the collection or
surface it to the user. To fix, either remove the externalPackages slice and the
block that appends to it (and delete the `_ = externalPackages` line) to
eliminate dead code, or render an "External Packages" section after
tui.HeaderTable by checking len(externalPackages)>0 and printing a brief header
and each external package's name and version (use the same dependency fields as
used for dependenciesTableData) so the information collected by externalPackages
is displayed to users.
🧹 Nitpick comments (4)
v2/cmd/wails/generate.go (1)

26-29: ⚡ Quick win

Dead logger setup left over from the migration — drop it (and the _ = logger discard).

In generateModule (Line 26-28), logger is constructed and muted but never written to. In generateTemplate (Line 67-69), the same pattern appears, followed by an explicit _ = logger to silence the compiler — a strong tell that the migration removed the only call sites and the setup wasn't cleaned up. Either remove the dead code or actually route progress messages through the (muted) logger so --quiet continues to suppress them.

🧹 Suggested cleanup
 func generateModule(f *flags.GenerateModule) error {
 	if f.NoColour {
 		tui.SetNoColour()
 	}

-	quiet := f.Verbosity == flags.Quiet
-	logger := clilogger.New(os.Stdout)
-	logger.Mute(quiet)
-
 	buildTags, err := buildtags.Parse(f.Tags)
 func generateTemplate(f *flags.GenerateTemplate) error {
 	if f.NoColour {
 		tui.SetNoColour()
 	}

-	quiet := f.Quiet
-	logger := clilogger.New(os.Stdout)
-	logger.Mute(quiet)
-	_ = logger
-
 	if f.Name == "" {

If you'd rather respect --quiet here, keep the logger and replace the tui.Section / tui.BulletPoint / tui.Info calls with logger.Println(...) so that mute takes effect.

Also applies to: 66-69

🤖 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 `@v2/cmd/wails/generate.go` around lines 26 - 29, The logger created in
generateModule and generateTemplate (logger := clilogger.New(os.Stdout);
logger.Mute(quiet)) is dead code: remove the unused logger variable and the `_ =
logger` discard if you don't intend to route output through it, or alternatively
replace the tui.Section / tui.BulletPoint / tui.Info calls in those functions
with logger.Println/logger.Printf so --quiet (f.Verbosity == flags.Quiet)
actually mutes progress output; update generateModule and generateTemplate
accordingly and delete the unused logger setup to eliminate the compiler
discard.
v2/internal/tui/tui.go (1)

108-177: ⚡ Quick win

Column-width math uses byte length; will misalign with ANSI / wide runes.

Both Table (Line 114, 127, 131) and HeaderTable (Line 146, 157) measure width with len(cell). Two failure modes:

  • Callers passing colored cells (tui.Green("OK") etc.) will have ANSI escapes counted in the width, producing visibly skewed columns.
  • East-Asian / emoji / combining characters render at a different terminal width than their byte length.

go-runewidth is already an indirect dep (v0.0.19), so this is a cheap fix if/when the alignment becomes user-visible. Today, all in-repo callers appear to pass plain ASCII strings, so this is preventive rather than user-visible — flagging only because the helpers are now public API for the whole CLI.

🔧 Sketch
import "github.com/mattn/go-runewidth"

// helper: visible width, ignoring ANSI SGR escapes.
func visualWidth(s string) int {
    // strip CSI sequences first if cells may contain ANSI...
    return runewidth.StringWidth(stripansi.Strip(s))
}

Then replace len(cell)/len(key) with visualWidth(...) and adjust the strings.Repeat(" ", …) calculations accordingly.

🤖 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 `@v2/internal/tui/tui.go` around lines 108 - 177, Replace byte-length
measurements with a visual-width helper and strip ANSI before measuring: add
imports "github.com/mattn/go-runewidth" and "github.com/acarl005/stripansi" and
implement visualWidth(s string) int { return
runewidth.StringWidth(stripansi.Strip(s)) }. In Table, replace
len(key)/len(row[0]) and len(key) uses with visualWidth(key) and use
strings.Repeat(" ", maxKey-visualWidth(key)) when padding (don't rely on fmt's
%-*s). In HeaderTable, replace len(cell) and widths computation with
visualWidth(cell), compute padded := cell + strings.Repeat(" ",
w-visualWidth(cell)), and build the separator using runewidth-derived widths.
Update all occurrences in functions Table and HeaderTable to use visualWidth so
ANSI escapes and wide runes are handled correctly.
v2/internal/colour/colour.go (2)

13-32: ⚡ Quick win

ColourEnabled is no longer honored by the per-color helpers, only by Rainbow.

The compatibility shim documents ColourEnabled as the legacy toggle, but Yellow/Red/Green/.../DarkX all unconditionally delegate to tui, so callers that still do colour.ColourEnabled = false (without also touching tui) will keep getting ANSI sequences. Only Rainbow (lines 38-48) checks the flag, which is an inconsistent contract for a "backward-compatible wrapper."

Either gate every helper on ColourEnabled, or make ColourEnabled a write-through that calls tui.SetNoColour() so the legacy and new toggles stay in sync.

♻️ Option A: gate every helper on the local flag
-func Yellow(text string) string  { return tui.Yellow(text) }
-func Red(text string) string     { return tui.Red(text) }
-func Blue(text string) string    { return tui.Blue(text) }
-func Green(text string) string   { return tui.Green(text) }
-func Cyan(text string) string    { return tui.Cyan(text) }
-func Magenta(text string) string { return tui.Magenta(text) }
-func White(text string) string   { return tui.White(text) }
-func Black(text string) string   { return tui.Black(text) }
+func apply(fn func(string) string, text string) string {
+	if !ColourEnabled {
+		return text
+	}
+	return fn(text)
+}
+
+func Yellow(text string) string  { return apply(tui.Yellow, text) }
+func Red(text string) string     { return apply(tui.Red, text) }
+func Blue(text string) string    { return apply(tui.Blue, text) }
+func Green(text string) string   { return apply(tui.Green, text) }
+func Cyan(text string) string    { return apply(tui.Cyan, text) }
+func Magenta(text string) string { return apply(tui.Magenta, text) }
+func White(text string) string   { return apply(tui.White, text) }
+func Black(text string) string   { return apply(tui.Black, text) }

(apply the same pattern to the Dark* helpers)

♻️ Option B: make `ColourEnabled` a setter that mirrors `tui`

Replace the var ColourEnabled = true with a getter/setter pair (or a small SetColourEnabled(bool) helper) that calls tui.SetNoColour() so legacy callers that toggle the package-level flag stay in sync with tui.ColourEnabled.

🤖 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 `@v2/internal/colour/colour.go` around lines 13 - 32, The per-color helpers
(Yellow, Red, Green, Blue, Cyan, Magenta, White, Black and the Dark* variants
like DarkGreen) currently ignore the legacy ColourEnabled flag while Rainbow
checks it; either make every helper respect ColourEnabled by gating their return
on that flag, or convert the package-level var into a write-through setter
(e.g., replace var ColourEnabled with a SetColourEnabled(bool) or a
getter/setter pair) that calls tui.SetNoColour(!enabled) so toggling
ColourEnabled stays in sync with the tui backend; update all helper functions
referenced above (and Rainbow if needed) to use the chosen approach.

15-15: ⚡ Quick win

Col is now a no-op that returns text unmodified; clarify intent and consistency.

The current implementation of Col simply returns text without any color transformation, differing from other color functions. Since this is an internal package with no callers found in the codebase, the breaking change concern does not have practical impact.

However, the inconsistency with ColourEnabled remains: all color functions in this package (including Col) delegate to the tui package unconditionally and ignore the local ColourEnabled variable set for backward compatibility. If Col is intended to be a retained public function, either implement it with color support that respects ColourEnabled, or clearly document that it is a deprecated no-op.

🤖 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 `@v2/internal/colour/colour.go` at line 15, The Col function is currently a
no-op and inconsistent with other color helpers and the ColourEnabled flag;
update Col to behave like the other color functions by delegating to the tui
color helper while observing the local ColourEnabled boolean, or explicitly mark
it deprecated in its comment/signature. Concretely, change the Col
implementation to check ColourEnabled and return text unchanged when false,
otherwise call the equivalent tui color function (same pattern as other helpers
that reference tui) and add/update a short docstring stating its deprecation or
intended behavior; ensure you reference the Col function and the ColourEnabled
variable so the change mirrors existing helper implementations.
🤖 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 `@v2/cmd/wails/build.go`:
- Around line 223-248: The spinner is racing with build output because
tui.WithSpinner redraws while build.Build writes to buildOptions.Logger
(os.Stdout) — fix by not running the spinner when build output is unmuted: in
the DryRun/real build block, check verbosity (or whether buildOptions.Logger is
os.Stdout) and if not Quiet, replace the WithSpinner call with a simple status
emit (e.g., tui.Section or tui.Info stating "Compiling PLATFORM/ARCH...") and
then call build.Build directly; alternatively, add a boolean flag to
tui.WithSpinner (e.g., pauseRedraw bool) and set it to true when
buildOptions.Logger is not Quiet so the spinner stops redrawing while the
closure runs; update references in this block: WithSpinner, build.Build,
buildOptions.Logger, and the status emit used.

In `@v2/cmd/wails/init.go`:
- Around line 131-139: The inline spinner races with direct subprocess
stdout/stderr; instead of assigning cmd.Stdout/cmd.Stderr to os.Stdout/os.Stderr
inside the tui.WithSpinner closure (where you call
exec.Command("go","mod","tidy") with cmd.Dir = options.TargetDir and using the
quiet flag), capture both outputs into bytes.Buffer(s) (e.g., var out, errBuf
bytes.Buffer), set cmd.Stdout/ cmd.Stderr to those buffers, run cmd.Run(), then
after WithSpinner returns write the buffered output to os.Stdout/os.Stderr
(respecting the quiet flag) so the spinner animation won't interleave with
command output.

In `@v2/internal/tui/tui.go`:
- Around line 193-253: WithSpinner has races on frame and runErr and lacks
panic-safe cleanup; protect accesses to frame and runErr with the existing mu
(lock around frame++ and any reads glyph may perform, and lock around assigning
runErr and reading it after RunNonInteractive), and make the worker goroutine
panic-safe by deferring a cleanup that sets done=true under mu and calls
app.Stop() (and re-panics if recover() returns a panic). Update the spinner
ticker goroutine to lock mu when incrementing frame and when reading done, and
update the worker goroutine that calls fn() to assign runErr while holding mu
(or lock around assignment) and use defer to guarantee done/app.Stop/recover
behavior.

---

Outside diff comments:
In `@v2/cmd/wails/dev.go`:
- Around line 12-33: Quiet mode currently only mutes clilogger in devApplication
but dev.Application still emits output via logutils/tui (e.g.,
logutils.LogRed(), logutils.LogDarkYellow(), println(tui.Red(...))) which
bypasses the muted logger; update the bootstrap so dev.Application's startup
messages respect --quiet by either routing those messages through the created
logger (use logger.Info/Error/Warn instead of direct logutils/tui printlns) or
by adding and checking a quiet flag in tui/logutils (e.g., tui.IsQuiet or
logutils.Muted) before printing; change calls in dev.Application (and any helper
that calls tui.Red/println) to use the logger or check the new quiet flag so
--quiet fully silences output.

In `@v2/cmd/wails/doctor.go`:
- Around line 197-210: externalPackages is populated when dependency.External is
true but never used; either drop the collection or surface it to the user. To
fix, either remove the externalPackages slice and the block that appends to it
(and delete the `_ = externalPackages` line) to eliminate dead code, or render
an "External Packages" section after tui.HeaderTable by checking
len(externalPackages)>0 and printing a brief header and each external package's
name and version (use the same dependency fields as used for
dependenciesTableData) so the information collected by externalPackages is
displayed to users.

In `@v2/go.mod`:
- Line 23: The go.mod is out of sync with imports (unused labstack/gommon entry,
pterm still imported in v2/pkg/commands/build/base.go and
v2/pkg/commands/build/build.go) and entries are not ordered; run "go mod tidy"
in the v2 module to regenerate the manifest and sort dependencies, verify and
remove any unused entries (e.g., github.com/labstack/gommon) and ensure pterm
was actually removed or update the PR description if pterm is still used; after
tidying, run `go list -m all` or `grep` those packages in v2/ to confirm the
dependency graph matches the code.

---

Nitpick comments:
In `@v2/cmd/wails/generate.go`:
- Around line 26-29: The logger created in generateModule and generateTemplate
(logger := clilogger.New(os.Stdout); logger.Mute(quiet)) is dead code: remove
the unused logger variable and the `_ = logger` discard if you don't intend to
route output through it, or alternatively replace the tui.Section /
tui.BulletPoint / tui.Info calls in those functions with
logger.Println/logger.Printf so --quiet (f.Verbosity == flags.Quiet) actually
mutes progress output; update generateModule and generateTemplate accordingly
and delete the unused logger setup to eliminate the compiler discard.

In `@v2/internal/colour/colour.go`:
- Around line 13-32: The per-color helpers (Yellow, Red, Green, Blue, Cyan,
Magenta, White, Black and the Dark* variants like DarkGreen) currently ignore
the legacy ColourEnabled flag while Rainbow checks it; either make every helper
respect ColourEnabled by gating their return on that flag, or convert the
package-level var into a write-through setter (e.g., replace var ColourEnabled
with a SetColourEnabled(bool) or a getter/setter pair) that calls
tui.SetNoColour(!enabled) so toggling ColourEnabled stays in sync with the tui
backend; update all helper functions referenced above (and Rainbow if needed) to
use the chosen approach.
- Line 15: The Col function is currently a no-op and inconsistent with other
color helpers and the ColourEnabled flag; update Col to behave like the other
color functions by delegating to the tui color helper while observing the local
ColourEnabled boolean, or explicitly mark it deprecated in its
comment/signature. Concretely, change the Col implementation to check
ColourEnabled and return text unchanged when false, otherwise call the
equivalent tui color function (same pattern as other helpers that reference tui)
and add/update a short docstring stating its deprecation or intended behavior;
ensure you reference the Col function and the ColourEnabled variable so the
change mirrors existing helper implementations.

In `@v2/internal/tui/tui.go`:
- Around line 108-177: Replace byte-length measurements with a visual-width
helper and strip ANSI before measuring: add imports
"github.com/mattn/go-runewidth" and "github.com/acarl005/stripansi" and
implement visualWidth(s string) int { return
runewidth.StringWidth(stripansi.Strip(s)) }. In Table, replace
len(key)/len(row[0]) and len(key) uses with visualWidth(key) and use
strings.Repeat(" ", maxKey-visualWidth(key)) when padding (don't rely on fmt's
%-*s). In HeaderTable, replace len(cell) and widths computation with
visualWidth(cell), compute padded := cell + strings.Repeat(" ",
w-visualWidth(cell)), and build the separator using runewidth-derived widths.
Update all occurrences in functions Table and HeaderTable to use visualWidth so
ANSI escapes and wide runes are handled correctly.
🪄 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: 07962f33-02ee-4bf3-8be7-409e05d54277

📥 Commits

Reviewing files that changed from the base of the PR and between d7f2e38 and ddb38f4.

⛔ Files ignored due to path filters (1)
  • v2/go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • v2/cmd/wails/build.go
  • v2/cmd/wails/dev.go
  • v2/cmd/wails/doctor.go
  • v2/cmd/wails/generate.go
  • v2/cmd/wails/init.go
  • v2/cmd/wails/internal/logutils/color-logs.go
  • v2/cmd/wails/main.go
  • v2/cmd/wails/show.go
  • v2/cmd/wails/update.go
  • v2/go.mod
  • v2/internal/colour/colour.go
  • v2/internal/tui/tui.go
  • v2/pkg/clilogger/clilogger.go

Comment thread v2/cmd/wails/build.go
Comment on lines 223 to 248
if !f.DryRun {
// Start Time
start := time.Now()

compiledBinary, err := build.Build(buildOptions)
var compiledBinary string
err := tui.WithSpinner(
fmt.Sprintf("Compiling %s/%s", buildOptions.Platform, buildOptions.Arch),
func() error {
var buildErr error
compiledBinary, buildErr = build.Build(buildOptions)
return buildErr
},
)
if err != nil {
pterm.Error.Println(err.Error())
tui.Error(err.Error())
targetErr = err
return
}

buildOptions.IgnoreFrontend = true
buildOptions.CleanBinDirectory = false

// Output stats
buildOptions.Logger.Println("%s", fmt.Sprintf("Built '%s' in %s.\n", compiledBinary, time.Since(start).Round(time.Millisecond).String()))

outputBinaries[buildOptions.Platform+"/"+buildOptions.Arch] = compiledBinary
} else {
pterm.Info.Println("Dry run: skipped build.")
tui.Info("Dry run: skipped build.")
}
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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -e go . v2/internal/tui | xargs -r rg -nP -C5 '\bWithSpinner\b|NewInlineApp|\bRun\(|Pause|Suspend|Stop'

Repository: wailsapp/wails

Length of output: 1092


🏁 Script executed:

#!/bin/bash
# Find and read the complete WithSpinner function
fd -t f -e go . v2/internal/tui | head -5 | while read f; do
  if rg -q "func WithSpinner" "$f"; then
    echo "=== Found in: $f ==="
    # Get line number and read surrounding context
    line=$(rg -n "func WithSpinner" "$f" | cut -d: -f1)
    start=$((line - 5))
    [ $start -lt 1 ] && start=1
    end=$((line + 80))
    sed -n "${start},${end}p" "$f"
  fi
done

Repository: wailsapp/wails

Length of output: 2113


🏁 Script executed:

#!/bin/bash
# Look for glyph imports and understand what RunNonInteractive does
rg -A 20 "RunNonInteractive" v2/internal/tui/tui.go | head -30

Repository: wailsapp/wails

Length of output: 703


Spinner redraws race with build.Build's logger output on non-quiet verbosity.

The spinner animation loop in WithSpinner continuously redraws every 80ms via app.RequestRender() while fn() executes concurrently. Since buildOptions.Logger (initialized with os.Stdout) is unmuted when verbosity is not Quiet, the build pipeline's output and the spinner's terminal state management write to os.Stdout without synchronization. This causes visible artifacts: torn spinner frames, duplicated output, and misaligned text interleaved in interactive terminals.

Two reasonable options:

  • Only show the spinner when quiet is true (the only case where stdout is silent during the closure), and otherwise print a tui.Section or status line.
  • Have tui.WithSpinner pause spinner redraws while the closure runs, relying on the build's own progress output.
🤖 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 `@v2/cmd/wails/build.go` around lines 223 - 248, The spinner is racing with
build output because tui.WithSpinner redraws while build.Build writes to
buildOptions.Logger (os.Stdout) — fix by not running the spinner when build
output is unmuted: in the DryRun/real build block, check verbosity (or whether
buildOptions.Logger is os.Stdout) and if not Quiet, replace the WithSpinner call
with a simple status emit (e.g., tui.Section or tui.Info stating "Compiling
PLATFORM/ARCH...") and then call build.Build directly; alternatively, add a
boolean flag to tui.WithSpinner (e.g., pauseRedraw bool) and set it to true when
buildOptions.Logger is not Quiet so the spinner stops redrawing while the
closure runs; update references in this block: WithSpinner, build.Build,
buildOptions.Logger, and the status emit used.

Comment thread v2/cmd/wails/init.go
Comment on lines +131 to +139
err = tui.WithSpinner("Running go mod tidy", func() error {
cmd := exec.Command("go", "mod", "tidy")
cmd.Dir = options.TargetDir
cmd.Stderr = os.Stderr
if !quiet {
cmd.Stdout = os.Stdout
}
return cmd.Run()
})
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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'spinner|tui' v2/internal/tui | xargs -r rg -n -C5 'WithSpinner|InlineApp|os\.Stdout|Pause|Suspend' 2>/dev/null
ast-grep --pattern $'func WithSpinner($_, $_) $_ {
  $$$
}'

Repository: wailsapp/wails

Length of output: 3807


🏁 Script executed:

# Check how WithSpinner is used elsewhere and whether output is typically expected
rg -n 'WithSpinner' v2/cmd/wails/ -A 5 -B 2

Repository: wailsapp/wails

Length of output: 2860


🏁 Script executed:

# Look for any stderr/stdout handling patterns in the tui module
fd -t f . v2/internal/tui | xargs rg -n 'Stdout|Stderr|Run|Command' -B 2 -A 2

Repository: wailsapp/wails

Length of output: 509


🏁 Script executed:

# Check if there are any comments or issues about spinner output corruption
rg -n 'spinner|output|corrupt|interleave' v2/ -i --max-count=20

Repository: wailsapp/wails

Length of output: 29095


🏁 Script executed:

# Check shell.RunCommand implementation
fd -t f 'shell.go' v2/ | xargs rg -n 'func RunCommand' -A 15

Repository: wailsapp/wails

Length of output: 1445


🏁 Script executed:

# Search for go mod tidy behavior and whether output is expected
rg -n 'go mod tidy' v2/ -B 3 -A 3

Repository: wailsapp/wails

Length of output: 1549


🏁 Script executed:

# Check if there are TODOs or comments about spinner and output
rg -n 'TODO|FIXME|BUG|XXX' v2/internal/tui/tui.go

Repository: wailsapp/wails

Length of output: 40


🏁 Script executed:

# Get full context of update.go WithSpinner usage
sed -n '120,135p' v2/cmd/wails/update.go

Repository: wailsapp/wails

Length of output: 497


🏁 Script executed:

# Also check the full context of init.go around lines 131-139
sed -n '128,145p' v2/cmd/wails/init.go

Repository: wailsapp/wails

Length of output: 450


Buffer go mod tidy output to prevent TTY corruption during inline spinner animation.

The closure attaches cmd.Stdout = os.Stdout and cmd.Stderr = os.Stderr while WithSpinner actively animates using glyph.NewInlineApp() (inline rendering with carriage returns). The concurrent spinner redraw goroutine and the subprocess output will race for TTY writes, interleaving partial spinner glyphs with command output.

The cmd.Stderr is unconditionally attached (line 134), so even quiet mode is affected when go mod tidy downloads modules. Note that update.go (line 122) uses shell.RunCommand() to capture output into buffers and displays it after the spinner stops—this is the correct pattern for use with WithSpinner.

Recommended fix: capture output into bytes.Buffer inside the closure and write after the spinner completes.

🛠️ Suggested fix: buffer stdio, flush after spinner
 	if !f.CIMode {
-		err = tui.WithSpinner("Running go mod tidy", func() error {
-			cmd := exec.Command("go", "mod", "tidy")
-			cmd.Dir = options.TargetDir
-			cmd.Stderr = os.Stderr
-			if !quiet {
-				cmd.Stdout = os.Stdout
-			}
-			return cmd.Run()
-		})
+		var tidyOut, tidyErr bytes.Buffer
+		err = tui.WithSpinner("Running go mod tidy", func() error {
+			cmd := exec.Command("go", "mod", "tidy")
+			cmd.Dir = options.TargetDir
+			cmd.Stderr = &tidyErr
+			cmd.Stdout = &tidyOut
+			return cmd.Run()
+		})
+		if !quiet && tidyOut.Len() > 0 {
+			os.Stdout.Write(tidyOut.Bytes())
+		}
+		if tidyErr.Len() > 0 {
+			os.Stderr.Write(tidyErr.Bytes())
+		}
 		if err != nil {
 			return err
 		}

(remember to add the bytes import)

🤖 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 `@v2/cmd/wails/init.go` around lines 131 - 139, The inline spinner races with
direct subprocess stdout/stderr; instead of assigning cmd.Stdout/cmd.Stderr to
os.Stdout/os.Stderr inside the tui.WithSpinner closure (where you call
exec.Command("go","mod","tidy") with cmd.Dir = options.TargetDir and using the
quiet flag), capture both outputs into bytes.Buffer(s) (e.g., var out, errBuf
bytes.Buffer), set cmd.Stdout/ cmd.Stderr to those buffers, run cmd.Run(), then
after WithSpinner returns write the buffered output to os.Stdout/os.Stderr
(respecting the quiet flag) so the spinner animation won't interleave with
command output.

Comment thread v2/internal/tui/tui.go
Comment on lines +193 to +253
func WithSpinner(message string, fn func() error) error {
if !ColourEnabled {
fmt.Println(message + "...")
err := fn()
if err != nil {
fmt.Println(" Failed: " + err.Error())
} else {
fmt.Println(" Done.")
}
return err
}

var (
done bool
runErr error
mu sync.Mutex
frame int
)

displayMsg := message
app := glyph.NewInlineApp()
app.Height(1)
app.SetView(
glyph.HBox(
glyph.Spinner(&frame).Frames(glyph.SpinnerBraille).FG(glyph.Cyan),
glyph.SpaceW(1),
glyph.Text(&displayMsg),
),
)

go func() {
for {
mu.Lock()
d := done
mu.Unlock()
if d {
return
}
frame++
app.RequestRender()
time.Sleep(80 * time.Millisecond)
}
}()

go func() {
runErr = fn()
mu.Lock()
done = true
mu.Unlock()
app.Stop()
}()

_ = app.RunNonInteractive()

if runErr != nil {
fmt.Println(ansi("91", " ✗") + " " + message)
} else {
fmt.Println(ansi("92", " ✓") + " " + message)
}
return runErr
}
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 | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does github.com/kungfusheep/glyph v0.6.0 support passing *atomic.Int32 (or similar) to Spinner(...), or does it strictly require *int? Is there a recommended pattern for driving a glyph Spinner's frame from a separate goroutine without races?

💡 Result:

No, github.com/kungfusheep/glyph v0.6.0 does not explicitly document support for passing *atomic.Int32 (or similar atomic types) directly to Spinner(...). The Spinner component expects a frame pointer of type *int, as indicated by the opSpinner struct in the template compiler which defines framePtr *int [1]. Glyph's general binding pattern is to pass pointers to data (*int, *float64, etc.), which glyph reads atomically during render passes [2]. Atomic types like *atomic.Int32 are not directly compatible since they don't expose a plain int pointer; their Load method must be called to get the value.

Citations:


🏁 Script executed:

# First, let's locate and examine the actual tui.go file
find . -name "tui.go" -type f | head -20

Repository: wailsapp/wails

Length of output: 83


🏁 Script executed:

# Read the WithSpinner function from the file to confirm it matches the snippet
cat -n v2/internal/tui/tui.go | sed -n '193,253p'

Repository: wailsapp/wails

Length of output: 1458


🏁 Script executed:

# Check the imports at the top of the file to understand what's already imported
head -30 v2/internal/tui/tui.go

Repository: wailsapp/wails

Length of output: 822


WithSpinner has three critical concurrency bugs.

  1. Data race on frame (lines 217, 231). The glyph library reads the pointer-passed frame value asynchronously during every render call. The ticker goroutine increments frame++ (line 231) without holding the mutex, creating a race between the goroutine's write and glyph's reads.

  2. Data race on runErr (lines 238, 247). runErr = fn() is written in the worker goroutine without synchronization, and the main goroutine reads it after RunNonInteractive() returns without acquiring the mutex. The mutex protects only done; there is no happens-before guarantee for runErr.

  3. No panic safety (line 238). If fn() panics, the goroutine crashes immediately without executing done = true or app.Stop(). The spinner is left in an inconsistent state with no recovery path. Use defer to ensure cleanup runs even on panic.

All three will be detected by go test -race.

Fix approach: Extend the mutex to protect frame reads and writes, and wrap the worker goroutine with defer to guarantee cleanup:

func WithSpinner(message string, fn func() error) error {
	// ... [no change to the non-TTY branch] ...

	var (
		done   bool
		runErr error
		mu     sync.Mutex
		frame  int
	)

	// ... [setup code unchanged] ...

	go func() {
		for {
			mu.Lock()
			d := done
			mu.Unlock()
			if d {
				return
			}
			mu.Lock()
			frame++
			mu.Unlock()
			app.RequestRender()
			time.Sleep(80 * time.Millisecond)
		}
	}()

	go func() {
		defer func() {
			mu.Lock()
			done = true
			mu.Unlock()
			app.Stop()
			if r := recover(); r != nil {
				panic(r)
			}
		}()
		runErr = fn()
	}()

	// ... [rest unchanged] ...
}

(Note: The suggested fix in the original review uses atomic.Int32, but glyph.Spinner requires *int and does not support atomic types, so a mutex-based approach is necessary.)

🤖 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 `@v2/internal/tui/tui.go` around lines 193 - 253, WithSpinner has races on
frame and runErr and lacks panic-safe cleanup; protect accesses to frame and
runErr with the existing mu (lock around frame++ and any reads glyph may
perform, and lock around assigning runErr and reading it after
RunNonInteractive), and make the worker goroutine panic-safe by deferring a
cleanup that sets done=true under mu and calls app.Stop() (and re-panics if
recover() returns a panic). Update the spinner ticker goroutine to lock mu when
incrementing frame and when reading done, and update the worker goroutine that
calls fn() to assign runErr while holding mu (or lock around assignment) and use
defer to guarantee done/app.Stop/recover behavior.

@leaanthony leaanthony marked this pull request as draft May 11, 2026 11:01
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.

Enable setting the app's activation policy in MacOS

1 participant