Preset/palette completion: unified config layering + custom keybinds - #58
Merged
Conversation
Add 6 OKLch palettes (jade, amber, slate, pastel, ink, copper) and 12 lever-showcase presets (drift, constellation, mosaic, marble, prism, vellum, forge, wane, gossamer, codex, tide, and the now-removed lattice). Extend RenderArtDefaults with color_aa and hue_shift, plumbed in runner. Add IntensityMapping::sigmoid constructor. Apply review triage: remove 17 presets (14 originals plus showcase aurora and bloom and the new lattice) with their goldens and tests. Surviving set is 29 presets and 22 palettes.
Introduce const PALETTES: &[PaletteSpec] in render/palette.rs as the single identity table for built-in palettes. Display names, CLI parsing, the saved-config palette index, and the gradient cache seed all derive from it. Kills the parse_palette_index hardcoded 0-21 indices that had to mirror ALL_PALETTES order by hand (silent corruption on reorder); the index is now derived via PALETTES position. Collapses the duplicated 23-arm name matches in palette.rs and terminal/input.rs into one lookup, and the 22-arm CLI parse match into a name scan. Gradient getters stay exhaustive matches (already compiler-checked and get_256_gradient is a per-cell hot path). Behavior-preserving: all palette goldens unchanged. Adds drift + round-trip tests.
Introduce const PRESETS: &[PresetSpec] in simulation/config.rs as the single identity table for presets. CLI parsing (names + aliases), the 'Must be one of' error list, display names, the live quick-select keys (number row + shifted compare), and the validation test all derive from it. Collapses the duplicated 29-arm name matches in config.rs and terminal/input.rs into Preset::name(), the 29-arm FromStr parse + its hand-maintained error string into preset_from_name/preset_name_list, and the 14 hardcoded quick-key arms into two catalog-driven guard arms (preset_for_set_key / preset_for_compare_key). Per-preset sim params (Preset::apply) and render defaults (RenderArtDefaults) stay as separate exhaustive matches — the table carries identity only, preserving the sim/render split. Behavior- preserving: all preset goldens unchanged. Adds round-trip + quick-key consistency tests.
…_weight These two SimConfig fields were assigned unconditionally in ConfigBuilder::assemble() from non-optional CLI args whose defaults equal the global defaults. A preset that set decay_gamma or diffuse_weight in apply() had its value silently overwritten unless the user also passed the matching flag. RuntimeState::new hardcoded both to 1.0 and runner re-clobbered diffuse_weight from raw args, compounding the loss across the live-edit/undo paths. Make the args Option<f32> (unset = let the preset choose), guard the builder assignments like the neighbouring deposit_* knobs, and read both from the assembled config in RuntimeState::new. Affected presets: Lumen (decay_gamma 0.8), Wane (decay_gamma 0.6), Marble (diffuse_weight 0.8). Lumen golden regenerated on Linux. afterglow/afterglow_rate share the same defect class but route through a separate args-direct render path; deferred to the layering refactor.
Introduce PresetSimDefaults, the sim-layer counterpart to RenderArtDefaults: a struct of Option<T> simulation levers resolved per Preset and layered into SimConfig by config_builder. This gives sim levers the same 'preset suggests, CLI overrides, default fills' precedence that render levers already have, without merging the sim and render config structs. Currently carries only boundary_mode. config_builder applies the preset layer before the existing CLI --boundary-mode override, so the CLI flag still wins. Behaviour-preserving: no preset declares a boundary mode yet, so every preset still resolves to the global default (Bounce) and goldens are unchanged. Per-preset declarations land in the next commit.
River piles up on wall contact with the default Bounce boundary, and Ripple's rings need to re-enter the field; both read better wrapped. Declare boundary-mode wrap for them via PresetSimDefaults. An explicit --boundary-mode flag still overrides the preset. River golden regenerated (now wraps); added a ripple visual-regression golden to lock the new behaviour. Goldens generated on Linux.
Grow PresetSimDefaults into the full sim spec (Tasks 2+3 combined): - Replace the Option<BoundaryMode>-only struct with a complete declarative struct covering all sim levers (sensor/rotation/step, decay, diffusion, deposit curve, boundary, wind, obstacles, species_configs). - Add Default impl mirroring SimConfig::default() exactly so unset fields resolve byte-identically. - Add apply_to(&self, &mut SimConfig) to materialize the spec. - Port all 30 Preset arms from Preset::apply() into From<Preset>, one exhaustive match arm per variant using ..Self::default() fill. No _ arm so the compiler enforces exhaustiveness for future presets. - afterglow lines intentionally dropped from Lumen/Etching/Drift/Forge/ Gossamer (moves to RenderArtDefaults in a later commit). - Patch config_builder to read boundary_mode as a plain BoundaryMode (not Option) from the spec; CLI --boundary-mode flag still wins.
Remove the 637-line Preset::apply() imperative block and route From<Preset> for SimConfig through PresetSimDefaults::apply_to instead.
`resolve_render_config()` was hardcoding `AaStrength::Off` as the `color_aa` fallback, which would overwrite Braille's per-charset `Strong` default once Task 13 wires the resolver into startup. Replace with `DEFAULT_COLOR_AA[charset_index]` so Braille keeps Strong and all other charsets keep Off. Add two regression tests that verify the per-charset behaviour: braille→Strong and halfblock→Off. Also fix the misleading doc comment that said "else `AaStrength::Off`".
Replace scattered per-lever startup resolution (palette index, intensity, palette_cycle, glyph, color_aa, hue→speed, afterglow) in runner.rs with resolve_render_config() + apply_render_config(). RuntimeState drops the four initial_* render fields (palette_index, charset_index, color_aa, intensity_mapping) in favour of baseline_render: ResolvedRenderConfig, seeded at startup and reapplied by the ResetToDefaults handler via apply_render_config. reset_to_defaults() retains only sim-param and window_frame restores; the 13 render levers are fully owned by the apply path. 52/52 visual-regression goldens unchanged (behaviour-preserving).
Adds 4 tests to the cli.rs #[cfg(test)] module that lock the resolve_render_config / apply_render_config resolution semantics introduced in Commit 3: - switch_identity_to_art_on_enables_temporal_and_afterglow: Lumen resolves to Slime palette, temporal_color > 0, afterglow > 0. - switch_art_on_to_identity_disables_levers: Network resolves to temporal_color=0 and afterglow=0; also exercises the Task-11 set_compute_temporal toggle-clear path. - model_b_cli_palette_persists: --palette ocean survives a post-parse preset mutation to Lumen (simulates a live preset-switch). - model_b_cli_sensor_angle_persists: --sensor-angle 30 survives a post-parse preset mutation through ConfigBuilder.assemble(). Full-state switch→tweak→reset test (reset_to_defaults + apply_render_ config against a live Renderer) is not included: Renderer construction requires a real terminal + crossterm backend, making it impractical in a #[cfg(test)] context. The Task-11 toggle-clear units plus the above resolution tests provide coverage of the observable state layers. Verification matrix: cargo fmt --check PASS; clippy --all-features -D warnings PASS; cargo test --lib PASS (912); preset_config_snapshot PASS; Linux visual_regression PASS (52/52, no golden changes).
Wire Profile::resolve_from_args into run_simulation startup (replaces the previous separate to_sim_config + resolve_render_config calls). Add active_source: ProfileSource to RuntimeState; runner.rs sets it to Preset(p) or StartupCli based on args.preset at startup. Remove #[allow(dead_code)] from ProfileSource and resolve_from_args now that they have callsites; keep allow on SavedConfig variant (Task 3).
Trail-modulated presets (Pulse, Flocking, Ripple, Vortex36, DynamicTendrils) lost their per-species trail_modulation on save/reload because SpeciesArg — the captured/persisted form — had no field for it, so resolve restored None. - Derive Serialize/Deserialize on PointConfig (flat 15xf32 struct). - Add a persistence-only Option<PointConfig> to SpeciesArg (CLI FromStr leaves it None; sourced from the live SpeciesConfig on capture). - Thread it through capture_overrides and resolve's species branch. - Regression test: capture -> TOML -> reload -> resolve preserves modulation. The dirty-guard still normalises trail_modulation to None on both sides since it is not runtime-editable; comment updated to reflect it is now capturable.
…onsts The grid_color hex (0xffffff) and the skip_warmup / auto_reset / grid / grid_adaptive bool defaults were inline literals in AppRuntimeConfig::default. Add named consts (DEFAULT_GRID_COLOR_HEX, DEFAULT_GRID_ENABLED, DEFAULT_GRID_ADAPTIVE, DEFAULT_SKIP_WARMUP, DEFAULT_AUTO_RESET) so every default sources from config_defaults; assert against them in the default test.
…g swap Two config-load-path follow-ups: - The terminal-resize handler sized buffers against the frozen startup config.chrome_style. After loading a config that changed chrome and then resizing, the window layout used stale chrome. Read runtime_state.chrome_style (live) instead; the init-time closure stays on the startup value (pre-swap). - do_swap's Config arm re-read all configs from disk to find the one already selected in the browser. PendingSwap::Config now carries the resolved NamedProfile (boxed to keep the enum small), resolved once at selection time and surviving the dirty guard intact, so the load no longer re-lists.
…ng seam
apply_to_runtime_state was fully dead in production (the apply_overrides /
resolve seam replaced it), but ~20 round-trip tests still exercised it as a
parallel apply path — giving false confidence in code the runner never runs.
- Repoint each round-trip test to the shipping path: capture -> (TOML) ->
resolve, asserting on the resolved Profile { sim, render } instead of poking a
RuntimeState. Persist-only levers (reverse/invert/food_persist) assert the
override's unwrap_or(false) semantics; per-charset AA asserts via the real
rs.apply_color_aa_all seam.
- Delete the 185-line function and its now-unused imports/helper block.
- Refresh stale comments that referenced it.
No production behavior change; full gate green, goldens byte-identical.
auto_normalize was runtime-only (CLI flag -> loop-local current_auto_normalize + AdaptiveBrightness, toggled by B). Thread it through the full render seam so a preset can default it ON and a clean session stays non-dirty. - RenderArtDefaults.auto_normalize: Option<bool> (None = inherit, default off) - ResolvedRenderConfig.auto_normalize: bool - ProfileOverrides.auto_normalize: Option<bool>; from_args emits None-on-absent (Some(false) would shadow a preset default); resolve_render merges self.or(art).unwrap_or(false) - capture_overrides serializes the explicit live value for dirty parity - apply_render_config writes rs.auto_normalize; DefaultValues::from_preset reflects the preset's art default - runner seeds the loop-local + AdaptiveBrightness from rs, and rebuilds them after every swap (reset, preset, config) so a clean preset switch honors it - all 4 headless export paths read the resolved render.auto_normalize, so --preset ... --export-* honors the preset
…sent Add PresetAppDefaults (third declarative seam) so presets can opt in to auto_reset without the flag being present on the CLI. Constellation sets auto_reset=true. resolve_app now falls through: explicit override → preset default → AppRuntimeConfig::default(). from_args emits None for auto_reset when --auto-reset is absent (same None-on-absent pattern as auto_normalize).
Pulse→Slime, Flocking→Vines, Ripple→Smoke, Lumen→Mold. Old names (pulse, flocking, ripple, lumen) continue to work as case-insensitive CLI aliases via the PRESETS alias table. Updated all consumer sites: enum variants, PRESETS table, match arms in preset_sim_defaults/render_art_defaults/profile_overrides, and all internal test references. Replaced stale --preset help string with a generic pointer to the README. Renamed golden files ripple_preset→smoke_preset and lumen_preset→mold_preset (content unchanged; same simulation parameters).
Rename test functions to match the shipped preset names (Mold/Smoke): - preset_sim_defaults: lumen_arm→mold_arm, only_river_and_ripple→only_river_and_smoke - profile_overrides: test_only_river_and_ripple→test_only_river_and_smoke, test_ripple_and_river→test_smoke_and_river; inline comment Pulse/Flocking/Ripple→Slime/Vines/Smoke Convert 5 cli.rs test cases from --preset lumen to canonical --preset mold; update accompanying comments and assertions. Rewrite doc comments on Preset::Slime/Vines/Smoke in config.rs and the inline match-arm comments in preset_sim_defaults.rs to reflect the new identities (no parameter values changed).
…ines/exploratory) Add render-layer art arms (charset/palette/color-aa/auto-normalize) and sim-layer window_frame/kernel/brightness tweaks for the showcase presets. Update EXISTING_PRESETS identity invariants and Mold palette assertions to the new truth. Fix a dirty-guard false positive: project() now builds the canonical per-charset color_aa array from the fully-resolved render.color_aa (scalar + preset-art + default) instead of the raw override scalar, so a clean preset-only swap whose art layer sets color_aa (Slime) no longer reads dirty; the clean_session test mirror now also carries the resolved auto_normalize flag.
Identical sim to Vines; render layer applies Charset::Ascii. Adds enum variant, PRESETS entry (alias vines-ascii), sim arm (Vines body duplicated verbatim), render arm, snapshot token, and identity test.
Add InitMode::random over all 9 variants and a non-mutating runner-local resolver effective_init_mode(preset, base) that returns a fresh random mode only for Constellation. Route every production sim.reset re-seed site through it (collapse auto-reset, manual r/R restart, and the apply_overrides restart seam). The baseline original_init_mode and config preferred_init_mode are left untouched, so dirty-projection reports no permanent edit after a reset.
…/slime/vines/vinescii README: reflect pulse→slime, flocking→vines, ripple→smoke, lumen→mold renames; add vinescii; note aliases; update preset count to 30; point at config sources of truth. CHANGELOG: add entries for vinescii addition, rename pass, and help-text fix. tests/visual_regression.rs: add four new test fns (organic, slime, vines, vinescii) mirroring the existing network/exploratory pattern; goldens to be generated separately.
…/slime/vines/vinescii coverage
…es doc + guard InitMode::random
… to organic Default launch and `--preset organic` now resolve identically: - Organic sim arm = SimConfig::default() (decay 0.5, max_brightness 100); the old 0.85/20 was an over-saturating blob. Render mods (Braille+Vibrant) stay in RenderArtDefaults. - clap default_value="organic" on the preset arg routes a bare launch through the Organic preset everywhere (config, classification, telemetry); no separate hard-coded default config set. Args::default() struct stays None for tests. Mold: retune to a stable branching network. Wider sensor (15->22.5) + rotation (30->50) + boundary Bounce->Wrap; persistence (decay 0.85) preserved. The old regime condensed the whole population onto one wall-hugging diagonal filament. Constellation: fix inverted collapse detector. track_entropy fired on entropy > threshold, but healthy patterns sit ~4-6 on the 0..8 brightness-value entropy scale, so the 0.95 default was true every frame -> auto-reset presets restarted every ~90 frames. Flipped to < threshold (collapse = low entropy); corrected three stale 'above threshold' docs. River: render arm Ocean palette + Braille; sim arm Accented window frame. Vines/Vinescii: auto_normalize. River+Vines leave the EXISTING_PRESETS identity set. capture_overrides: capture boundary_mode (was hardcoded None while window_frame was captured), so a Wrap preset round-trips through projection instead of reading falsely dirty in the save/undo guard. Regenerated 41 goldens (37 from the default->organic routing, plus mold/river/vines/vinescii preset goldens and the sim-config snapshot).
The entropy detector measured brightness-value diversity, not motion, so coherent-but-alive Constellation patterns (rotating rings, slow blobs) read as collapsed and triggered false restarts within ~3s. Make --collapse-threshold an Option; unset now defers to a per-preset default via PresetAppDefaults. Constellation defaults it to 0 (entropy is always >= 0, so the detector never fires), leaving only the stagnation detector. Other presets keep the global 0.95 default; explicit --collapse-threshold still overrides on any preset.
Batched live-review work: - Add --frame-matte-cols/--frame-matte-rows: background gap between the window frame border and the simulation, threaded through ProfileOverrides and render config. - Trim window-frame modes to none/accented/glow/frame. - Add a stagnation collapse detector (near-static patterns) alongside entropy. - Retune preset boundaries/params (mold Wrap->Bounce, exploratory) and fix assorted preset bugs; regenerate affected goldens. - Config save/load and runtime-state adjustments.
Rebind number-row quick-keys to the launch presets (1=Organic, 2=Constellation, 3=Vinescii) and let users bind keys 1-7 to any preset or saved config via ~/.config/tslime/keybinds.toml. - keybind_manager: parse + validate keybinds.toml (pool 1-7, preset or config target, file-level fallback to built-ins, entry-level silent skip with last-valid-wins, Windows USERPROFILE fallback) - user binds override built-in defaults; resolution centralized in the runner; config quick-keys load through the shared dirty-guard route - extract dispatch_swap helper, collapsing the dirty-guard-or-swap block from four duplicated runner sites - input emits QuickKey/CompareQuickKey for 1-7 / Shift+1-7; unbound keys show a hint toast - generalize A/B comparison to ComparisonTarget so saved configs are comparable (resolved values shown as the B side) - the ? keyboard-reference overlay shows live per-key bindings - docs: README controls + custom-keybinds section, CHANGELOG
Bounce alone accumulates a wall-parallel trail that eventually condenses all agents onto one wall-hugging filament (collapse by ~6000f). Add a gentle trail-dependent respawn (every 90 frames, scatter agents sitting in high-trail back into the field, probability up to 5x where brightest) so the runaway feedback can never close. Holds a crisp large-celled vein network out to 40000f.
capture_overrides was hardcoding time_scale: None, meaning any live time-scale adjustment (via +/- keys) was silently dropped on config save and ignored on reload. Emit Some(rs.time_scale) so the live value round-trips through save → TOML → reload → resolve. Adds a unit test that verifies capture produces Some(non-default) and that resolve preserves it.
When a saved config's overrides fail to resolve, the comparison overlay was silently falling back to Organic defaults via DefaultValues::from_config without any indication to the user. Call profile.overrides.resolve() directly in the Config arm of build_overlay: on Ok, build defaults from the resolved sim config; on Err, append " (unresolved)" to the label and use Organic defaults for the comparison columns so the dialog still renders. Also exposes DefaultValues::from_sim_config as pub(crate) to enable this call site.
When start_x >= self.width, the expression `self.width - start_x` wraps on usize, producing a very large panel_width and causing out-of-bounds index panics in the panel-background loop. Add a start_x < self.width guard to the panel branch condition and replace the subtraction with saturating_sub so overflow is impossible. Adds a regression test that calls the function with start_x equal to and beyond the buffer width, asserting no panic.
calculate_position was using saturating_sub(5) which only accounts for the 6 main panel lines, missing the 1-line title-box drawn one row above. The dialog appeared shifted up by 1 row. Change to saturating_sub(7) to account for all 7 visible rows: 6 main panel lines (border + 4 content rows + border) plus the title-box line floated above.
Respawn probability scaled on the raw, unbounded pheromone value, so max_probability_multiplier was not a real cap (brightest cells scattered ~100% regardless of the knob). Add RespawnConfig::trail_rescale and normalize x = (trail * trail_rescale).clamp(0,1) before the multiplier, mirroring PointConfig. Retune Mold (base 0.0067 x mult 150 = 1.0 max probability at saturation, rescale 0.0033) to honest, bounded params. Headless probe (300x150, 50k agents, 40000f): edge:interior trail holds ~2.0-2.4 with full coverage; respawn-off collapses to ~7.5 by 1500f.
RespawnConfig now derives Serialize/Deserialize. ProfileOverrides gains a respawn_config: Option<RespawnConfig> field (serialized; None from CLI). capture_overrides captures the full struct (copy); resolve_sim applies it before the scalar respawn_interval override so --respawn-interval still wins. project() continues to normalize both sides to Default so the dirty guard stays blind to a field that is not runtime-editable. Fixes silent data loss when saving a Mold config and reloading it: previously only the interval scalar round-tripped, so base_probability, trail_dependent, max_probability_multiplier, and trail_rescale were silently reset to defaults and the reloaded Mold could wall-collapse. Test: test_respawn_config_round_trips_through_toml (confirmed fail->pass).
tamirelazar
added a commit
that referenced
this pull request
Jun 24, 2026
Resolve conflicts between the UI-system unification and dev's launch-quick-keys / custom-keybinds feature (#58): - KeyboardHintsOverlay::build_overlay now takes both `st: &PanelStyle` (UI-system theming) and `user_binds` (dev's live key-label lookup); all call sites and tests updated to the 3-arg signature. - runner.rs: keep both the UI-system controls-dispatch/ambient helpers and dev's resolve_bind quick-key resolver; thread panel_style + user_binds into the keyboard-hints build. - overlay.rs comparison header keeps dev's dynamic column label with the UI-system spacing token (add_empty_n(spacing::ROW)). - input.rs / state.rs: keep both sides' added tests. - README: keep both the Controls-panel-navigation and Custom-Keybinds docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes the
feat/preset-palette-completioneffort todev. The branch unifies preset/config resolution behind a single layered pipeline, hardens config save/load and live-apply, retunes presets, and adds custom key bindings.Highlights
Config layering & resolution
Profile/ProfileOverridesunify preset and config resolution; startup, live preset-switch, config-load, and reset all flow through one apply seam (apply_render_config/do_swap), resolving byte-identically to the former builder path.PresetSimDefaults/RenderArtDefaults/AppRuntimeConfigdeclare per-preset sim, render, and app levers as optional overrides (noPreset::apply()).Config save/load robustness
presets.tomlno longer poisons all config IO.Presets
Vinescii(ASCII-rendered vines); per-preset visual mods; Constellation re-rolls init mode on reset + stagnation collapse detector; Mold wall-collapse fixed via trail-dependent respawn; collapse/divergence bug fixes; default launch routes to Organic.Frames
Custom keybinds (final piece)
1/2/3→ Organic/Constellation/Vinescii;4–7bindable.~/.config/tslime/keybinds.tomlbinds keys1–7to any preset or saved config; user binds override defaults; invalid entries skipped silently.Shift+1–7A/B compare works for bound presets and configs (ComparisonTarget).?overlay shows live bindings. In-app editor deferred (tracked separately).Stats
96 files changed, ~10.3k insertions / ~4.6k deletions across 76 commits (26 feat, 20 fix, 13 refactor, 10 test, 3 chore, 2 perf, 2 docs).
Test plan
cargo fmt --check,cargo clippy --all-targets --all-features -- -D warnings— clean.cargo test— 1009 lib + 56 visual-regression + 30 integration pass.