Skip to content

feat(ui): scaffold internal/ui with Writer + theme + mode (#74 PR1)#102

Merged
dipto0321 merged 1 commit into
mainfrom
feat/ui/scaffold-poc
Jul 3, 2026
Merged

feat(ui): scaffold internal/ui with Writer + theme + mode (#74 PR1)#102
dipto0321 merged 1 commit into
mainfrom
feat/ui/scaffold-poc

Conversation

@dipto0321

Copy link
Copy Markdown
Owner

Summary

Scaffolds the new internal/ui package and migrates nodeup version
to use it, closing #74 PR1 of 4. PR1 establishes the Writer surface
(theme + mode decision + plain/fancy implementations) so the rest of
the migration has a well-defined dependency to move call-sites to in
PR2 (spinners), PR3 (interactive prompts), and PR4 (the remaining
direct-print sites in upgrade/check/list/packages/config/report).

What this PR does

  1. internal/ui/mode.go — new Mode type
    (PlainMode / FancyMode) and a single DecideMode(noColor bool)
    decision point. The rules: --no-color flag → PlainMode, NO_COLOR
    env → PlainMode, non-TTY stdout/stdin → PlainMode, otherwise
    FancyMode. TTY detection is centralized here (a small
    isTerminal helper around os.File.Stat() checking
    os.ModeCharDevice) so lipgloss's own detection and our gating
    never disagree.
  2. internal/ui/theme.goTheme struct holding 6 lipgloss
    styles (Success, Error, Warning, Info, Dim, Heading)
    and DefaultTheme() constructor. Colors deliberately muted —
    nodeup output is informational, not decorative. The accent
    colors (success green, error red, warning amber) are
    AA-readable against the standard dark terminal background.
  3. internal/ui/writer.goWriter interface with 6 methods
    (Success/Info/Warn/Error/Println/Mode), NewWriter
    constructor, and two implementations: plainWriter (every method
    is a single io.WriteString, no decoration) and fancyWriter
    (each method renders the message with the corresponding
    Theme style). NewWriter forces
    lipgloss.SetColorProfile(termenv.TrueColor) in FancyMode so
    ANSI escapes are emitted even when stdout isn't a real TTY (the
    test-only escape hatch the package's tests rely on).
  4. internal/cli/root.goPersistentPreRunE now resolves
    the active ui.Writer once per invocation (calls
    ui.DecideMode(--no-color) and constructs the right Writer) and
    stashes it on cmd.Context() under an unexported
    writerCtxKey{} key. Subcommands pull it out via a
    writerFromCmd(cmd) helper. warnInterruptedUpgrade now takes
    the writer and routes its hint lines through w.Warn.
  5. internal/cli/version.go — the PoC migration. Every
    fmt.Fprintf(out, ...) becomes w.Println(fmt.Sprintf(...)).
    The byte-level output shape stays identical (version.go calls
    Println which renders raw bytes in both modes), so the
    existing TestVersionCmd_* tests pass unchanged.
  6. internal/cli/version_test.go — added
    TestVersionCmd_UsesInjectedWriter which constructs a full
    NewRootCmd(...) (so PersistentPreRunE runs) and exercises
    nodeup version end-to-end, confirming the Writer is correctly
    threaded and the PlainMode output is byte-clean (no ANSI, no
    glyphs).
  7. CHANGELOG.md — entry under [Unreleased] / ### Added.

What this PR does NOT do (by design)

  • Spinners — bubbletea integration into upgrade.go's
    long-running steps is PR2.
  • Interactive prompts — huh migration for cleanup.go's
    y/N + any manager-picker is PR3.
  • Remaining migrationscheck.go, list.go, packages.go,
    config.go, root.go (the rest), and the final report.go
    summary block move to the new Writer in PR4.
  • bubbletea/huh deps — kept out of go.mod for this PR. They'll
    be re-pulled by go mod tidy in PR2/PR3 when the spinner /
    prompt methods are actually used.

Verification

$ make ci
go mod tidy
gofmt -s -w .
goimports not installed, skipping (run: go install golang.org/x/tools/cmd/goimports@latest)
go vet ./...
golangci-lint run ./...
0 issues.
go test -race -coverprofile=coverage.out -covermode=atomic ./...
	github.com/dipto0321/nodeup/cmd/nodeup		coverage: 0.0% of statements
ok  	github.com/dipto0321/nodeup/internal/cli	1.612s	coverage: 40.1% of statements
ok  	github.com/dipto0321/nodeup/internal/config	(cached)	coverage: 81.0% of statements
ok  	github.com/dipto0321/nodeup/internal/detector	(cached)	coverage: 77.5% of statements
ok  	github.com/dipto0321/nodeup/internal/node	(cached)	coverage: 71.5% of statements
ok  	github.com/dipto0321/nodeup/internal/packages	(cached)	coverage: 37.4% of statements
ok  	github.com/dipto0321/nodeup/internal/platform	(cached)	coverage: 55.6% of statements
ok  	github.com/dipto0321/nodeup/internal/ui	2.097s	coverage: 67.7% of statements
total:									(statements)			62.2%

Coverage went from 59.6% → 62.2% (the new ui package brings 67.7%
on its own, and the cli package climbed from 31.1% to 40.1% because
the new TestVersionCmd_UsesInjectedWriter covers NewRootCmd's
writer-wiring path).

Refs

Establishes the single source of truth for user-facing output
ahead of the larger #74 migration. Four files in a new
internal/ui package:

- mode.go: Mode type (PlainMode / FancyMode) and the single
  DecideMode decision point (--no-color, NO_COLOR env, TTY
  detection for stdout + stdin). Centralizing the TTY check
  here keeps lipgloss's color profile detection and our gating
  in sync.
- theme.go: DefaultTheme with 6 lipgloss styles
  (Success/Error/Warning/Info/Dim/Heading). Muted colors so
  output reads as informational, not decorative.
- writer.go: Writer interface (6 methods), NewWriter
  constructor, plain + fancy implementations. FancyMode
  forces termenv.TrueColor so ANSI escapes emit even when
  stdout is not a TTY (the test-only escape hatch).
- writer_test.go: 5 tests pinning the routing rules
  (stdout vs stderr per level, trailing-newline contract,
  ANSI + glyph presence in FancyMode, NO_COLOR override,
  Mode.String).

Root.go's PersistentPreRunE now resolves the Writer once per
invocation and stashes it on cmd.Context() under an unexported
key. warnInterruptedUpgrade takes the Writer and routes its
hint lines through w.Warn. Subcommands read it back via
writerFromCmd(cmd).

nodeup version is the proof-of-concept migration: every
fmt.Fprintf becomes w.Println(fmt.Sprintf(...)). The byte-level
output shape is identical (Println renders raw bytes in both
modes), so the existing TestVersionCmd_* tests pass unchanged.
A new TestVersionCmd_UsesInjectedWriter exercises the
NewRootCmd wiring end-to-end and pins the PlainMode byte
contract (no ANSI, no glyphs).

Closes #74 (PR1 of 4).
@cocogitto-bot

cocogitto-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

✔️ c207a05 - Conventional commits check succeeded.

@dipto0321 dipto0321 merged commit e37b326 into main Jul 3, 2026
10 checks passed
@dipto0321 dipto0321 deleted the feat/ui/scaffold-poc branch July 3, 2026 07:42
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.

feat(ui): implement internal/ui — colored output, spinners, and interactive prompts via the Charm stack

1 participant