Skip to content

Commit 7837e72

Browse files
bomly-guyclaude
andauthored
fix(cli): make startup logo static by default, animate via BOMLY_LOGO opt-in (#371)
User feedback: help must render instantly. The ~2s startup animation now only plays when BOMLY_LOGO is set (truthy value = random variant, variant name pins one), and runs faster when it does (28 frames x 45ms ~= 1.3s, down from 70ms ~= 2s). NO_COLOR / BOMLY_NO_ANIMATION / CI / BOMLY_QUIET keep their meaning and still win over the opt-in. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6a9842a commit 7837e72

4 files changed

Lines changed: 37 additions & 24 deletions

File tree

dev-docs/ARCHITECTURE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -542,12 +542,14 @@ The detect stage resolves subprojects concurrently (`resolveAll` fans out to per
542542
- Each detector's public `ResolveGraph` rebinds `d.Logger = req.DetectorLogger(d.Logger)` on its value-receiver copy, so every private helper inherits the scoped logger with no signature churn and no shared mutable state.
543543
- The console encoder enables `NameKey`/`EncodeName` so the subproject scope renders as a prefix. Logs remain real-time (not buffered per subproject) because `-vv` is used precisely to watch slow or hung detectors.
544544

545-
### Decision: startup banner frames are procedural; gating is env-var-only
545+
### Decision: startup banner frames are procedural; animation is opt-in; gating is env-var-only
546546

547-
The help-path startup banner (`internal/cli/render/logo.go`) is a frame-based animation in the style of GitHub Copilot CLI's banner: the art itself changes per frame. Four variants exist — `reveal` (a boundary sweeps left to right with a scramble band), `rain` (matrix-style green code glyphs fall down the logo silhouette), `glitch` (the full logo is visible from frame one but corrupted, and the noise density decays to zero), and `slide` (rows enter alternately from the left and right edges). A random variant plays each run; `BOMLY_LOGO=<name>` pins one. All variants share the finale (finished art, tagline fading in), the frame budget (28 frames × 70ms ≈ 2s), and the cell/run-grouping renderer. Three choices worth recording:
547+
The help-path startup banner (`internal/cli/render/logo.go`) is a frame-based animation in the style of GitHub Copilot CLI's banner: the art itself changes per frame. Four variants exist — `reveal` (a boundary sweeps left to right with a scramble band), `rain` (matrix-style green code glyphs fall down the logo silhouette), `glitch` (the full logo is visible from frame one but corrupted, and the noise density decays to zero), and `slide` (rows enter alternately from the left and right edges). All variants share the finale (finished art, tagline fading in), the frame budget (28 frames × 45ms ≈ 1.3s), and the cell/run-grouping renderer.
548+
549+
**The animation is opt-in; the default is the static colored logo.** The banner originally animated by default, but user feedback was consistent that a CLI — and the help command especially — must render instantly, so the default flipped to static. `BOMLY_LOGO` is the single opt-in knob: any truthy value animates a random variant, and a variant name (`reveal`, `rain`, `glitch`, `slide`) animates that variant. Three further choices worth recording:
548550

549551
- **Frames are generated procedurally, not stored as files.** Copilot ships ~20 hand-drawn frame files plus a position→color-role→theme mapping layer. At our scale (6×42 cells, a handful of visual roles) that indirection is over-engineering: frames are computed from the final art with a deterministic FNV-1a hash (stable across runs, so tests assert exact frame properties per variant), and styling uses lightweight run-grouped style constants — consecutive same-style cells share one escape sequence. Only variant *selection* is random; frame content is deterministic.
550-
- **Gating is env-var-only (`NO_COLOR`, `BOMLY_NO_ANIMATION`, `CI`, `BOMLY_QUIET`), deliberately not a config key.** Cobra's `execute()` returns `flag.ErrHelp` right after flag parsing, *before* the `PersistentPreRunE` chain where `options.ResolveConfig` runs — so on `bomly --help` / `bomly <cmd> --help` (the banner's primary path) resolved config simply does not exist yet. A `logo.animate` config key would silently work only for bare `bomly` and `bomly help <cmd>`, which is a trap. When animation is gated off but stderr is a TTY, the static final frame prints instead (plain under `NO_COLOR`, colored otherwise); non-TTY stderr prints nothing.
552+
- **Gating is env-var-only (`BOMLY_LOGO` to opt in; `NO_COLOR`, `BOMLY_NO_ANIMATION`, `CI`, `BOMLY_QUIET` to force static), deliberately not a config key.** Cobra's `execute()` returns `flag.ErrHelp` right after flag parsing, *before* the `PersistentPreRunE` chain where `options.ResolveConfig` runs — so on `bomly --help` / `bomly <cmd> --help` (the banner's primary path) resolved config simply does not exist yet. A `logo.animate` config key would silently work only for bare `bomly` and `bomly help <cmd>`, which is a trap. When animation is gated off but stderr is a TTY, the static final frame prints instead (plain under `NO_COLOR`, colored otherwise); non-TTY stderr prints nothing.
551553
- **The animation leaves cursor visibility unchanged.** Hiding the cursor would make the animation slightly cleaner, but a process-level interrupt can bypass deferred cleanup and leave the user's shell cursor hidden. Avoiding that terminal-state mutation keeps interruption safe without introducing signal handling into the render package.
552554

