Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,24 @@ rain_animation_mode = "garden"
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.

### Snow mode and rain panel size

`rain_animation_mode = "snow"` uses the same animation strip for a winter scene:
falling snowflakes, snow that keeps piling on the ground, a small log cabin
with chimney smoke and lit windows, occasional evergreen trees that pick up
frost, and a snowman that grows in stages (two spheres, then face, pipe, and
top hat).

`rain_panel_size` controls how many terminal rows the animation canvas uses:
`compact` (5), `comfortable` (8, default), or `tall` (11). The TUI clamps the
height automatically so the bordered panel still fits short terminals.

```toml
[ui]
rain_animation_mode = "snow"
rain_panel_size = "comfortable"
```

### 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.
Expand Down
30 changes: 30 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,39 @@ import (
"github.com/git-rain/git-rain/internal/config"
)

func TestRainPanelRowsPresets(t *testing.T) {
tests := []struct {
in string
want int
}{
{config.UIRainPanelCompact, 5},
{config.UIRainPanelComfortable, 8},
{config.UIRainPanelTall, 11},
{"", 8},
{"unknown-preset", 8},
}
for _, tc := range tests {
if got := config.RainPanelRows(tc.in); got != tc.want {
t.Errorf("RainPanelRows(%q) = %d, want %d", tc.in, got, tc.want)
}
}
}

func TestNormalizeRainPanelSize(t *testing.T) {
if got := config.NormalizeRainPanelSize(" TALL "); got != config.UIRainPanelTall {
t.Errorf("NormalizeRainPanelSize = %q, want %q", got, config.UIRainPanelTall)
}
if got := config.NormalizeRainPanelSize("compact"); got != config.UIRainPanelCompact {
t.Errorf("got %q", got)
}
}

func TestDefaultConfig_Values(t *testing.T) {
cfg := config.DefaultConfig()

if cfg.UI.RainPanelSize != config.UIRainPanelComfortable {
t.Errorf("default RainPanelSize = %q, want %q", cfg.UI.RainPanelSize, config.UIRainPanelComfortable)
}
if cfg.Global.BranchMode != "mainline" {
t.Errorf("default BranchMode = %q, want %q", cfg.Global.BranchMode, "mainline")
}
Expand Down
15 changes: 13 additions & 2 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ func DefaultConfig() Config {
UI: UIConfig{
ShowRainAnimation: true,
RainAnimationMode: UIRainAnimationBasic,
RainPanelSize: UIRainPanelComfortable,
ShowStartupQuote: true,
StartupQuoteBehavior: UIQuoteBehaviorRefresh,
StartupQuoteIntervalSec: DefaultUIStartupQuoteIntervalSec,
RainTickMS: DefaultUIRainTickMS,
ColorProfile: UIColorProfileStorm,
// SnowAccumulationRate 0 => runtime uses 1× (see SnowAccumPerLanding).
SnowAccumulationRate: 0,
},
}
}
Expand Down Expand Up @@ -114,10 +117,14 @@ mainline_patterns = []
show_rain_animation = true

# Animation mode: "basic" (rain drops), "advanced" (clouds + rain + flowers),
# "matrix" (falling code characters), or "garden" (seeds bloom into a meadow,
# then the rain stops and the sun comes out)
# "matrix" (falling code characters), "garden" (seeds bloom into a meadow,
# then the rain stops and the sun comes out), or "snow" (winter scene)
rain_animation_mode = "basic"

# Animation canvas height: "compact" (5 rows), "comfortable" (8), or "tall" (11).
# Clamped automatically if the terminal is short.
rain_panel_size = "comfortable"

# Show flavor quotes in the TUI banner
show_startup_quote = true

Expand All @@ -133,6 +140,10 @@ rain_tick_ms = 150
# Color profile: "storm", "drizzle", "monsoon", "rainbow", "synthwave"
color_profile = "storm"

# --- Snow mode (rain_animation_mode = "snow") -------------------------------
# Ground depth added per landed flake (1–8). 1 = default; 3 ≈ three times faster piling.
# snow_accumulation_rate = 1

# --- 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
Expand Down
4 changes: 4 additions & 0 deletions internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ func LoadWithOptions(opts LoadOptions) (*Config, error) {
return nil, fmt.Errorf("invalid config: %w", err)
}

cfg.UI.RainPanelSize = NormalizeRainPanelSize(cfg.UI.RainPanelSize)

return &cfg, nil
}

