From 8df4cd26b36184579fe15863e80511739bfee2ae Mon Sep 17 00:00:00 2001 From: shawn Date: Fri, 29 May 2026 11:26:41 +0800 Subject: [PATCH 1/2] feat: move config to XDG path with legacy migration warning termdown now reads ~/.config/termdown/config.toml (or $XDG_CONFIG_HOME/termdown/config.toml) instead of ~/.termdown/config.toml. When a legacy ~/.termdown/config.toml is found while the new path is empty, it warns rather than dropping the old settings silently. - config_dir() honors an absolute $XDG_CONFIG_HOME, else ~/.config - uninstall.sh removes both new and legacy dirs, mirroring the loader's absolute-XDG rule - add config.example.toml documenting every default - tests for XDG loading, the migration warning, and example-file drift - sweep docs/README/CHANGELOG/ADR to the new path Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 11 +- CONTEXT.md | 2 +- README.md | 7 +- README_CN.md | 6 +- config.example.toml | 40 +++++++ docs/ARCHITECTURE.md | 4 +- docs/MARKDOWN_FEATURE_COVERAGE.md | 2 +- docs/TUI_MODE_DESIGN.md | 2 +- docs/adr/0001-metadata-block-handling.md | 7 +- src/config.rs | 63 ++++++++++- src/main.rs | 2 +- tests/cli.rs | 133 ++++++++++++++++++++++- tests/common/mod.rs | 6 +- uninstall.sh | 20 +++- 14 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 config.example.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index de1f4ef..a33a9e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`[metadata · key=value, …]`); TUI shows the same line and can expand it to an inline key/value box with the new `m` key. A blank row follows the summary for visual separation. Opt out entirely via `[metadata] show = false` - in `~/.termdown/config.toml`. See `docs/adr/0001-metadata-block-handling.md`. + in `~/.config/termdown/config.toml`. See `docs/adr/0001-metadata-block-handling.md`. + +### Changed +- **Config location moved to the XDG path.** termdown now reads + `~/.config/termdown/config.toml` (or `$XDG_CONFIG_HOME/termdown/config.toml`) + instead of `~/.termdown/config.toml`. If a legacy `~/.termdown/config.toml` + is found while the new path is empty, termdown prints a one-line warning so + the old settings aren't dropped silently — move the file to the new location + to clear it. A documented `config.example.toml` with every default ships in + the repo root. ## [0.5.1] - 2026-05-26 diff --git a/CONTEXT.md b/CONTEXT.md index 218f47b..4255325 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -36,7 +36,7 @@ The two display states for a metadata block in TUI mode: Cat mode has no "expanded" state — only the one-line summary or nothing. ### `[metadata] show` -The single config knob (in `~/.termdown/config.toml`) controlling whether +The single config knob (in `~/.config/termdown/config.toml`) controlling whether frontmatter is visible at all. `show = true` (default) renders the [[metadata one-line summary]] / expanded box; `show = false` hides the metadata block in **both** cat and TUI. The pulldown-cmark metadata extensions are always diff --git a/README.md b/README.md index 81699a4..73b6d49 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ curl -fsSL https://raw.githubusercontent.com/rrbe/termdown/master/uninstall.sh | ```sh rm $(which termdown) -rm -rf ~/.termdown +rm -rf ~/.config/termdown ``` @@ -139,7 +139,10 @@ TUI mode requires a file path; stdin input is not supported. ## Configuration -termdown reads configuration from `~/.termdown/config.toml`. +termdown reads configuration from `~/.config/termdown/config.toml` (or +`$XDG_CONFIG_HOME/termdown/config.toml` if `XDG_CONFIG_HOME` is set). All +settings are optional; see [`config.example.toml`](config.example.toml) for a +copy-pasteable file with every default. ```toml # Theme: "auto" (default), "dark", or "light" diff --git a/README_CN.md b/README_CN.md index 4e6a533..6eb9ee3 100644 --- a/README_CN.md +++ b/README_CN.md @@ -69,7 +69,7 @@ curl -fsSL https://raw.githubusercontent.com/rrbe/termdown/master/uninstall.sh | ```sh rm $(which termdown) -rm -rf ~/.termdown +rm -rf ~/.config/termdown ``` @@ -131,7 +131,9 @@ TUI 模式需要指定文件路径,不支持从 stdin 读取。 ## 配置 -配置文件位于 `~/.termdown/config.toml`。 +配置文件位于 `~/.config/termdown/config.toml`(若设置了 `XDG_CONFIG_HOME`,则为 +`$XDG_CONFIG_HOME/termdown/config.toml`)。所有配置项均为可选;仓库根目录的 +[`config.example.toml`](config.example.toml) 提供了一份包含全部默认值、可直接复制的示例。 ```toml # 主题:"auto"(默认)、"dark" 或 "light" diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..858cf7f --- /dev/null +++ b/config.example.toml @@ -0,0 +1,40 @@ +# termdown configuration +# +# Copy this file to: +# ~/.config/termdown/config.toml +# (or $XDG_CONFIG_HOME/termdown/config.toml when XDG_CONFIG_HOME is set) +# +# Every value below is termdown's built-in default, so an empty or missing +# config behaves exactly like this file. Uncomment and edit only what you +# want to change. + +# Color theme: "auto" (default), "dark", or "light". +# "auto" detects the terminal background via OSC 11. +theme = "auto" + +# Vim-style edge bell: emit a terminal BEL when you scroll past the top or +# bottom of the document. The terminal emulator decides the visible effect +# (audible beep, title-bar 🔔, dock bounce, …). Default true. +# CLI override: --no-bell. +bell = true + +[metadata] +# Render YAML (`---`) / TOML (`+++`) frontmatter metadata blocks. When true +# (default), --cat and the TUI show a dim one-line summary, and the TUI `m` +# key expands it to a key/value box. When false, metadata is hidden entirely +# (it is still parsed, so it never leaks into body content). +show = true + +[font.heading] +# Fonts for image-rendered headings (H1–H3). When unset, termdown uses +# platform defaults and falls back to an embedded SourceSerif4 font. Body +# text always uses your terminal's own font, not these. + +# Latin / ASCII heading font (sans-serif recommended). +# latin = "Inter" + +# CJK heading font. +# cjk = "LXGW WenKai" + +# Emoji / symbol fallback font for headings. +# emoji = "Apple Color Emoji" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5178227..633e31d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -5,7 +5,7 @@ ``` src/ ├── main.rs CLI entry point, arg parsing, terminal state management -├── config.rs ~/.termdown/config.toml loading (serde) +├── config.rs ~/.config/termdown/config.toml loading (serde) ├── font.rs Font resolution, caching, CJK/Latin detection ├── style.rs HeadingStyle, ANSI constants, display width utilities ├── render.rs Text measurement, glyph drawing, PNG encoding, Kitty protocol @@ -145,7 +145,7 @@ On UNIX, `main.rs` temporarily disables terminal echo before rendering and resto ## Configuration -`~/.termdown/config.toml` is deserialized via serde: +`~/.config/termdown/config.toml` is deserialized via serde: ``` Config diff --git a/docs/MARKDOWN_FEATURE_COVERAGE.md b/docs/MARKDOWN_FEATURE_COVERAGE.md index 9ea2753..cc69197 100644 --- a/docs/MARKDOWN_FEATURE_COVERAGE.md +++ b/docs/MARKDOWN_FEATURE_COVERAGE.md @@ -18,7 +18,7 @@ Audit of `src/markdown.rs` against pulldown-cmark 0.13 and common Markdown exten | Horizontal rule | ✓ | | | HTML blocks | ✓ | Rendered verbatim as a dim preformatted block; HTML comments dropped | | Inline HTML | ⚠ | Format tags (`b`/`strong`, `i`/`em`, `u`, `s`/`del`/`strike`, `code`/`kbd`) map to ANSI; `
` / `
` handled; comments dropped; unknown tags stripped but their content is preserved. Attributes (e.g. `style="color:red"`, `href`) are not interpreted. | -| YAML / TOML frontmatter | ✓ | Parsed via pulldown-cmark's metadata-block extensions. Rendered as a dim one-line summary (`[metadata · key=value, …]`) in `--cat`; foldable inline box in TUI (toggle with `m`). Heuristic key/value extraction. See `docs/adr/0001-metadata-block-handling.md`. Opt out via `[metadata] show = false` in `~/.termdown/config.toml`. | +| YAML / TOML frontmatter | ✓ | Parsed via pulldown-cmark's metadata-block extensions. Rendered as a dim one-line summary (`[metadata · key=value, …]`) in `--cat`; foldable inline box in TUI (toggle with `m`). Heuristic key/value extraction. See `docs/adr/0001-metadata-block-handling.md`. Opt out via `[metadata] show = false` in `~/.config/termdown/config.toml`. | ## Enabled GFM extensions diff --git a/docs/TUI_MODE_DESIGN.md b/docs/TUI_MODE_DESIGN.md index a2c64e1..d24fc47 100644 --- a/docs/TUI_MODE_DESIGN.md +++ b/docs/TUI_MODE_DESIGN.md @@ -31,7 +31,7 @@ if you want the "why this stack"; this doc is the "what we're building". - Future evolution (documented but not implemented in v1): - Automatic mode when output is a TTY and the rendered document exceeds terminal height (git-log-style). - - `[tui]` section in `~/.termdown/config.toml` to opt into automatic + - `[tui]` section in `~/.config/termdown/config.toml` to opt into automatic mode or override defaults. Single binary, no cargo feature flag. TUI code is always compiled in; diff --git a/docs/adr/0001-metadata-block-handling.md b/docs/adr/0001-metadata-block-handling.md index 26d3707..66c2d80 100644 --- a/docs/adr/0001-metadata-block-handling.md +++ b/docs/adr/0001-metadata-block-handling.md @@ -42,7 +42,7 @@ as body content and not as completely invisible noise: own row. The box is part of the scrolling content (not pinned), not part of search, and not part of the Table of Contents. Default state is folded. 6. **Config gate**: A single boolean knob `[metadata] show` (default `true`) - in `~/.termdown/config.toml`. When `false`, frontmatter is **completely + in `~/.config/termdown/config.toml`. When `false`, frontmatter is **completely hidden** in both cat and TUI; `m` becomes a no-op. The pulldown extensions remain enabled — `show` gates rendering only, never parsing, so frontmatter never leaks back into body regardless of config. @@ -128,7 +128,8 @@ as body content and not as completely invisible noise: ## Open follow-ups -- Migration of config dir from `~/.termdown/` to XDG `~/.config/termdown/` - is unrelated and explicitly out of scope here. +- Migration of the config dir from `~/.termdown/` to XDG `~/.config/termdown/` + is handled alongside this change (same branch); see `config.example.toml` + and the `## Configuration` section of the README. - "Use `title` field in TUI title bar" — sketched as a future enhancement, not committed. diff --git a/src/config.rs b/src/config.rs index cb3d2a3..cd6f9f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use serde::Deserialize; @@ -61,8 +61,17 @@ pub struct HeadingFontConfig { } fn config_dir() -> Option { + // XDG Base Directory spec: prefer an absolute $XDG_CONFIG_HOME, otherwise + // fall back to ~/.config (USERPROFILE covers Windows, where HOME is often + // unset). The config file itself lives at `/termdown/config.toml`. + if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") { + let xdg = PathBuf::from(xdg); + if xdg.is_absolute() { + return Some(xdg.join("termdown")); + } + } let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?; - Some(PathBuf::from(home).join(".termdown")) + Some(PathBuf::from(home).join(".config").join("termdown")) } pub fn load() -> Config { @@ -76,6 +85,54 @@ pub fn load() -> Config { eprintln!("Warning: invalid config at {}: {}", path.display(), e); Config::default() }), - Err(_) => Config::default(), + Err(_) => { + warn_if_legacy_config(&path); + Config::default() + } + } +} + +/// Nudge users upgrading from the pre-XDG layout: if nothing was found at the +/// new path but a config still sits at the old `~/.termdown/config.toml`, warn +/// so the settings aren't silently dropped. Stays silent on a clean install +/// (no legacy file) and matches `uninstall.sh`'s legacy-cleanup awareness. +fn warn_if_legacy_config(new_path: &Path) { + let home = match std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) { + Some(home) => home, + None => return, + }; + let legacy = PathBuf::from(home).join(".termdown").join("config.toml"); + if legacy.is_file() { + eprintln!( + "Warning: ignoring legacy config at {}; move it to {}", + legacy.display(), + new_path.display() + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `config.example.toml` is the user-facing source of truth for defaults, + /// so guard it against bit-rot: it must always parse, and its explicit + /// values must match what termdown treats as the built-in defaults. + #[test] + fn example_config_parses_and_matches_defaults() { + let example = include_str!("../config.example.toml"); + let parsed: Config = + toml::from_str(example).expect("config.example.toml must remain valid TOML"); + + // The example spells out the *effective* defaults explicitly. + assert_eq!(parsed.theme.as_deref(), Some("auto")); + assert_eq!(parsed.bell, Some(true)); + // `metadata.show` is a real struct default — assert the example tracks it. + assert_eq!(parsed.metadata.show, Config::default().metadata.show); + assert!(parsed.metadata.show); + // Font overrides are commented out, so they must parse as unset. + assert!(parsed.font.heading.latin.is_none()); + assert!(parsed.font.heading.cjk.is_none()); + assert!(parsed.font.heading.emoji.is_none()); } } diff --git a/src/main.rs b/src/main.rs index 7f7ef18..9c7ce68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ fn main() { println!("By default, passing FILE opens it in the interactive TUI."); println!("Piped/redirected stdout and stdin input automatically use cat mode."); println!(); - println!("Config: ~/.termdown/config.toml"); + println!("Config: ~/.config/termdown/config.toml"); return; } diff --git a/tests/cli.rs b/tests/cli.rs index ef48426..ed1209c 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -19,6 +19,13 @@ fn run_termdown( command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); + // Isolate config loading from the developer/CI environment so a real + // ~/.config/termdown/config.toml (or $XDG_CONFIG_HOME) can't leak in. + // Callers may re-set any of these via `envs`. + command.env_remove("HOME"); + command.env_remove("USERPROFILE"); + command.env_remove("XDG_CONFIG_HOME"); + if stdin.is_some() { command.stdin(Stdio::piped()); } @@ -122,6 +129,54 @@ impl Drop for TempMarkdownFile { } } +/// A throwaway directory tree, cleaned up on drop. Files are written at +/// caller-chosen relative paths so the same helper can stage either an XDG +/// config root (`termdown/config.toml`) or a fake `$HOME` (`.termdown/...`). +struct TempDir { + root: PathBuf, +} + +impl TempDir { + fn new() -> Self { + static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should move forward") + .as_nanos(); + let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "termdown-cfg-{}-{}-{}", + std::process::id(), + unique, + seq + )); + fs::create_dir_all(&root).expect("failed to create temp dir"); + Self { root } + } + + fn write(&self, rel: &str, contents: &str) { + let path = self.root.join(rel); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("failed to create temp subdir"); + } + fs::write(path, contents).expect("failed to write temp file"); + } + + fn path_str(&self) -> &str { + self.root.to_str().expect("temp path should be valid UTF-8") + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +// Body is a plain paragraph (not a heading) so it renders as text under +// ghostty rather than as a Kitty-graphics image. +const FRONTMATTER_DOC: &str = "---\ntitle: Hello\nauthor: Me\n---\n\nPlain body text.\n"; + #[test] fn help_flag_prints_usage() { let output = run_termdown(&["--help"], None, &[], &[]); @@ -130,7 +185,7 @@ fn help_flag_prints_usage() { assert!(output.status.success()); assert!(stdout.contains("Render Markdown with large-font headings in the terminal")); assert!(stdout.contains("Usage:")); - assert!(stdout.contains("Config: ~/.termdown/config.toml")); + assert!(stdout.contains("Config: ~/.config/termdown/config.toml")); assert!(stderr_text(&output).trim().is_empty()); } @@ -272,3 +327,79 @@ fn cat_flag_forces_cat_output_with_file() { assert!(output.status.success(), "stderr: {}", stderr_text(&output)); assert!(stdout.contains("plain content"), "stdout was: {stdout:?}"); } + +#[test] +fn xdg_config_home_is_honored_for_config_loading() { + let doc = TempMarkdownFile::new(FRONTMATTER_DOC); + let path = doc.path().to_str().expect("path should be valid UTF-8"); + + // Baseline: with no config at all, the folded metadata summary is shown. + let baseline = run_termdown(&["--cat", path], None, &[("TERM_PROGRAM", "ghostty")], &[]); + let baseline_out = strip_ansi(&stdout_text(&baseline)); + assert!( + baseline.status.success(), + "stderr: {}", + stderr_text(&baseline) + ); + assert!( + baseline_out.contains("[metadata ·"), + "baseline should show the metadata summary, was: {baseline_out:?}" + ); + + // A `config.toml` placed at `$XDG_CONFIG_HOME/termdown/` must take effect: + // `show = false` hides the summary the baseline rendered. + let cfg = TempDir::new(); + cfg.write("termdown/config.toml", "[metadata]\nshow = false\n"); + let configured = run_termdown( + &["--cat", path], + None, + &[ + ("TERM_PROGRAM", "ghostty"), + ("XDG_CONFIG_HOME", cfg.path_str()), + ], + &[], + ); + let configured_out = strip_ansi(&stdout_text(&configured)); + assert!( + configured.status.success(), + "stderr: {}", + stderr_text(&configured) + ); + assert!( + !configured_out.contains("[metadata"), + "config should hide the metadata summary, was: {configured_out:?}" + ); + assert!( + configured_out.contains("Plain body text"), + "body should still render, was: {configured_out:?}" + ); +} + +#[test] +fn legacy_config_location_triggers_migration_warning() { + // A fake $HOME that still holds the pre-XDG `~/.termdown/config.toml` but + // has no config at the new XDG path. termdown should ignore the legacy + // file and warn the user to move it, instead of failing silently. + let home = TempDir::new(); + home.write(".termdown/config.toml", "theme = \"dark\"\n"); + + let doc = TempMarkdownFile::new("hello\n"); + let path = doc.path().to_str().expect("path should be valid UTF-8"); + let output = run_termdown( + &["--cat", path], + None, + &[("TERM_PROGRAM", "ghostty"), ("HOME", home.path_str())], + &[], + ); + + assert!(output.status.success(), "stderr: {}", stderr_text(&output)); + let stderr = stderr_text(&output); + assert!( + stderr.contains("ignoring legacy config"), + "expected a migration warning, stderr was: {stderr:?}" + ); + assert!( + stderr.contains(".termdown/config.toml"), + "warning should name the legacy path, stderr was: {stderr:?}" + ); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1544985..2f210c6 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,8 +7,9 @@ pub fn binary_path() -> &'static str { /// Run the compiled termdown binary against `path` in a controlled test /// environment: ghostty-like terminal (so kitty graphics emission is enabled), -/// dark theme, and `HOME`/`USERPROFILE` cleared so a developer's -/// `~/.termdown/config.toml` can't leak into the test. Returns raw stdout +/// dark theme, and `HOME`/`USERPROFILE`/`XDG_CONFIG_HOME` cleared so a +/// developer's `~/.config/termdown/config.toml` can't leak into the test. +/// Returns raw stdout /// bytes; callers decide whether to treat it as UTF-8 or scan for kitty APC /// payloads. pub fn run_termdown(path: &Path) -> Vec { @@ -19,6 +20,7 @@ pub fn run_termdown(path: &Path) -> Vec { .env("TERM_PROGRAM", "ghostty") .env_remove("HOME") .env_remove("USERPROFILE") + .env_remove("XDG_CONFIG_HOME") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/uninstall.sh b/uninstall.sh index ea979d4..beadf5e 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # termdown uninstaller # -# Removes the installed binary and deletes the config directory (~/.termdown). +# Removes the installed binary and deletes the config directory (~/.config/termdown). # Never invokes sudo; if the binary's location is not writable, prints a clear # hint and exits. # @@ -10,7 +10,7 @@ # # Environment variables: # TERMDOWN_INSTALL_DIR location of the binary (default: auto-detect via `command -v`) -# TERMDOWN_KEEP_CONFIG set to 1 to keep ~/.termdown (default: remove it) +# TERMDOWN_KEEP_CONFIG set to 1 to keep the config dir (default: remove it) set -euo pipefail @@ -43,10 +43,22 @@ EOF fi if [ "${TERMDOWN_KEEP_CONFIG:-0}" = "1" ]; then - info "Keeping config directory (~/.termdown)" + info "Keeping config directory (~/.config/termdown)" else + # Mirror the binary's XDG logic (see src/config.rs): honor $XDG_CONFIG_HOME + # only when it is an absolute path, otherwise fall back to ~/.config. + if [ -n "${XDG_CONFIG_HOME:-}" ] && [[ "$XDG_CONFIG_HOME" == /* ]]; then + CONFIG_DIR="$XDG_CONFIG_HOME/termdown" + else + CONFIG_DIR="$HOME/.config/termdown" + fi + if [ -d "$CONFIG_DIR" ]; then + info "Removing config directory (${CONFIG_DIR})" + rm -rf "$CONFIG_DIR" + fi + # Clean up the older config location too, if present. if [ -d "$HOME/.termdown" ]; then - info "Removing config directory (~/.termdown)" + info "Removing legacy config directory (~/.termdown)" rm -rf "$HOME/.termdown" fi fi From 1079add3e2898e74410a9c95c55951672d0d11cd Mon Sep 17 00:00:00 2001 From: shawn Date: Fri, 29 May 2026 11:30:59 +0800 Subject: [PATCH 2/2] test: make legacy-config warning assertion path-separator agnostic The Windows CI run failed because `Path::display` emits `\` separators, so `contains(".termdown/config.toml")` never matched. Assert on the distinctive `.termdown` legacy dir name instead, which holds on all platforms. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/cli.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/cli.rs b/tests/cli.rs index ed1209c..f7d08eb 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -398,8 +398,11 @@ fn legacy_config_location_triggers_migration_warning() { stderr.contains("ignoring legacy config"), "expected a migration warning, stderr was: {stderr:?}" ); + // Match the legacy dir name only (not a `/`-joined path) so the assertion + // holds on Windows, where `Path::display` uses `\` separators. The new + // path is `.configtermdown`, which never contains `.termdown`. assert!( - stderr.contains(".termdown/config.toml"), + stderr.contains(".termdown"), "warning should name the legacy path, stderr was: {stderr:?}" ); }