feat(ui): garden mode pacing, seed throttle, and flower colors#10
Conversation
Introduce ui.rain_animation_mode "matrix" with falling half-width glyphs and a green gradient, matching the existing column/drift pattern. Settings TUI lists the new option. Normalize each inner panel line with lipgloss Width so rows with wide emoji cannot exceed the inner cell count and skew rounded borders. Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
Seeds and rain fall under a cloud row; rain waters plots through growth stages to full bloom, then wither cycles spawn new seeds. When plant visuals cover about 80% of the rain strip, rain stops, the sky clears to sun and blue tones, and plants stay in an eternal bloom state. Settings UI lists the new mode; switching animation mode recreates the background so garden state initializes correctly.
Garden mode previously raced through plants because every raindrop that touched the soil row added moisture and lifecycle thresholds were small. Each garden column now accepts at most one rain hit per frame (with a small probabilistic chance of an extra hit so dense rain still reads as soaking), moisture thresholds are larger, and bloom/wither timing has a randomized lifetime so plants don't all die in unison. Dying plants now scatter 2-3 seeds at jittered X positions around the parent column instead of self-replacing, giving the meadow a more organic spread before the storm clears. Tuning is centralized in a new GardenTuning struct on RainBackground. Defaults live in DefaultGardenTuning, and a small set of advanced TOML keys under [ui] (garden_seed_rate, garden_growth_pace, bloom/wither durations, offspring bounds) feed through ResolveGardenTuning. The settings TUI is intentionally left alone; the keys are documented in README and the example config so power users can tweak pacing without cluttering the in-app picker.
Scale storm completion toward wall-clock targets using rain_tick_ms, terminal width, and growth/seed TUI presets. Lower default moisture and seed density; cap sky seeds per refill batch. Throttle flying seeds by count and a stage-based relief curve so young beds stay sparse and mature bloom can carry more seeds. Assign a stable per-column flower tint when a seed first plants; bloom and eternal heads draw from a shared accent palette.
📝 WalkthroughWalkthroughAdds a new "garden" rain animation mode with configuration, UI controls, lifecycle simulation, rendering, and tests. Garden introduces seed vs rain drops, per-column garden plots that progress through planting→sprout→bud→bloom→wither→eternal, offspring spawning, storm completion logic, and tuning parameters applied from config/UI. Changes
Sequence DiagramsequenceDiagram
actor User
participant ConfigView as Config View
participant RepoSelector as RepoSelector Model
participant RainBg as Rain Background
participant Renderer as Frame Renderer
User->>ConfigView: Select "garden" mode + tune parameters
ConfigView->>RepoSelector: Apply config changes
RepoSelector->>RepoSelector: gardenTuningFromConfig(cfg, rainTickMS, width)
RepoSelector->>RainBg: SetGardenTuning(resolved tuning)
RainBg->>RainBg: Initialize garden plots & seed drops
loop Each Frame
RepoSelector->>RainBg: Update()
RainBg->>RainBg: Apply per-column watering caps
RainBg->>RainBg: Advance plant lifecycle stages
RainBg->>RainBg: Spawn offspring on wither
RainBg->>RainBg: Check coverage → set GardenSunny
RepoSelector->>RepoSelector: renderRainWaveStrip(width)
RepoSelector->>Renderer: RenderRainWave(..., gardenSunny)
Renderer->>Renderer: Draw clouds/sun + plant glyphs
Renderer-->>User: Display animated garden
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Keep garden mode, pacing, and tests; integrate matrix subliminal marquee from main (spawn + drop updates, RenderRainWave, matrix_subliminal.go). Resolve config/docs comments and rain_bg_test for four-arg RenderRainWave.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/ui/rain_bg.go (1)
622-680:⚠️ Potential issue | 🟠 MajorConsume garden drops after their first landing.
Line 658 uses
p.Y >= maxDropY, but the drop remains alive until the later cleanup pass. That lets one rain/seed drop apply moisture or seed boost on multiple frames, including after it has moved below the landing band, which bypasses the new pacing/throttle behavior.Proposed fix
- if rb.Mode == config.UIRainAnimationGarden && p.Y >= maxDropY && rb.GardenPlots != nil && p.X >= 0 && p.X < len(rb.GardenPlots) { + if rb.Mode == config.UIRainAnimationGarden && p.Y >= maxDropY && p.Y < rb.Height && rb.GardenPlots != nil && p.X >= 0 && p.X < len(rb.GardenPlots) { g := &rb.GardenPlots[p.X] if p.IsSeed { if g.stage == gardenStageNone { g.stage = gardenStagePlanted @@ } else if rb.Garden.RainAbsorbExtraChance > 0 && rand.Float64() < rb.Garden.RainAbsorbExtraChance { rb.gardenWaterPlot(g) } } + p.Age = p.MaxAge }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/ui/rain_bg.go` around lines 622 - 680, The landing logic uses p.Y >= maxDropY but doesn't remove/consume the drop, so a single drop can apply effects repeatedly; after handling landing effects in both the advanced (rb.Flowers) and garden (rb.GardenPlots) branches (inside the rb.Mode checks), mark the drop as consumed so it won't apply again — e.g., set p.Alive = false (or p.Y = rb.Height / set a consumed flag) immediately after calling rb.gardenWaterPlot or incrementing drops / applying seed boost; update references to p (and gardenWatered) accordingly so subsequent frames skip this drop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/config/types.go`:
- Around line 105-108: The inline comment for GardenOffspringMin and
GardenOffspringMax is stale (mentions "currently 1 and 2") and should be
reconciled with the actual defaults returned by DefaultGardenTuning() and the
README example; update the comment to reflect the real defaults (e.g.,
"currently 2 and 3") or adjust DefaultGardenTuning()/README to match the comment
so they are consistent, ensuring references to GardenOffspringMin and
GardenOffspringMax in defaults.go and README are all aligned.
- Around line 80-83: Update the stale comment above the Garden mode tuning block
to remove the "three rows" claim and reflect reality: mention that this block
defines eight garden tuning fields for rain_animation_mode = "garden" and that
these fields are not surfaced in the in-app settings TUI (they fall back to
built-in defaults if left zero). Edit the comment near the Garden mode tuning
declaration in types.go to describe the correct number of fields and current TUI
visibility instead of the old "three rows" wording.
In `@internal/ui/rain_bg_test.go`:
- Around line 77-101: The test is nondeterministic because
gardenSkySeedsForBatch uses random binomial sampling; to guarantee a nonzero
result in the mature-garden branch set a deterministic, guaranteed seed rate
(e.g. rb.Garden.SeedSpawnRate = 1.0) before calling
rb.gardenSkySeedsForBatch(40) after you clear rb.Drops and flip GardenPlots to
gardenStageBloom, so the throttle ceiling (checked via
rb.gardenMaxFlyingSkySeeds(rb.gardenSeedThrottleRelief())) is what’s being
tested rather than randomness.
In `@internal/ui/rain_bg.go`:
- Line 486: The S1009 lint failure is caused by a redundant nil check on the
slice rb.GardenPlots; remove the explicit nil comparison and replace the
condition with only len(rb.GardenPlots) == 0 (i.e., change the if that currently
reads "if rb.GardenPlots == nil || len(rb.GardenPlots) == 0" to check only
length). Update any related branches in the same function (e.g., in rain_bg.go
where rb.GardenPlots is used) to rely on len for emptiness checks and eliminate
the nil check to satisfy golangci-lint.
In `@internal/ui/repo_selector.go`:
- Around line 764-773: applyGardenTuning currently dereferences m.cfg (accessing
m.cfg.UI.RainTickMS) which can be nil in static-mode; guard against this by
checking m.cfg != nil before reading its UI.RainTickMS (or by using a local tick
default when m.cfg is nil) and then call
rb.SetGardenTuning(gardenTuningFromConfig(m.cfg, tick, rb.Width)) as before;
update the applyGardenTuning function to compute tick using m.cfg only when
m.cfg != nil (falling back to config.DefaultUIRainTickMS) so the method no
longer panics when m.cfg is nil.
In `@README.md`:
- Line 346: The README's commented default for garden_seed_rate (0.10) conflicts
with the actual default used in code (garden_seed_rate = 0.055); update the
README.md entry so the commented default matches the canonical value defined in
internal/config/types.go and internal/config/defaults.go (garden_seed_rate =
0.055), or alternatively change the code defaults in internal/config/types.go
and internal/config/defaults.go to 0.10 if that was intended—ensure all three
places reference the same numeric default for garden_seed_rate.
---
Outside diff comments:
In `@internal/ui/rain_bg.go`:
- Around line 622-680: The landing logic uses p.Y >= maxDropY but doesn't
remove/consume the drop, so a single drop can apply effects repeatedly; after
handling landing effects in both the advanced (rb.Flowers) and garden
(rb.GardenPlots) branches (inside the rb.Mode checks), mark the drop as consumed
so it won't apply again — e.g., set p.Alive = false (or p.Y = rb.Height / set a
consumed flag) immediately after calling rb.gardenWaterPlot or incrementing
drops / applying seed boost; update references to p (and gardenWatered)
accordingly so subsequent frames skip this drop.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6c785b46-e3c9-4ce6-8b21-de049f76be26
📒 Files selected for processing (11)
README.mdinternal/config/defaults.gointernal/config/loader.gointernal/config/types.gointernal/ui/config_view.gointernal/ui/panel_layout.gointernal/ui/panel_layout_test.gointernal/ui/rain_bg.gointernal/ui/rain_bg_test.gointernal/ui/repo_selector.gointernal/ui/view_layout.go
| // Garden mode tuning (used when rain_animation_mode = "garden"). | ||
| // In the settings TUI, three rows appear under Rain animation mode; any | ||
| // field left at zero here still falls back to built-in defaults at runtime. | ||
|
|
There was a problem hiding this comment.
Stale doc: "three rows" claim.
The comment asserts that "three rows appear under Rain animation mode" in the settings TUI, but this block introduces eight garden tuning fields. Since the README explicitly states these are not surfaced in the in-app settings TUI, this inline comment is likely stale and misleading for future maintainers.
✏️ Suggested wording
// Garden mode tuning (used when rain_animation_mode = "garden").
- // In the settings TUI, three rows appear under Rain animation mode; any
- // field left at zero here still falls back to built-in defaults at runtime.
+ // These are TOML-only (not exposed in the in-app settings TUI). Any field
+ // left at zero falls back to built-in defaults at runtime.📝 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.
| // Garden mode tuning (used when rain_animation_mode = "garden"). | |
| // In the settings TUI, three rows appear under Rain animation mode; any | |
| // field left at zero here still falls back to built-in defaults at runtime. | |
| // Garden mode tuning (used when rain_animation_mode = "garden"). | |
| // These are TOML-only (not exposed in the in-app settings TUI). Any field | |
| // left at zero falls back to built-in defaults at runtime. | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/config/types.go` around lines 80 - 83, Update the stale comment
above the Garden mode tuning block to remove the "three rows" claim and reflect
reality: mention that this block defines eight garden tuning fields for
rain_animation_mode = "garden" and that these fields are not surfaced in the
in-app settings TUI (they fall back to built-in defaults if left zero). Edit the
comment near the Garden mode tuning declaration in types.go to describe the
correct number of fields and current TUI visibility instead of the old "three
rows" wording.
| // GardenOffspringMin/Max bound how many seeds a dying plant scatters. | ||
| // 0 = use defaults (currently 1 and 2). | ||
| GardenOffspringMin int `mapstructure:"garden_offspring_min" toml:"garden_offspring_min,omitempty"` | ||
| GardenOffspringMax int `mapstructure:"garden_offspring_max" toml:"garden_offspring_max,omitempty"` |
There was a problem hiding this comment.
Stale doc: offspring defaults.
The comment says "currently 1 and 2" but the README example and defaults.go example TOML list garden_offspring_min = 2 and garden_offspring_max = 3. Please reconcile the doc with the actual DefaultGardenTuning() values so users don't get surprised by non-zero overrides behaving differently than documented.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/config/types.go` around lines 105 - 108, The inline comment for
GardenOffspringMin and GardenOffspringMax is stale (mentions "currently 1 and
2") and should be reconciled with the actual defaults returned by
DefaultGardenTuning() and the README example; update the comment to reflect the
real defaults (e.g., "currently 2 and 3") or adjust DefaultGardenTuning()/README
to match the comment so they are consistent, ensuring references to
GardenOffspringMin and GardenOffspringMax in defaults.go and README are all
aligned.
| // the global flying-seed ceiling so young plants see few sky seeds, and mature | ||
| // meadows can carry more falling seeds. | ||
| func (rb *RainBackground) gardenSeedThrottleRelief() float64 { | ||
| if rb.GardenPlots == nil || len(rb.GardenPlots) == 0 { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n internal/ui/rain_bg.go | sed -n '480,495p'Repository: git-fire/git-rain
Length of output: 648
🌐 Web query:
golangci-lint S1009 rule staticcheck
💡 Result:
The S1009 rule from the staticcheck linter in golangci-lint flags redundant nil checks before calling len on slices, maps, or channels. Since len returns 0 for nil values of these types, the explicit nil check is unnecessary. Example flagged code: if x != nil && len(x) != 0 {} Suggested fix: if len(x) != 0 {} This check is part of the 'simple' (S) category in staticcheck, available since 2017.1. In golangci-lint, staticcheck is enabled by default in many configurations, and S1009 reports appear in projects like argo-cd. To configure in golangci-lint .golangci.yml: linters: enable: - staticcheck To disable S1009 specifically: linters: settings: staticcheck: checks: ["all", "-S1009"]
Citations:
- 1: https://staticcheck.dev/docs/checks
- 2: simple: S1009 doesn't handle
if x == nil || len(x) < Nredundant nil-checks dominikh/go-tools#1605 - 3: errors reported by golangci-lint: S1009: should omit nil check; printf: non-constant format string argoproj/argo-cd#19772
- 4: https://golangci-lint.run/docs/linters/configuration/
- 5: https://golangci-lint.run/docs/linters/false-positives
Fix the CI-blocking S1009 lint failure at line 492.
len is defined for nil slices in Go, so the nil check is redundant and currently fails golangci-lint.
Proposed fix
- if rb.GardenPlots == nil || len(rb.GardenPlots) == 0 {
+ if len(rb.GardenPlots) == 0 {
return 0
}📝 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.
| if rb.GardenPlots == nil || len(rb.GardenPlots) == 0 { | |
| if len(rb.GardenPlots) == 0 { | |
| return 0 | |
| } |
🧰 Tools
🪛 GitHub Actions: CI
[error] 486-486: golangci-lint: S1009 (staticcheck) should omit nil check; len() for nil slices is defined as zero
🪛 GitHub Check: lint
[failure] 486-486:
S1009: should omit nil check; len() for nil slices is defined as zero (staticcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/ui/rain_bg.go` at line 486, The S1009 lint failure is caused by a
redundant nil check on the slice rb.GardenPlots; remove the explicit nil
comparison and replace the condition with only len(rb.GardenPlots) == 0 (i.e.,
change the if that currently reads "if rb.GardenPlots == nil ||
len(rb.GardenPlots) == 0" to check only length). Update any related branches in
the same function (e.g., in rain_bg.go where rb.GardenPlots is used) to rely on
len for emptiness checks and eliminate the nil check to satisfy golangci-lint.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Nil pointer dereference on
m.cfginapplyGardenTuningapplyGardenTuningnow resolves rain tick fromconfig.DefaultUIRainTickMSunlessm.cfgis non-nil with a positiveRainTickMS, matching the constructor and avoiding a nil dereference onWindowSizeMsg.
Preview (32dc264ab9)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -327,6 +327,36 @@
GIT_RAIN_GLOBAL_SCAN_PATH=/tmp/repos git-rain+### Garden mode tuning (advanced)
+
+Picking rain_animation_mode = "garden" swaps the rain background for a
+slow-paced lifecycle: seeds drift down with the rain, plants progress through
+sprout, bud, and bloom, then wither and scatter 2–3 new seeds nearby. When the
+visible plants cover roughly 80% of the rain strip, the storm clears, the sun
+comes out, and the surviving flowers stay forever.
+
+The defaults aim for a calm pace, but a few advanced TOML keys are available
+under [ui] for tweaking. They are intentionally not surfaced in the
+in-app settings TUI — leave any unset (or set to 0) to keep the default.
+
+toml +[ui] +rain_animation_mode = "garden" + +# garden_seed_rate = 0.10 # fraction of new sky drops that fall as seeds (0..1) +# garden_growth_pace = 1.0 # multiplier on stage moisture thresholds (>1 = slower) +# garden_bloom_duration_base = 60 # min frames a flower lingers in full bloom +# garden_bloom_duration_jitter = 40 # extra random frames added to bloom lifetime +# garden_wither_duration = 28 # frames a withered plant lingers before re-seeding +# garden_offspring_min = 2 # minimum seeds a dying plant scatters +# garden_offspring_max = 3 # maximum seeds a dying plant scatters +# garden_offspring_spread = 3 # X-jitter half-width around the parent column +
+
+garden_growth_pace is the most useful single dial: set it to 2.0 to roughly
+halve growth speed, or 0.5 to roughly double it. The other knobs trade
+visual density (more or fewer seeds, longer or shorter blooms) for clarity.
+
Config file, locks, and crashes
Registry (repos.toml) — Writes use a cross-process lock file (repos.toml.lock), atomic replace, and stale-lock detection (owner PID). If a process dies mid-run you may still see a leftover lock: the CLI prompts to remove it when safe, or you can use --force-unlock-registry in scripts. This is the same class of “stale lock / don’t corrupt the database” problem as other multi-repo tools; treat lock removal like any other forced unlock — only when you are sure no other git-rain is running.
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -114,7 +114,8 @@
show_rain_animation = true
Animation mode: "basic" (rain drops), "advanced" (clouds + rain + flowers),
-# or "matrix" (falling code characters)
+# "matrix" (falling code characters), or "garden" (seeds bloom into a meadow,
+# then the rain stops and the sun comes out)
rain_animation_mode = "basic"
Show flavor quotes in the TUI banner
@@ -131,5 +132,19 @@
Color profile: "storm", "drizzle", "monsoon", "rainbow", "synthwave"
color_profile = "storm"
+
+# --- Garden mode tuning (advanced) -----------------------------------------
+# These keys only affect rain_animation_mode = "garden". Leave them unset
+# (or at 0) to use the built-in defaults; tweak to make growth slower or
+# offspring more (or less) prolific.
+#
+# garden_seed_rate = 0.055 # sky seed density (0..1); runtime caps bursts per frame
+# garden_growth_pace = 1.0 # multiplier on stage moisture thresholds (>1 = slower)
+# garden_bloom_duration_base = 60 # min frames a flower lingers in full bloom
+# garden_bloom_duration_jitter = 40 # extra random frames added to bloom lifetime
+# garden_wither_duration = 28 # frames a withered plant lingers before re-seeding
+# garden_offspring_min = 1 # minimum seeds a dying plant scatters
+# garden_offspring_max = 2 # maximum seeds a dying plant scatters
+# garden_offspring_spread = 2 # X-jitter half-width around the parent column
`
}
diff --git a/internal/config/loader.go b/internal/config/loader.go
--- a/internal/config/loader.go
+++ b/internal/config/loader.go
@@ -97,6 +97,17 @@
v.SetDefault("ui.startup_quote_interval_sec", defaults.UI.StartupQuoteIntervalSec)
v.SetDefault("ui.rain_tick_ms", defaults.UI.RainTickMS)
v.SetDefault("ui.color_profile", defaults.UI.ColorProfile)
+
- // Garden tuning keys default to zero so the runtime can detect "unset"
- // and substitute the built-in defaults from DefaultGardenTuning().
- v.SetDefault("ui.garden_seed_rate", defaults.UI.GardenSeedRate)
- v.SetDefault("ui.garden_growth_pace", defaults.UI.GardenGrowthPace)
- v.SetDefault("ui.garden_bloom_duration_base", defaults.UI.GardenBloomDurationBase)
- v.SetDefault("ui.garden_bloom_duration_jitter", defaults.UI.GardenBloomDurationJitter)
- v.SetDefault("ui.garden_wither_duration", defaults.UI.GardenWitherDuration)
- v.SetDefault("ui.garden_offspring_min", defaults.UI.GardenOffspringMin)
- v.SetDefault("ui.garden_offspring_max", defaults.UI.GardenOffspringMax)
- v.SetDefault("ui.garden_offspring_spread", defaults.UI.GardenOffspringSpread)
}
// Bounded lock acquisition for config.toml: SaveConfig runs from the TUI on
diff --git a/internal/config/types.go b/internal/config/types.go
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -57,7 +57,8 @@
ShowRainAnimation bool mapstructure:"show_rain_animation" toml:"show_rain_animation"
// Animation mode: "basic" (rain drops), "advanced" (clouds + rain + flowers),
- // or "matrix" (falling code glyphs in the same column pattern).
-
// "matrix" (falling code glyphs in the same column pattern), or "garden"
-
// (seeds, rain, growth, then sun).
RainAnimationMode stringmapstructure:"rain_animation_mode" toml:"rain_animation_mode"// Show flavor quotes: TUI banner plus CLI motivation lines.
@@ -76,6 +77,40 @@
// Color profile for rain and TUI accents.
// Options: "storm", "drizzle", "monsoon", "rainbow", "synthwave".
ColorProfile stringmapstructure:"color_profile" toml:"color_profile" -
// Garden mode tuning (used when rain_animation_mode = "garden").
-
// In the settings TUI, three rows appear under Rain animation mode; any
-
// field left at zero here still falls back to built-in defaults at runtime.
-
// GardenSeedRate is the target probability (0..1) for sky seeds; the
-
// simulator caps seeds per frame, so this reads as density, not i.i.d.
-
// rolls. Lower = rarer; default ~0.055.
-
GardenSeedRate float64
mapstructure:"garden_seed_rate" toml:"garden_seed_rate,omitempty" -
// GardenGrowthPace multiplies the moisture thresholds for every growth
-
// stage. >1 slows growth; <1 speeds it up; 0 = use defaults.
-
GardenGrowthPace float64
mapstructure:"garden_growth_pace" toml:"garden_growth_pace,omitempty" -
// GardenBloomDurationBase is the minimum bloom lifetime in animation
-
// frames before a flower starts to wither. 0 = default.
-
GardenBloomDurationBase int
mapstructure:"garden_bloom_duration_base" toml:"garden_bloom_duration_base,omitempty" -
// GardenBloomDurationJitter is added on top of the base bloom duration
-
// (uniform random in [0, jitter)). 0 = default.
-
GardenBloomDurationJitter int
mapstructure:"garden_bloom_duration_jitter" toml:"garden_bloom_duration_jitter,omitempty" -
// GardenWitherDuration is how many frames a withered plant lingers
-
// before crumbling and seeding offspring. 0 = default.
-
GardenWitherDuration int
mapstructure:"garden_wither_duration" toml:"garden_wither_duration,omitempty" -
// GardenOffspringMin/Max bound how many seeds a dying plant scatters.
-
// 0 = use defaults (currently 1 and 2).
-
GardenOffspringMin int
mapstructure:"garden_offspring_min" toml:"garden_offspring_min,omitempty" -
GardenOffspringMax int
mapstructure:"garden_offspring_max" toml:"garden_offspring_max,omitempty" -
// GardenOffspringSpread is the half-width X jitter applied around the
-
// parent column when scattering offspring seeds. 0 = default.
-
GardenOffspringSpread int
mapstructure:"garden_offspring_spread" toml:"garden_offspring_spread,omitempty"
}
const (
@@ -91,6 +126,7 @@
UIRainAnimationBasic = "basic"
UIRainAnimationAdvanced = "advanced"
UIRainAnimationMatrix = "matrix"
- UIRainAnimationGarden = "garden"
)
// UIColorProfiles returns valid built-in UI color profile names.
diff --git a/internal/ui/config_view.go b/internal/ui/config_view.go
--- a/internal/ui/config_view.go
+++ b/internal/ui/config_view.go
@@ -41,6 +41,7 @@
config.UIRainAnimationBasic,
config.UIRainAnimationAdvanced,
config.UIRainAnimationMatrix,
-
}},
config.UIRainAnimationGarden,
{label: "Show flavor quotes", kind: configRowBool},
{label: "Flavor quote behavior", kind: configRowEnum, options: []string{
@@ -57,11 +58,122 @@
{label: "Custom hex palette", kind: configRowComingSoon},
}
-func configRowValue(i int, cfg *config.Config) string {
+// Garden settings rows appear in the menu only when rain mode is garden,
+// directly under "Rain animation mode" (see logicalRowIndex).
+var gardenSettingsConfigRows = []configRow{
- {label: "Garden growth pace", kind: configRowEnum, options: []string{"calm", "normal", "fast"}},
- {label: "Garden seed rate", kind: configRowEnum, options: []string{"rare", "normal", "often"}},
- {label: "Garden offspring", kind: configRowEnum, options: []string{"few", "default", "many"}},
+}
+func gardenSettingsRowCount(cfg *config.Config) int {
- if cfg != nil && strings.EqualFold(strings.TrimSpace(cfg.UI.RainAnimationMode), config.UIRainAnimationGarden) {
-
return len(gardenSettingsConfigRows) - }
- return 0
+}
+func visibleConfigRowCount(cfg *config.Config) int {
- return len(configRows) + gardenSettingsRowCount(cfg)
+}
+// logicalRowIndex maps a visible settings row to legacy ids 0..len(configRows)-1
+// or len(configRows)+k for garden-only rows.
+func logicalRowIndex(visibleI int, cfg *config.Config) int {
- g := gardenSettingsRowCount(cfg)
- if g == 0 {
-
return visibleI - }
- if visibleI < 5 {
-
return visibleI - }
- if visibleI < 5+g {
-
return len(configRows) + (visibleI - 5) - }
- return visibleI - g
+}
+func configRowAt(visibleI int, cfg *config.Config) configRow {
- li := logicalRowIndex(visibleI, cfg)
- if li < len(configRows) {
-
return configRows[li] - }
- gi := li - len(configRows)
- if gi >= 0 && gi < len(gardenSettingsConfigRows) {
-
return gardenSettingsConfigRows[gi] - }
- return configRows[len(configRows)-1]
+}
+func clampConfigCursor(cfg *config.Config, cur int) int {
- n := visibleConfigRowCount(cfg)
- if n <= 0 {
-
return 0 - }
- if cur >= n {
-
return n - 1 - }
- if cur < 0 {
-
return 0 - }
- return cur
+}
+func gardenGrowthPaceLabel(cfg *config.Config) string {
if cfg == nil {
-
return "normal" - }
- p := cfg.UI.GardenGrowthPace
- if p <= 0 {
-
return "normal" - }
- if p >= 1.2 {
-
return "calm" - }
- if p < 0.9 {
-
return "fast" - }
- return "normal"
+}
+func gardenSeedRateLabel(cfg *config.Config) string {
- if cfg == nil {
-
return "normal" - }
- s := cfg.UI.GardenSeedRate
- if s <= 0 {
-
return "normal" - }
- if s < 0.08 {
-
return "rare" - }
- if s > 0.12 {
-
return "often" - }
- return "normal"
+}
+func gardenOffspringLabel(cfg *config.Config) string {
- if cfg == nil {
-
return "default" - }
- if cfg.UI.GardenOffspringMin <= 0 && cfg.UI.GardenOffspringMax <= 0 {
-
return "default" - }
- if cfg.UI.GardenOffspringMin >= 3 {
-
return "many" - }
- if cfg.UI.GardenOffspringMax <= 2 && cfg.UI.GardenOffspringMin > 0 {
-
return "few" - }
- return "default"
+}
+func configRowValue(visibleI int, cfg *config.Config) string {
- if cfg == nil {
return ""
}
- switch i {
- switch logicalRowIndex(visibleI, cfg) {
case 0:
return cfg.Global.DefaultMode
case 1:
@@ -99,18 +211,25 @@
return cfg.UI.ColorProfile
case 10:
return "coming soon" - case 11:
-
return gardenGrowthPaceLabel(cfg) - case 12:
-
return gardenSeedRateLabel(cfg) - case 13:
-
}
return gardenOffspringLabel(cfg)
return ""
}
-func applyConfigChange(i int, cfg *config.Config, dir int) {
+func applyConfigChange(visibleI int, cfg *config.Config, dir int) {
if cfg == nil {
return
}
- row := configRows[i]
- row := configRowAt(visibleI, cfg)
- li := logicalRowIndex(visibleI, cfg)
switch row.kind {
case configRowBool:
-
switch i {
-
switch li { case 1: cfg.Global.DisableScan = !cfg.Global.DisableScan case 3:
@@ -120,7 +239,7 @@
}
case configRowEnum:
opts := row.options
-
cur := configRowValue(i, cfg)
-
cur := configRowValue(visibleI, cfg) idx := 0 for j, o := range opts { if o == cur {
@@ -129,7 +248,7 @@
}
}
idx = (idx + dir + len(opts)) % len(opts)
-
switch i {
-
switch li { case 0: cfg.Global.DefaultMode = opts[idx] case 2:
@@ -150,6 +269,39 @@
applyRainTickChange(cfg, opts, dir)
case 9:
cfg.UI.ColorProfile = opts[idx]
-
case 11: -
switch opts[idx] { -
case "calm": -
cfg.UI.GardenGrowthPace = 1.32 -
case "fast": -
cfg.UI.GardenGrowthPace = 0.78 -
default: -
cfg.UI.GardenGrowthPace = 0 -
} -
case 12: -
switch opts[idx] { -
case "rare": -
cfg.UI.GardenSeedRate = 0.06 -
case "often": -
cfg.UI.GardenSeedRate = 0.15 -
default: -
cfg.UI.GardenSeedRate = 0 -
} -
case 13: -
switch opts[idx] { -
case "few": -
cfg.UI.GardenOffspringMin = 1 -
cfg.UI.GardenOffspringMax = 2 -
cfg.UI.GardenOffspringSpread = 2 -
case "many": -
cfg.UI.GardenOffspringMin = 3 -
cfg.UI.GardenOffspringMax = 4 -
cfg.UI.GardenOffspringSpread = 5 -
default: -
cfg.UI.GardenOffspringMin = 0 -
cfg.UI.GardenOffspringMax = 0 -
cfg.UI.GardenOffspringSpread = 0 -
} }case configRowComingSoon:
// reserved
@@ -207,12 +359,13 @@
}case "down", "j":
-
if m.configCursor < len(configRows)-1 {
-
if m.configCursor < visibleConfigRowCount(m.cfg)-1 { m.configCursor++ }case " ", "right", "l":
applyConfigChange(m.configCursor, m.cfg, +1) -
m.configCursor = clampConfigCursor(m.cfg, m.configCursor) if m.cfg != nil { applyColorProfile(m.cfg.UI.ColorProfile) }
@@ -221,6 +374,7 @@
case "left", "h":
applyConfigChange(m.configCursor, m.cfg, -1)
-
m.configCursor = clampConfigCursor(m.cfg, m.configCursor) if m.cfg != nil { applyColorProfile(m.cfg.UI.ColorProfile) }
@@ -251,7 +405,7 @@
rainW := RainDisplayWidth(m.windowWidth)
s.WriteString(m.rainBg.Render())
s.WriteString("\n")
-
s.WriteString(RenderRainWave(rainW, m.frameIndex, m.rainAnimationMode))
-
}
s.WriteString(m.renderRainWaveStrip(rainW)) s.WriteString("\n\n")
@@ -273,7 +427,8 @@
valueStyle := lipgloss.NewStyle().Foreground(activeProfile().configValue).Bold(true)
dimStyle := lipgloss.NewStyle().Foreground(activeProfile().configDim)
- for i, row := range configRows {
- for i := 0; i < visibleConfigRowCount(m.cfg); i++ {
-
row := configRowAt(i, m.cfg) cur := " " if m.configCursor == i { cur = ">"
@@ -342,7 +497,13 @@
m.rainTick = time.Duration(m.cfg.UI.RainTickMS) * time.Millisecond
m.rainAnimationMode = m.cfg.UI.RainAnimationMode
if m.rainBg != nil {
-
m.rainBg.Mode = m.rainAnimationMode
-
bgW, h := m.rainBg.Width, m.rainBg.Height -
if m.rainBg.Mode != m.rainAnimationMode { -
m.rainBg = NewRainBackground(bgW, h, m.rainAnimationMode) -
} else { -
m.rainBg.Mode = m.rainAnimationMode -
} -
}
m.applyGardenTuning(m.rainBg)
m.showStartupQuote = m.cfg.UI.ShowStartupQuote
m.startupQuoteBehavior = m.cfg.UI.StartupQuoteBehavior
diff --git a/internal/ui/rain_bg.go b/internal/ui/rain_bg.go
--- a/internal/ui/rain_bg.go
+++ b/internal/ui/rain_bg.go
@@ -26,6 +26,38 @@
// flowerStages for advanced mode bottom row: growth over time
var flowerStages = []string{"·", "♦", "✿", "❀"}
+// gardenFlowerAccentHex holds distinct flower-head colors (garden mode).
+var gardenFlowerAccentHex = []string{
- "#EC407A", // pink
- "#AB47BC", // purple
- "#FFCA28", // amber
- "#FF7043", // deep orange
- "#BA68C8", // lavender
- "#F06292", // light pink
- "#FFD54F", // golden yellow
- "#7E57C2", // deep purple
- "#26C6DA", // cyan
- "#FFEE58", // yellow
- "#EF5350", // coral
- "#66BB6A", // green blossom
+}
+func gardenFlowerForeground(tint int) lipgloss.Color {
- n := len(gardenFlowerAccentHex)
- if n == 0 {
-
return activeProfile().flowerColor - }
- u := tint % n
- if u < 0 {
-
u += n - }
- return lipgloss.Color(gardenFlowerAccentHex[u])
+}
+func gardenFlowerStyle(tint int) lipgloss.Style {
- return lipgloss.NewStyle().Foreground(gardenFlowerForeground(tint))
+}
// RainDrop represents a single falling raindrop particle
type RainDrop struct {
X int
@@ -35,8 +67,238 @@
Age int
MaxAge int
Speed int // move down every Speed frames (1 = every frame, 2 = every other, etc.)
- IsSeed bool // garden mode: seed vs rain
}
+// gardenStage drives the lifecycle in garden animation mode.
+const (
- gardenStageNone = iota
- gardenStagePlanted
- gardenStageSprout
- gardenStageBud
- gardenStageBloom
- gardenStageWither
- gardenStageEternal
+)
+type gardenPlot struct {
- stage int
- moisture int
- bloomAge int
- witherAge int
- maxBloom int // randomized per-bloom duration; computed when entering bloom
- // flowerTint picks a stable accent from gardenFlowerAccentHex for bloom /
- // eternal flower heads; set when a seed first plants the column.
- flowerTint int
+}
+// GardenTuning controls the pacing and reproductive behavior of garden mode.
+// Zero-valued fields are filled in from DefaultGardenTuning() by ResolveGardenTuning.
+type GardenTuning struct {
- // SeedSpawnRate is the fraction of new sky drops that fall as seeds (0..1).
- SeedSpawnRate float64
- // RainAbsorbExtraChance is the probability that an extra rain drop in the
- // same frame still counts after the per-column water cap is reached.
- RainAbsorbExtraChance float64
- // Moisture thresholds to advance through plant stages.
- PlantedToSproutMoisture int
- SproutToBudMoisture int
- BudToBloomMoisture int
- // SeedMoistureBoost is the moisture added when a seed lands on an existing plot.
- SeedMoistureBoost int
- // Bloom and wither timing (frames). Effective bloom duration is
- // BloomDurationBase + rand.Intn(BloomDurationJitter).
- BloomDurationBase int
- BloomDurationJitter int
- WitherDuration int
- // OffspringMin..OffspringMax (inclusive) seeds spawn from each dying plant.
- // OffspringSpread is the half-width of the X jitter window around the parent.
- OffspringMin int
- OffspringMax int
- OffspringSpread int
+}
+// DefaultGardenTuning returns the built-in pacing constants chosen so the
+// garden takes its time: most sky pixels are rain, columns advance at most
+// once per frame, blooms linger, and dying plants scatter few nearby seeds.
+func DefaultGardenTuning() GardenTuning {
- return GardenTuning{
-
SeedSpawnRate: 0.055, -
RainAbsorbExtraChance: 0.11, -
PlantedToSproutMoisture: 16, -
SproutToBudMoisture: 22, -
BudToBloomMoisture: 30, -
SeedMoistureBoost: 1, -
BloomDurationBase: 85, -
BloomDurationJitter: 55, -
WitherDuration: 34, -
OffspringMin: 1, -
OffspringMax: 2, -
OffspringSpread: 2, - }
+}
+// ResolveGardenTuning fills any zero-valued field in t with the corresponding
+// default. Callers (e.g. config plumbing) can pass partial structs.
+func ResolveGardenTuning(t GardenTuning) GardenTuning {
- d := DefaultGardenTuning()
- if t.SeedSpawnRate <= 0 {
-
t.SeedSpawnRate = d.SeedSpawnRate - }
- if t.RainAbsorbExtraChance <= 0 {
-
t.RainAbsorbExtraChance = d.RainAbsorbExtraChance - }
- if t.PlantedToSproutMoisture <= 0 {
-
t.PlantedToSproutMoisture = d.PlantedToSproutMoisture - }
- if t.SproutToBudMoisture <= 0 {
-
t.SproutToBudMoisture = d.SproutToBudMoisture - }
- if t.BudToBloomMoisture <= 0 {
-
t.BudToBloomMoisture = d.BudToBloomMoisture - }
- if t.SeedMoistureBoost <= 0 {
-
t.SeedMoistureBoost = d.SeedMoistureBoost - }
- if t.BloomDurationBase <= 0 {
-
t.BloomDurationBase = d.BloomDurationBase - }
- if t.BloomDurationJitter <= 0 {
-
t.BloomDurationJitter = d.BloomDurationJitter - }
- if t.WitherDuration <= 0 {
-
t.WitherDuration = d.WitherDuration - }
- if t.OffspringMin <= 0 {
-
t.OffspringMin = d.OffspringMin - }
- if t.OffspringMax <= 0 {
-
t.OffspringMax = d.OffspringMax - }
- if t.OffspringMax < t.OffspringMin {
-
t.OffspringMax = t.OffspringMin - }
- if t.OffspringSpread <= 0 {
-
t.OffspringSpread = d.OffspringSpread - }
- if t.SeedSpawnRate > 1 {
-
t.SeedSpawnRate = 1 - }
- if t.RainAbsorbExtraChance > 1 {
-
t.RainAbsorbExtraChance = 1 - }
- return t
+}
+// gardenTargetStormWallSeconds picks a storm-completion time budget from the
+// same TUI presets as config_view (growth pace + seed rate). Used to scale
+// tuning so GardenSunny tends to land near these wall times at a reference
+// width, adjusted by applyGardenStormWallClockScale for tick + width.
+func gardenTargetStormWallSeconds(cfg *config.Config) float64 {
- if cfg == nil {
-
return 32 - }
- p := cfg.UI.GardenGrowthPace
- s := cfg.UI.GardenSeedRate
- rareSeed := s > 0 && s < 0.08
- calm := p >= 1.2
- oftenSeed := s > 0.12
- fastPace := p > 0 && p < 0.9
- switch {
- case rareSeed && calm:
-
return 180 - case rareSeed:
-
return 150 - case calm:
-
return 60 - case oftenSeed && fastPace:
-
return 26 - case oftenSeed || fastPace:
-
return 36 - default:
-
return 32 - }
+}
+// applyGardenStormWallClockScale stretches resolved garden pacing so the
+// storm phase (until ~80% visual fill / GardenSunny) tends toward
+// gardenTargetStormWallSeconds, scaled by rain tick (ms/frame) and sqrt(width)
+// so wider terminals get proportionally more frames for the same wall time.
+func applyGardenStormWallClockScale(t *GardenTuning, cfg *config.Config, rainTickMS, gardenWidth int) {
- if t == nil {
-
return - }
- tick := rainTickMS
- if tick <= 0 {
-
tick = config.DefaultUIRainTickMS - }
- sec := gardenTargetStormWallSeconds(cfg)
- w := float64(gardenWidth)
- if w < 12 {
-
w = 12 - }
- const refW = 56.0
- widthNorm := math.Sqrt(w / refW)
- targetFrames := sec * (1000.0 / float64(tick)) * widthNorm
- // Empirical baseline: pre–wall-clock tuning tended to finish the storm in
- // roughly this many frames at default moisture + seed rates. Ratio maps
- // desired wall time into multipliers on thresholds and spawn rate.
- const refFrames = 66.0
- scale := targetFrames / refFrames
- if scale < 1.0 {
-
scale = 1.0 - }
- // Slightly extra stretch on the path to first bloom; sky seeds are capped
- // separately, so moisture still needs headroom to feel gradual.
- const moistureBloomStretch = 1.12
- scaleMoist := scale * moistureBloomStretch
- scaleInt := func(v int, m float64) int {
-
x := int(float64(v)*m + 0.5) -
if x < 1 { -
return 1 -
} -
return x - }
- t.PlantedToSproutMoisture = scaleInt(t.PlantedToSproutMoisture, scaleMoist)
- t.SproutToBudMoisture = scaleInt(t.SproutToBudMoisture, scaleMoist)
- t.BudToBloomMoisture = scaleInt(t.BudToBloomMoisture, scaleMoist)
- t.BloomDurationBase = scaleInt(t.BloomDurationBase, scale)
- t.BloomDurationJitter = scaleInt(t.BloomDurationJitter, scale)
- t.WitherDuration = scaleInt(t.WitherDuration, scale)
- if t.SeedMoistureBoost > 0 {
-
b := int(float64(t.SeedMoistureBoost)/math.Sqrt(scale) + 0.5) -
if b < 1 { -
b = 1 -
} -
t.SeedMoistureBoost = b - }
- t.SeedSpawnRate /= scale
- if t.SeedSpawnRate < 0.008 {
-
t.SeedSpawnRate = 0.008 - }
- if t.SeedSpawnRate > 0.45 {
-
t.SeedSpawnRate = 0.45 - }
- t.RainAbsorbExtraChance /= scale*0.85 + 0.15
- if t.RainAbsorbExtraChance < 0.02 {
-
t.RainAbsorbExtraChance = 0.02 - }
- if t.RainAbsorbExtraChance > 0.55 {
-
t.RainAbsorbExtraChance = 0.55 - }
+}
// flowerCell tracks accumulated rainfall at a column for the advanced animation
type flowerCell struct {
drops int // accumulated drop count at this column
@@ -44,13 +306,16 @@
// RainBackground manages the animated rain background
type RainBackground struct {
- Width int
- Height int
- Drops []RainDrop
- Frame int
- Mode string // "basic" or "advanced"
- Flowers []flowerCell
- CloudRow []string // pre-rendered cloud chars per column
- Width int
- Height int
- Drops []RainDrop
- Frame int
- Mode string // "basic" or "advanced"
- Flowers []flowerCell
- CloudRow []string // pre-rendered cloud chars per column
- GardenPlots []gardenPlot
- GardenSunny bool // garden mode: rain finished, sky cleared
- Garden GardenTuning // pacing knobs (always resolved to non-zero values)
}
// NewRainBackground creates a new rain background
@@ -61,15 +326,25 @@
Drops: make([]RainDrop, 0),
Frame: 0,
Mode: mode,
-
}
Garden: DefaultGardenTuning(),
if width > 0 {
rb.Flowers = make([]flowerCell, width)
rb.CloudRow = rb.buildCloudRow()
} - if width > 0 && mode == config.UIRainAnimationGarden {
-
rb.GardenPlots = make([]gardenPlot, width) - }
rb.Reset()
return rb
}
+// SetGardenTuning replaces the active garden tuning. Zero-valued fields in t
+// are resolved to defaults; callers can pass partial structs.
+func (rb *RainBackground) SetGardenTuning(t GardenTuning) {
- rb.Garden = ResolveGardenTuning(t)
+}
func (rb *RainBackground) buildCloudRow() []string {
row := make([]string, rb.Width)
for x := 0; x < rb.Width; x++ {
@@ -85,13 +360,21 @@
// Reset reinitializes all drops
func (rb *RainBackground) Reset() {
rb.Drops = make([]RainDrop, 0)
- rb.GardenSunny = false
- if rb.Mode == config.UIRainAnimationGarden && rb.Width > 0 {
-
rb.GardenPlots = make([]gardenPlot, rb.Width) - }
startY := 0
- if rb.Mode == config.UIRainAnimationAdvanced {
-
startY = 1 // leave top row for clouds
- if rb.Mode == config.UIRainAnimationAdvanced || rb.Mode == config.UIRainAnimationGarden {
-
}
startY = 1 // leave top row for clouds / sky
targetCount := rb.Width * 2
- for i := 0; i < targetCount; i++ {
-
rb.spawnDrop(startY)
- if rb.Mode == config.UIRainAnimationGarden {
-
rb.spawnGardenMaintainingDrops(startY, targetCount) - } else {
-
for i := 0; i < targetCount; i++ { -
rb.spawnDrop(startY) -
}
}
}
@@ -106,6 +389,7 @@
}
... diff truncated: showing 800 of 1820 lines
</details>
<sub>You can send follow-ups to the cloud agent <a href="https://cursor.com/agents/bc-206fd568-d609-464e-b1d3-8d2e2b8d0143">here</a>.</sub>
<!-- BUGBOT_AUTOFIX_REVIEW_FOOTNOTE_END -->
<sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 77f6ce80f53cb73338cc5bb714c9cdb6bddb0932. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/ui/rain_bg.go (1)
538-547: Variable namecapshadows the builtin.Minor nit: using
capas a local variable shadows Go's built-incap(). Not currently harmful since the function doesn't callcap(), but defensive against future edits.♻️ Suggested rename
- lo := max(1, hi/5) - cap := int(0.5 + float64(lo)+(float64(hi-lo))*relief) - if cap < 1 { - cap = 1 - } - if cap > hi { - cap = hi - } - return cap + lo := max(1, hi/5) + ceiling := int(0.5 + float64(lo) + float64(hi-lo)*relief) + if ceiling < 1 { + ceiling = 1 + } + if ceiling > hi { + ceiling = hi + } + return ceiling🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/ui/rain_bg.go` around lines 538 - 547, The local variable named "cap" in the block that computes capacity (using lo, hi and relief) shadows Go's built-in cap(), so rename that variable (e.g., to "capacity" or "capVal") everywhere in this function/block (including assignments and return) to avoid shadowing and future confusion; ensure you replace all references to "cap" in this expression and keep the same boundary checks (min 1, max hi) and returned value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@internal/ui/rain_bg.go`:
- Around line 538-547: The local variable named "cap" in the block that computes
capacity (using lo, hi and relief) shadows Go's built-in cap(), so rename that
variable (e.g., to "capacity" or "capVal") everywhere in this function/block
(including assignments and return) to avoid shadowing and future confusion;
ensure you replace all references to "cap" in this expression and keep the same
boundary checks (min 1, max hi) and returned value.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 380335fe-bc23-4295-b9a7-e991f854565d
📒 Files selected for processing (5)
internal/config/defaults.gointernal/config/types.gointernal/ui/rain_bg.gointernal/ui/rain_bg_test.gointernal/ui/repo_selector.go
✅ Files skipped from review due to trivial changes (1)
- internal/config/defaults.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/ui/rain_bg_test.go
Derive PresetAggression from TUI growth/seed/offspring rows, dampen storm length scaling for aggressive combos, shorten fast+often storm targets, and relax early sky-seed caps and batch coefficients when aggression is high.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
internal/ui/rain_bg.go (1)
540-540:⚠️ Potential issue | 🟠 MajorRemove the redundant nil check to unblock lint.
lenis defined for nil slices in Go, and this condition is the confirmed S1009 failure.Proposed fix
- if rb.GardenPlots == nil || len(rb.GardenPlots) == 0 { + if len(rb.GardenPlots) == 0 {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/ui/rain_bg.go` at line 540, The condition checking both nil and length on rb.GardenPlots is redundant (len works on nil slices); change the if in the RainBackground code that currently reads "if rb.GardenPlots == nil || len(rb.GardenPlots) == 0" to a single length check "if len(rb.GardenPlots) == 0" so the S1009 lint warning is resolved while preserving the existing branch behavior; locate the check around the RainBackground/GardenPlots usage and replace it accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/ui/rain_bg.go`:
- Around line 1156-1179: The glyph styling logic currently picks styles based on
position (k, h) so second flower glyphs render as leaves; after calling
gardenGlyph(g.stage, k, h) capture the returned ch and if ch is a bloom/eternal
flower glyph (e.g., '❀' or '✿') override the chosen st with
gardenFlowerStyle(g.flowerTint) (and keep .Bold(true) for gardenStageEternal top
if desired); update the switch branches (references: gardenGlyph,
gardenFlowerStyle, gardenStageEternal, g.stage, ch) to apply this glyph-based
check so every actual flower glyph receives the flower accent regardless of k
position.
- Around line 765-768: The guard uses a negated conjunction which triggers
QF1001; replace the third clause !(rb.Mode == config.UIRainAnimationGarden &&
rb.GardenSunny) with the equivalent De Morgan form (rb.Mode !=
config.UIRainAnimationGarden || !rb.GardenSunny) so the if condition becomes
rb.Width > 0 && rb.Height > 0 && (rb.Mode != config.UIRainAnimationGarden ||
!rb.GardenSunny); leave the inner logic (including
spawnGardenMaintainingDrops(minY, targetCount)) unchanged to preserve behavior.
---
Duplicate comments:
In `@internal/ui/rain_bg.go`:
- Line 540: The condition checking both nil and length on rb.GardenPlots is
redundant (len works on nil slices); change the if in the RainBackground code
that currently reads "if rb.GardenPlots == nil || len(rb.GardenPlots) == 0" to a
single length check "if len(rb.GardenPlots) == 0" so the S1009 lint warning is
resolved while preserving the existing branch behavior; locate the check around
the RainBackground/GardenPlots usage and replace it 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: e4888ae7-410e-4057-b14a-6b3ef5453be3
📒 Files selected for processing (3)
internal/ui/rain_bg.gointernal/ui/rain_bg_test.gointernal/ui/repo_selector.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/ui/rain_bg_test.go
- internal/ui/repo_selector.go
Apply staticcheck/gocritic cleanups (nil len check, De Morgan spawn guard, mode switch in Render, tagged k switches in garden overlays, rename cap). Expire drops after one garden soil interaction so rain and seeds do not re-trigger every frame. Align README garden_seed_rate with defaults and make the mature sky-seed throttle test deterministic.

Summary
Garden rain mode is tuned to feel less rushed and more readable: storm pacing tracks
rain_tick_msand garden width, sky seeds are no longer independent per-drop floods, and flower heads use per-column accent colors.Changes
applyGardenStormWallClockScalestretches moisture, bloom, and seed spawn from config/TUI presets (normal vs rare/calm) and tick/width.gardenSeedThrottleRelief).flowerTintchosen when a seed first plants a column; bloom and eternal heads usegardenFlowerAccentHex.Testing
go test -race -count=1 ./...Note
Medium Risk
Moderate risk due to substantial changes in the TUI animation engine and settings/config plumbing; regressions could affect TUI rendering/performance but do not touch git/auth/data paths.
Overview
Improves the
gardenrain animation mode with wall-clock–based pacing (scaled byrain_tick_msand terminal width), seed throttling (batch caps plus a maturity-based in-flight seed budget), and per-column flower accent colors that persist into the post-storm sunny state.Adds new
[ui]garden_*config keys (defaulting to0to mean “unset”) and wires them through config loading, runtime tuning (SetGardenTuning), and the settings UI (garden-only rows). Updates the wave strip rendering to support a sunny-sky variant and expands tests around garden rendering, tuning resolution, throttling, and lifecycle transitions.Reviewed by Cursor Bugbot for commit 95062e7. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Documentation
Tests