Each per-section config field on `UserConfig` and `UserProjectOverrides`
(`ListConfig`, `CommitConfig`, `MergeConfig`, `SwitchConfig`,
`StepConfig`, `aliases`) was wrapped in `Option<>` despite each section
type having all-`Option<T>` fields internally — making
`Some(MergeConfig::default())` semantically equivalent to `None`.
`ResolvedConfig::for_project` was already calling `.unwrap_or_default()`
on every accessor result.
Replace `Option<Type>` with `Type` using `#[serde(default,
skip_serializing_if = "is_default")]` (sections) or `BTreeMap::is_empty`
(aliases), where `is_default` compares against `T::default()`. This
removes redundant Option indirection across the accessor, persistence,
mutation, and resolved config layers. Net -53 lines.
Continues the config cleanup from #2093.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Removes the `config` crate dependency entirely and replaces it with
direct TOML parsing + a manual env-var overlay. This eliminates the root
causes of three problems we were working around: no source tracking
(can't tell if error came from file or env var), `#[serde(flatten)]`
destroying error locations, and a single bad override killing the entire
deserialize.
**Step 1 — Inline OverridableConfig.** The `#[serde(flatten)]` on
`configs: OverridableConfig` in `UserConfig` (and `overrides:
OverridableConfig` in `UserProjectOverrides`) caused toml to lose field
paths on errors. Inlining the fields directly means
`toml::from_str::<UserConfig>` now gives correct line/col for all
section field errors. ~31 accessor sites simplified from
`.configs.field` to `.field`. `OverridableConfig` stays as a merge-only
type for per-project resolution.
**Step 2 — Replace ConfigError.** Every usage was
`ConfigError::Message(String)`. Replaced with a simple
`ConfigError(String)` newtype.
**Step 3 — Replace config crate.** New load flow: parse each file with
`toml::from_str` for validation, parse as `toml::Table` for merging,
build a `toml::Table` from `WORKTRUNK_*` env vars with type coercion
(bool → i64 → f64 → string), deep-merge all tables, deserialize the
result. Env-var convention unchanged (`WORKTRUNK__LIST__TIMEOUT_MS` →
`list.timeout-ms`).
Deletes `collect_worktrunk_override_vars` heuristic (exact var tracking
replaces hardcoded exclusion list), the two-pass pre-validation
workaround, and the `config` dependency from Cargo.toml.
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Previously config deprecation required two synchronized layers: Layer 1
scanned raw TOML to generate `.new` migration files, while Layer 2
(`normalize_deprecated_sections()`) moved deprecated struct fields to
canonical locations after serde parsing. Every new deprecation required
changes in both layers.
Now `migrate_content()` transforms raw TOML before serde parses it, so
deprecated patterns are rewritten into canonical form at the single
boundary between file I/O and deserialization. The struct no longer
needs deprecated fields — serde only sees canonical TOML.
Key changes:
- `migrate_content()` is the single entry point for all structural TOML
migrations, called by all load paths (`load()`, `load_from_str()`,
`mutation.rs` reload)
- `check_and_migrate()` returns the migrated content alongside
deprecation info, so the user config load path doesn't re-parse the TOML
- `no-ff` → `ff` and `no-cd` → `cd` migrations added at the TOML level
(previously only handled post-deserialization)
- `[select]` migration extended to project-level sections (was top-level
only)
- Removed: `normalize_deprecated_sections()`, `SelectConfig`,
`commit_generation` fields, `no_ff_deprecated`, `no_cd_deprecated`
- `replace_deprecated_vars` intentionally excluded from
`migrate_content()` — template variable renaming is cosmetic and would
break `--var` overrides using old names
- CLAUDE.md deprecation recipe updated to document the single-layer
approach
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add per-project hooks with append semantics
Per-project hooks in user config now append to global hooks rather than
replacing them. Execution order: global → per-project → project.
Also:
- Move hooks into OverridableConfig for consistency
- Rename UserConfig.overrides to UserConfig.configs
- Add schemars for deriving valid config keys from JsonSchema
- Replace manual VALID_USER_CONFIG_KEYS with schema-derived function
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: unify config validation on JsonSchema
Both UserConfig and ProjectConfig now use JsonSchema-derived keys for
validation instead of serde HashMap catchall. This:
- Removes `unknown` field from ProjectConfig
- Removes `unknown()` method from WorktrunkConfig trait
- Simplifies `is_valid_key` to not need value parameter
- Adds JsonSchema to JSON output types for future schema generation
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: document per-project hooks in setting overrides section
- Add per-project hooks to Setting overrides (Experimental) section
- Update hook.md table to show execution order with per-project hooks
- Replace JSON context example with one showing complex conditionals
- Keep documentation DRY by linking from hook.md to config.md
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: use consistent terminology (user config, not settings)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Clarifies that this struct represents user-level configuration
(~/.config/worktrunk/config.toml), distinct from ProjectConfig
which is checked into the repository.
Co-authored-by: Claude <noreply@anthropic.com>
* Simplify assert messages by removing redundant descriptions
Remove verbose assertion messages that repeat the condition being tested.
Messages like "Should contain indicator" when asserting `contains("●")` are
redundant since the code is self-documenting. This reduces noise in tests
while maintaining clarity through meaningful assertion failures.
* test: remove verbose assertion messages and doc comments
Removed facile test verbosity across three categories:
1. **Doc comments** (~236 lines): Removed `/// Test X` comments that just
restated test function names. Kept multi-line explanations that add context.
2. **Test names** (9 functions): Shortened overly verbose names by removing
filler words while maintaining clarity:
- test_merge_when_primary_not_on_default_but_default_has_worktree →
test_merge_primary_not_on_default_with_default_worktree
- test_complete_switch_option_prefix_shows_options_not_branches →
test_complete_switch_option_prefix_shows_options
3. **Assertion messages** (19 instances): Removed messages that just restate
the check:
- assert!(path.exists(), "Path should exist") → assert!(path.exists())
- assert!(x.is_file(), "Should be a file") → assert!(x.is_file())
Kept messages that add semantic meaning (e.g., "hook should have run") or
include debugging variables.
Total: 279 lines removed across 31 files. All 723 integration + 401 unit tests
passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: remove unreferenced snapshot files from renamed tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add [select] pager config with backward-compatible auto-detection
Adds a `[select] pager` config option to override git's pager specifically
for `wt select` diff previews. When not configured, maintains existing
behavior by auto-detecting delta/bat/batcat and appending --paging=never
to prevent hangs in non-TTY contexts.
Users can now set explicit pager commands:
```toml
[select]
pager = "delta --paging=never"
```
The config option takes precedence over auto-detection, giving users full
control while preserving backward compatibility for existing setups.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: add coverage for has_explicit_pager_config
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: add config deserialization tests for [select] pager
Tests verify that the SelectConfig struct deserializes correctly from TOML,
including edge cases (empty string, optional section).
Note: The actual config-loading code path in get_diff_pager() (lines 38-43)
remains uncovered because it only executes during wt select's TUI preview
generation, which requires complex PTY test setup.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>