Expand All @@ -92,6 +94,7 @@ func setDefaults(v *viper.Viper) {

v.SetDefault("ui.show_rain_animation", defaults.UI.ShowRainAnimation)
v.SetDefault("ui.rain_animation_mode", defaults.UI.RainAnimationMode)
v.SetDefault("ui.rain_panel_size", defaults.UI.RainPanelSize)
v.SetDefault("ui.show_startup_quote", defaults.UI.ShowStartupQuote)
v.SetDefault("ui.startup_quote_behavior", defaults.UI.StartupQuoteBehavior)
v.SetDefault("ui.startup_quote_interval_sec", defaults.UI.StartupQuoteIntervalSec)
Expand All @@ -108,6 +111,7 @@ func setDefaults(v *viper.Viper) {
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)
v.SetDefault("ui.snow_accumulation_rate", defaults.UI.SnowAccumulationRate)
}

// Bounded lock acquisition for config.toml: SaveConfig runs from the TUI on
Expand Down
25 changes: 25 additions & 0 deletions internal/config/snow_accum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package config

import "testing"

func TestSnowAccumPerLanding(t *testing.T) {
tests := []struct {
rate float64
want int
}{
{0, 1},
{-1, 1},
{1, 1},
{1.4, 1},
{1.5, 2},
{3, 3},
{8, 8},
{8.4, 8},
{99, 8},
}
for _, tt := range tests {
if got := SnowAccumPerLanding(tt.rate); got != tt.want {
t.Errorf("SnowAccumPerLanding(%v) = %d, want %d", tt.rate, got, tt.want)
}
}
}
65 changes: 63 additions & 2 deletions internal/config/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Package config defines the git-rain configuration schema and related constants.
package config

import (
"math"
"strings"
)

// Config represents the complete git-rain configuration
type Config struct {
Global GlobalConfig `mapstructure:"global" toml:"global"`
Expand Down Expand Up @@ -57,10 +62,15 @@ type UIConfig struct {
ShowRainAnimation bool `mapstructure:"show_rain_animation" toml:"show_rain_animation"`

// Animation mode: "basic" (rain drops), "advanced" (clouds + rain + flowers),
// "matrix" (falling code glyphs in the same column pattern), or "garden"
// (seeds, rain, growth, then sun).
// "matrix" (falling code glyphs in the same column pattern), "garden"
// (seeds, rain, growth, then sun), or "snow" (winter scene).
RainAnimationMode string `mapstructure:"rain_animation_mode" toml:"rain_animation_mode"`

// RainPanelSize is how tall the animation canvas is in the TUI: "compact",
// "comfortable", or "tall". The runtime clamps to the terminal so the panel
// still fits (see RainPanelRows).
RainPanelSize string `mapstructure:"rain_panel_size" toml:"rain_panel_size"`

// Show flavor quotes: TUI banner plus CLI motivation lines.
ShowStartupQuote bool `mapstructure:"show_startup_quote" toml:"show_startup_quote"`

Expand Down Expand Up @@ -111,6 +121,11 @@ type UIConfig struct {
// 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"`

// SnowAccumulationRate scales how much ground snow depth each landed flake
// adds when rain_animation_mode = "snow". 1 = default; 2 ≈ twice as fast.
// Values are rounded to a whole number of depth units per landing (1..8).
SnowAccumulationRate float64 `mapstructure:"snow_accumulation_rate" toml:"snow_accumulation_rate,omitempty"`
}

const (
Expand All @@ -127,8 +142,38 @@ const (
UIRainAnimationAdvanced = "advanced"
UIRainAnimationMatrix = "matrix"
UIRainAnimationGarden = "garden"
UIRainAnimationSnow = "snow"

UIRainPanelCompact = "compact"
UIRainPanelComfortable = "comfortable"
UIRainPanelTall = "tall"
)

// RainPanelRows returns the target animation height in terminal rows for a
// panel size preset. Unknown or empty values use comfortable.
func RainPanelRows(preset string) int {
switch strings.ToLower(strings.TrimSpace(preset)) {
case UIRainPanelCompact:
return 5
case UIRainPanelTall:
return 11
default:
return 8
}
}

// NormalizeRainPanelSize returns a canonical preset name.
func NormalizeRainPanelSize(preset string) string {
switch strings.ToLower(strings.TrimSpace(preset)) {
case UIRainPanelCompact:
return UIRainPanelCompact
case UIRainPanelTall:
return UIRainPanelTall
default:
return UIRainPanelComfortable
}
}

// UIColorProfiles returns valid built-in UI color profile names.
func UIColorProfiles() []string {
return []string{
Expand All @@ -139,3 +184,19 @@ func UIColorProfiles() []string {
UIColorProfileSynthwave,
}
}

// SnowAccumPerLanding returns ground depth units added per landed snowflake.
// rate is cfg.UI.SnowAccumulationRate; 0 or negative means 1. Result is in [1, 8].
func SnowAccumPerLanding(rate float64) int {
if rate <= 0 {
return 1
}
n := int(math.Round(rate))
if n < 1 {
return 1
}
if n > 8 {
return 8
}
return n
}
Loading
Loading