553555
## Build Modes

docs/TROUBLESHOOTING.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,21 +129,21 @@ ANSI escapes are auto-stripped when stdout is not a TTY, but some CI runners rep
129129
NO_COLOR=1 bomly scan
130130
```
131131

132-
## Controlling the startup animation
132+
## Controlling the startup logo
133133

134-
`bomly --help` plays a short (~2s) logo animation when stderr is an interactive terminal. A random variant plays each run; pin a favorite with `BOMLY_LOGO` (`reveal`, `rain`, `glitch`, or `slide`):
134+
`bomly --help` prints a static logo when stderr is an interactive terminal. To play a short (~1.3s) logo animation instead, set `BOMLY_LOGO` to a truthy value — a random variant plays each run:
135135

136136
```bash
137-
BOMLY_LOGO=rain bomly --help
137+
BOMLY_LOGO=1 bomly --help
138138
```
139139

140-
To skip the animation and print a static logo instead:
140+
Pin a favorite variant by name (`reveal`, `rain`, `glitch`, or `slide`):
141141

142142
```bash
143-
BOMLY_NO_ANIMATION=1 bomly --help
143+
BOMLY_LOGO=rain bomly --help
144144
```
145145

146-
The animation is also skipped automatically when `CI` or `BOMLY_QUIET` is set, and `NO_COLOR` switches to a plain uncolored logo. When stderr is not a terminal (pipes, redirects), no logo is printed at all.
146+
The animation is skipped even when `BOMLY_LOGO` is set if `BOMLY_NO_ANIMATION`, `CI`, or `BOMLY_QUIET` is set, and `NO_COLOR` switches to a plain uncolored logo. When stderr is not a terminal (pipes, redirects), no logo is printed at all.
147147

148148
## Need more detail
149149

internal/cli/render/logo.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import (
1212
)
1313

1414
const (
15-
startupLogoFrameDelay = 70 * time.Millisecond
15+
startupLogoFrameDelay = 45 * time.Millisecond
1616

17-
// logoFrameCount is the total animation length (~2s at 70ms per frame);
17+
// logoFrameCount is the total animation length (~1.3s at 45ms per frame);
1818
// the last two frames hold the finished art while the tagline fades in.
1919
logoFrameCount = 28
2020
logoRevealFrameCount = logoFrameCount - 2
@@ -35,11 +35,13 @@ const (
3535
logoStaticPlain
3636
)
3737

38-
// StartupLogo plays a brief Bomly logo animation when w is an attached TTY.
39-
// On non-TTY writers (pipes, files), it is a silent no-op. The animation is
40-
// skipped in favor of a static logo when NO_COLOR (plain), BOMLY_NO_ANIMATION,
41-
// CI, or BOMLY_QUIET (colored) are set. A random animation variant plays each
42-
// run; BOMLY_LOGO pins one by name (reveal, rain, glitch, slide).
38+
// StartupLogo prints the Bomly logo when w is an attached TTY. On non-TTY
39+
// writers (pipes, files), it is a silent no-op. By default the logo is a
40+
// static colored banner so help stays instant; setting BOMLY_LOGO to a truthy
41+
// value opts into a brief animation with a random variant, and setting it to a
42+
// variant name (reveal, rain, glitch, slide) pins that variant. NO_COLOR
43+
// forces a plain static logo, and BOMLY_NO_ANIMATION, CI, or BOMLY_QUIET force
44+
// a static colored logo even when BOMLY_LOGO is set.
4345
func StartupLogo(w io.Writer) {
4446
file, ok := w.(*os.File)
4547
if !ok || file == nil {
@@ -80,15 +82,19 @@ func StartupLogo(w io.Writer) {
8082

8183
// startupLogoMode applies the environment gates. NO_COLOR follows the
8284
// no-color.org convention (any non-empty value); the remaining variables are
83-
// treated as boolean flags via envFlagSet.
85+
// treated as boolean flags via envFlagSet. Animation is opt-in via BOMLY_LOGO;
86+
// the off-switches (BOMLY_NO_ANIMATION, CI, BOMLY_QUIET) win over it.
8487
func startupLogoMode() logoMode {
8588
if os.Getenv("NO_COLOR") != "" {
8689
return logoStaticPlain
8790
}
8891
if envFlagSet("BOMLY_NO_ANIMATION") || envFlagSet("CI") || envFlagSet("BOMLY_QUIET") {
8992
return logoStaticColor
9093
}
91-
return logoAnimate
94+
if envFlagSet("BOMLY_LOGO") {
95+
return logoAnimate
96+
}
97+
return logoStaticColor
9298
}
9399

94100
// envFlagSet reports whether the named environment variable is set to a truthy

internal/cli/render/logo_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import (
77
)
88

99
// clearLogoEnv isolates logo gating tests from the ambient environment (CI
10-
// runners set CI=true, which would flip animate-expected cases).
10+
// runners set CI=true, and a developer's BOMLY_LOGO would flip the default).
1111
func clearLogoEnv(t *testing.T) {
1212
t.Helper()
13-
for _, name := range []string{"NO_COLOR", "BOMLY_NO_ANIMATION", "CI", "BOMLY_QUIET"} {
13+
for _, name := range []string{"NO_COLOR", "BOMLY_NO_ANIMATION", "CI", "BOMLY_QUIET", "BOMLY_LOGO"} {
1414
t.Setenv(name, "")
1515
}
1616
}
@@ -148,14 +148,19 @@ func TestStartupLogoMode(t *testing.T) {
148148
env map[string]string
149149
want logoMode
150150
}{
151-
{name: "default animates", env: nil, want: logoAnimate},
151+
{name: "default colored static", env: nil, want: logoStaticColor},
152+
{name: "logo flag animates", env: map[string]string{"BOMLY_LOGO": "1"}, want: logoAnimate},
153+
{name: "logo variant animates", env: map[string]string{"BOMLY_LOGO": "rain"}, want: logoAnimate},
154+
{name: "logo flag zero stays static", env: map[string]string{"BOMLY_LOGO": "0"}, want: logoStaticColor},
155+
{name: "logo flag false stays static", env: map[string]string{"BOMLY_LOGO": "FALSE"}, want: logoStaticColor},
152156
{name: "NO_COLOR plain static", env: map[string]string{"NO_COLOR": "1"}, want: logoStaticPlain},
153157
{name: "NO_COLOR wins over no-animation", env: map[string]string{"NO_COLOR": "1", "BOMLY_NO_ANIMATION": "1"}, want: logoStaticPlain},
158+
{name: "NO_COLOR wins over logo flag", env: map[string]string{"NO_COLOR": "1", "BOMLY_LOGO": "1"}, want: logoStaticPlain},
154159
{name: "no-animation colored static", env: map[string]string{"BOMLY_NO_ANIMATION": "1"}, want: logoStaticColor},
155-
{name: "no-animation zero animates", env: map[string]string{"BOMLY_NO_ANIMATION": "0"}, want: logoAnimate},
156-
{name: "no-animation false animates", env: map[string]string{"BOMLY_NO_ANIMATION": "FALSE"}, want: logoAnimate},
160+
{name: "no-animation wins over logo flag", env: map[string]string{"BOMLY_NO_ANIMATION": "1", "BOMLY_LOGO": "rain"}, want: logoStaticColor},
161+
{name: "no-animation zero stays static", env: map[string]string{"BOMLY_NO_ANIMATION": "0"}, want: logoStaticColor},
157162
{name: "CI colored static", env: map[string]string{"CI": "true"}, want: logoStaticColor},
158-
{name: "CI zero animates", env: map[string]string{"CI": "0"}, want: logoAnimate},
163+
{name: "CI wins over logo flag", env: map[string]string{"CI": "true", "BOMLY_LOGO": "1"}, want: logoStaticColor},
159164
{name: "quiet colored static", env: map[string]string{"BOMLY_QUIET": "1"}, want: logoStaticColor},
160165
}
161166
for _, tc := range cases {

0 commit comments

Comments
 (0)