use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
fn binary() -> &'static str {
env!("CARGO_BIN_EXE_colorant")
}
fn run_in(cwd: &Path, args: &[&str], envs: &[(&str, &str)]) -> (String, String, i32) {
let mut cmd = Command::new(binary());
cmd.current_dir(cwd).args(args);
for var in [
"COLORANT_MODE",
"TMUX",
"HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"TERM_PROGRAM",
"LC_TERMINAL",
"WEZTERM_PANE",
"TERM",
"NO_COLOR",
] {
cmd.env_remove(var);
}
for (k, v) in envs {
cmd.env(k, v);
}
let out = cmd.output().expect("running colorant");
(
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
out.status.code().unwrap_or(-1),
)
}
fn make_workspace() -> TempDir {
tempfile::tempdir().expect("creating tempdir")
}
fn setup_xdg(ws: &TempDir) -> (std::path::PathBuf, std::path::PathBuf) {
let xdg = ws.path().join("xdg");
let themes = xdg.join("colorant").join("themes");
fs::create_dir_all(&themes).unwrap();
(xdg, themes)
}
#[test]
fn current_finds_nearest_rc() {
let ws = make_workspace();
let nested = ws.path().join("a/b/c");
fs::create_dir_all(&nested).unwrap();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, _, code) = run_in(&nested, &["current"], &[]);
assert_eq!(code, 0);
assert!(
stdout.trim().ends_with(".colorantrc"),
"stdout was: {stdout:?}"
);
}
#[test]
fn current_prints_nothing_when_missing() {
let ws = make_workspace();
let nested = ws.path().join("a/b");
fs::create_dir_all(&nested).unwrap();
let (stdout, _, code) = run_in(&nested, &["current"], &[]);
assert_eq!(code, 0);
assert_eq!(stdout.trim(), "");
}
#[test]
fn init_zsh_emits_hook() {
let ws = make_workspace();
let (stdout, _, code) = run_in(ws.path(), &["init", "zsh"], &[]);
assert_eq!(code, 0);
assert!(stdout.contains("add-zsh-hook chpwd"));
assert!(stdout.contains("add-zsh-hook precmd"));
assert!(stdout.contains("# >>> colorant init >>>"));
}
#[test]
fn apply_no_op_on_unsupported_terminal() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "Apple_Terminal"),
("LC_TERMINAL", "screen-256color"),
("TERM", "xterm-256color"),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert_eq!(stdout, "");
}
#[test]
fn apply_emits_osc_in_ghostty() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"fg = #abcdef\nbg = #112233\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "ghostty"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
assert!(
stdout.contains("\x1b]11;#112233\x07"),
"missing bg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_emits_osc_in_iterm2() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"fg = #abcdef\nbg = #112233\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "iTerm.app"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
assert!(
stdout.contains("\x1b]11;#112233\x07"),
"missing bg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_emits_osc_when_lc_terminal_is_iterm2() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("LC_TERMINAL", "iTerm2"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_emits_osc_when_term_is_xterm_ghostty() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM", "xterm-ghostty"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_emits_osc_in_wezterm() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "WezTerm"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_emits_osc_when_only_wezterm_pane_is_set() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "tmux"),
("WEZTERM_PANE", "0"),
("COLORANT_MODE", "dark"),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_does_not_emit_tab_osc_in_wezterm_even_with_tab_bg() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"bg = #1e1e2e\ntab_bg = #cdd6f4\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "WezTerm"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]11;#1e1e2e\x07"),
"missing bg OSC, got: {stdout:?}"
);
assert!(
!stdout.contains("\x1b]1337"),
"WezTerm must not see OSC 1337, got: {stdout:?}"
);
}
#[test]
fn reset_emits_standard_resets_without_tab_osc_in_wezterm() {
let ws = make_workspace();
let (stdout, stderr, code) = run_in(ws.path(), &["reset"], &[("TERM_PROGRAM", "WezTerm")]);
assert_eq!(code, 0, "stderr: {stderr}");
for seq in [
"\x1b]110\x07",
"\x1b]111\x07",
"\x1b]112\x07",
"\x1b]104;15\x07",
] {
assert!(stdout.contains(seq), "missing {seq:?}, got: {stdout:?}");
}
assert!(
!stdout.contains("\x1b]1337"),
"WezTerm must not see OSC 1337 on reset, got: {stdout:?}"
);
}
#[test]
fn term_program_beats_inherited_wezterm_pane() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "tab_bg = #cdd6f4\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "iTerm.app"),
("WEZTERM_PANE", "0"),
("COLORANT_MODE", "dark"),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]1337;SetColors=tab=cdd6f4\x07"),
"expected iTerm2 tab OSC, got: {stdout:?}"
);
}
#[test]
fn apply_emits_tab_osc_in_iterm2_when_tab_bg_set() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"bg = #1e1e2e\ntab_bg = #cdd6f4\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "iTerm.app"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]1337;SetColors=tab=cdd6f4\x07"),
"expected iTerm2 tab OSC for tab_bg, got: {stdout:?}"
);
}
#[test]
fn apply_does_not_emit_tab_osc_in_ghostty_even_with_tab_bg() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"bg = #1e1e2e\ntab_bg = #cdd6f4\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "ghostty"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
!stdout.contains("\x1b]1337"),
"Ghostty must not see OSC 1337, got: {stdout:?}"
);
}
#[test]
fn apply_derives_tab_from_bg_when_tab_follows_window_default() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
fs::write(ws.path().join(".colorantrc"), "bg = #1e1e2e\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "iTerm.app"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]1337;SetColors=tab=1e1e2e\x07"),
"expected tab to derive from bg, got: {stdout:?}"
);
}
#[test]
fn apply_skips_tab_derive_when_tab_follows_window_false() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let cfg_path = xdg.join("colorant").join("config.toml");
fs::write(&cfg_path, "tab_follows_window = false\n").unwrap();
fs::write(ws.path().join(".colorantrc"), "bg = #1e1e2e\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "iTerm.app"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
!stdout.contains("\x1b]1337"),
"tab_follows_window=false must skip tab OSC, got: {stdout:?}"
);
}
#[test]
fn apply_explicit_tab_bg_beats_bg_in_iterm2() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"bg = #1e1e2e\ntab_bg = #ff00ff\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "iTerm.app"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]1337;SetColors=tab=ff00ff\x07"),
"expected explicit tab_bg, got: {stdout:?}"
);
assert!(
!stdout.contains("\x1b]1337;SetColors=tab=1e1e2e\x07"),
"explicit tab_bg must beat bg-derived value, got: {stdout:?}"
);
}
#[test]
fn reset_emits_tab_default_in_iterm2() {
let ws = make_workspace();
let (stdout, stderr, code) = run_in(ws.path(), &["reset"], &[("TERM_PROGRAM", "iTerm.app")]);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]1337;SetColors=tab=default\x07"),
"expected tab reset OSC in iTerm2, got: {stdout:?}"
);
}
#[test]
fn reset_does_not_emit_tab_osc_in_ghostty() {
let ws = make_workspace();
let (stdout, stderr, code) = run_in(ws.path(), &["reset"], &[("TERM_PROGRAM", "ghostty")]);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
!stdout.contains("\x1b]1337"),
"Ghostty must not see OSC 1337 on reset, got: {stdout:?}"
);
}
#[test]
fn apply_emits_tab_osc_for_tab_only_theme_in_iterm2() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "tab_bg = #cdd6f4\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "iTerm.app"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]1337;SetColors=tab=cdd6f4\x07"),
"expected tab OSC for tab-only theme, got: {stdout:?}"
);
assert!(
!stdout.contains("\x1b]110\x07"),
"tab-only theme must not route to emit_reset, got: {stdout:?}"
);
}
#[test]
fn apply_emits_tab_reset_when_follow_wants_to_derive_but_no_bg() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "iTerm.app"), ("COLORANT_MODE", "dark")],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
assert!(
stdout.contains("\x1b]1337;SetColors=tab=default\x07"),
"expected tab reset when derive wants to fire but bg is absent, got: {stdout:?}"
);
}
#[test]
fn apply_does_not_emit_tab_reset_when_follow_disabled_and_no_bg() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let cfg_path = xdg.join("colorant").join("config.toml");
fs::write(&cfg_path, "tab_follows_window = false\n").unwrap();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "iTerm.app"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
!stdout.contains("\x1b]1337"),
"tab_follows_window=false must leave the tab untouched, got: {stdout:?}"
);
}
#[test]
fn tab_osc_is_dcs_wrapped_inside_tmux_in_iterm2() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "tab_bg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "iTerm.app"),
("COLORANT_MODE", "dark"),
("TMUX", "/tmp/tmux-fake,0,0"),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1bPtmux;\x1b\x1b]1337;SetColors=tab=abcdef\x07\x1b\\"),
"expected DCS-wrapped tab OSC, got: {stdout:?}"
);
}
#[test]
fn term_program_beats_lc_terminal_when_both_set() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("LC_TERMINAL", "iTerm2"),
("COLORANT_MODE", "dark"),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07"),
"missing fg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_wraps_osc_in_tmux_dcs_when_tmux_is_set() {
let ws = make_workspace();
fs::write(ws.path().join(".colorantrc"), "fg = #abcdef\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "dark"),
("TMUX", "/tmp/tmux-fake,0,0"),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1bPtmux;\x1b\x1b]10;#abcdef\x07\x1b\\"),
"expected DCS-wrapped fg OSC, got: {stdout:?}"
);
}
#[test]
fn apply_respects_dark_section_override() {
let ws = make_workspace();
fs::write(
ws.path().join(".colorantrc"),
"fg = #aaaaaa\n[dark]\nfg = #111111\n[light]\nfg = #eeeeee\n",
)
.unwrap();
let (stdout, _, _) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "ghostty"), ("COLORANT_MODE", "dark")],
);
assert!(
stdout.contains("\x1b]10;#111111\x07"),
"expected dark fg override, got: {stdout:?}"
);
let (stdout, _, _) = run_in(
ws.path(),
&["apply"],
&[("TERM_PROGRAM", "ghostty"), ("COLORANT_MODE", "light")],
);
assert!(
stdout.contains("\x1b]10;#eeeeee\x07"),
"expected light fg override, got: {stdout:?}"
);
}
#[test]
fn child_top_level_beats_extended_palette() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(
themes_dir.join("parent.colorant"),
"fg = #ff0000\nbg = #ff0000\n",
)
.unwrap();
let work_dir = ws.path().join("work");
fs::create_dir_all(&work_dir).unwrap();
fs::write(
work_dir.join(".colorantrc"),
"extends = parent\nbg = #00ff00\n",
)
.unwrap();
let (stdout, stderr, code) = run_in(
&work_dir,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#ff0000\x07"),
"expected inherited fg, got: {stdout:?}"
);
assert!(
stdout.contains("\x1b]11;#00ff00\x07"),
"expected child top-level bg to win, got: {stdout:?}"
);
}
#[test]
fn per_mode_extends_pick_different_palettes() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(
themes_dir.join("nightly.colorant"),
"fg = #111111\nbg = #000000\n",
)
.unwrap();
fs::write(
themes_dir.join("daily.colorant"),
"fg = #eeeeee\nbg = #ffffff\n",
)
.unwrap();
let work_dir = ws.path().join("work");
fs::create_dir_all(&work_dir).unwrap();
fs::write(
work_dir.join(".colorantrc"),
"extends.dark = nightly\nextends.light = daily\n",
)
.unwrap();
let (stdout, _, _) = run_in(
&work_dir,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert!(
stdout.contains("\x1b]10;#111111\x07") && stdout.contains("\x1b]11;#000000\x07"),
"dark mode should use nightly: {stdout:?}"
);
let (stdout, _, _) = run_in(
&work_dir,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "light"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert!(
stdout.contains("\x1b]10;#eeeeee\x07") && stdout.contains("\x1b]11;#ffffff\x07"),
"light mode should use daily: {stdout:?}"
);
}
#[test]
fn per_mode_extends_overrides_global_extends_in_that_mode() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(themes_dir.join("base.colorant"), "fg = #aaaaaa\n").unwrap();
fs::write(themes_dir.join("override.colorant"), "fg = #bbbbbb\n").unwrap();
let work_dir = ws.path().join("work");
fs::create_dir_all(&work_dir).unwrap();
fs::write(
work_dir.join(".colorantrc"),
"extends = base\nextends.dark = override\n",
)
.unwrap();
let (stdout, _, _) = run_in(
&work_dir,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert!(
stdout.contains("\x1b]10;#bbbbbb\x07"),
"dark should pick `override`: {stdout:?}"
);
let (stdout, _, _) = run_in(
&work_dir,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "light"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert!(
stdout.contains("\x1b]10;#aaaaaa\x07"),
"light should fall through to `base`: {stdout:?}"
);
}
#[test]
fn dark_only_palette_in_light_mode_emits_reset() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(
themes_dir.join("nightly.colorant"),
"fg = #111111\nbg = #000000\n",
)
.unwrap();
let work_dir = ws.path().join("work");
fs::create_dir_all(&work_dir).unwrap();
fs::write(work_dir.join(".colorantrc"), "extends.dark = nightly\n").unwrap();
let (stdout, _, _) = run_in(
&work_dir,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "light"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert!(
stdout.contains("\x1b]110\x07")
&& stdout.contains("\x1b]111\x07")
&& stdout.contains("\x1b]112\x07"),
"expected reset OSCs, got: {stdout:?}"
);
assert!(
!stdout.contains("\x1b]10;#"),
"should not have emitted any fg setter: {stdout:?}"
);
}
#[test]
fn default_theme_applies_when_no_rc_found() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(
themes_dir.join("fallback.colorant"),
"fg = #abcdef\nbg = #123456\n",
)
.unwrap();
let cfg_path = xdg.join("colorant").join("config.toml");
fs::write(&cfg_path, "default_theme = \"fallback\"\n").unwrap();
let nested = ws.path().join("nowhere");
fs::create_dir_all(&nested).unwrap();
let (stdout, stderr, code) = run_in(
&nested,
&["apply"],
&[
("TERM_PROGRAM", "ghostty"),
("COLORANT_MODE", "dark"),
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("\x1b]10;#abcdef\x07") && stdout.contains("\x1b]11;#123456\x07"),
"expected fallback palette OSCs: {stdout:?}"
);
}
#[test]
fn themes_list_enumerates_bundled_palettes() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (stdout, stderr, code) = run_in(
ws.path(),
&["themes", "list", "--source", "bundled"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
for expected in ["catppuccin-mocha", "tokyo-night", "nord"] {
assert!(
stdout
.lines()
.any(|l| l.starts_with("[bundled] ") && l.contains(expected)),
"expected {expected:?} in list output: {stdout:?}"
);
}
}
#[test]
fn themes_list_marks_installed_palettes() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(themes_dir.join("nord.colorant"), "fg = #aaaaaa\n").unwrap();
let (stdout, _, code) = run_in(
ws.path(),
&["themes", "list", "--source", "bundled"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
assert!(
stdout.lines().any(|l| l == "[bundled] nord (installed)"),
"expected nord to be marked installed: {stdout:?}"
);
assert!(
stdout.lines().any(|l| l == "[bundled] catppuccin-mocha"),
"expected catppuccin-mocha unmarked: {stdout:?}"
);
}
#[test]
fn themes_path_prints_resolved_themes_dir() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
let (stdout, _, code) = run_in(
ws.path(),
&["themes", "path"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
assert_eq!(stdout.trim(), themes_dir.to_str().unwrap());
}
#[test]
fn themes_install_one_creates_file() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "install", "nord"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
let installed = themes_dir.join("nord.colorant");
assert!(
installed.is_file(),
"expected nord.colorant at {installed:?}"
);
let content = fs::read_to_string(&installed).unwrap();
assert!(
content.contains("fg") && content.contains("bg"),
"palette looks empty: {content:?}"
);
}
#[test]
fn themes_install_creates_missing_themes_dir() {
let ws = make_workspace();
let xdg = ws.path().join("xdg");
fs::create_dir_all(xdg.join("colorant")).unwrap();
let themes_dir = xdg.join("colorant").join("themes");
assert!(!themes_dir.exists());
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "install", "nord"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(themes_dir.join("nord.colorant").is_file());
}
#[test]
fn themes_install_one_refuses_overwrite_without_force() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(themes_dir.join("nord.colorant"), "old\n").unwrap();
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "install", "nord"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_ne!(code, 0, "expected non-zero exit; stderr: {stderr}");
assert!(
stderr.contains("--force"),
"expected --force hint in stderr: {stderr:?}"
);
assert_eq!(
fs::read_to_string(themes_dir.join("nord.colorant")).unwrap(),
"old\n"
);
}
#[test]
fn themes_install_one_force_overwrites() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(themes_dir.join("nord.colorant"), "old\n").unwrap();
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "install", "nord", "--force"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
let content = fs::read_to_string(themes_dir.join("nord.colorant")).unwrap();
assert_ne!(content, "old\n", "expected overwrite");
assert!(content.contains("fg"));
}
#[test]
fn themes_install_unknown_name_errors() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "install", "definitely-not-a-real-theme"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_ne!(code, 0);
assert!(
stderr.contains("no bundled palette"),
"expected 'no bundled palette' in stderr: {stderr:?}"
);
}
#[test]
fn themes_install_all_populates_themes_dir() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
let (stdout, stderr, code) = run_in(
ws.path(),
&["themes", "install", "--all"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
assert!(
stdout.contains("installed"),
"expected install summary: {stdout:?}"
);
for name in ["catppuccin-mocha", "tokyo-night", "nord"] {
let path = themes_dir.join(format!("{name}.colorant"));
assert!(path.is_file(), "expected {path:?} after --all");
}
}
#[test]
fn themes_install_all_is_idempotent_skipping_existing() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
let (_, _, code) = run_in(
ws.path(),
&["themes", "install", "--all"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
let nord = themes_dir.join("nord.colorant");
fs::write(&nord, "sentinel\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["themes", "install", "--all"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "second run must exit 0; stderr: {stderr}");
assert!(
stdout.contains("skipping") && stdout.contains("nord"),
"expected skip messages: {stdout:?}"
);
assert_eq!(
fs::read_to_string(&nord).unwrap(),
"sentinel\n",
"sentinel must survive a no-force --all"
);
}
#[test]
fn themes_install_all_force_overwrites_everything() {
let ws = make_workspace();
let (xdg, themes_dir) = setup_xdg(&ws);
fs::write(themes_dir.join("nord.colorant"), "sentinel\n").unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["themes", "install", "--all", "--force"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
let content = fs::read_to_string(themes_dir.join("nord.colorant")).unwrap();
assert_ne!(content, "sentinel\n");
assert!(
stdout.contains("1 overwritten"),
"expected summary to report 1 overwritten, got: {stdout:?}"
);
}
#[test]
fn themes_search_filters_bundled_substring_case_insensitively() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (stdout, _, code) = run_in(
ws.path(),
&["themes", "search", "Mocha", "--source", "bundled"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
assert!(
stdout.lines().any(|l| l.contains("catppuccin-mocha")),
"expected catppuccin-mocha hit: {stdout:?}"
);
assert!(
!stdout.lines().any(|l| l.contains("ayu-")),
"non-matching theme should not appear: {stdout:?}"
);
}
#[test]
fn themes_search_reports_no_hits_for_unknown_query() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (stdout, _, code) = run_in(
ws.path(),
&[
"themes",
"search",
"zzzzz-no-such-theme",
"--source",
"bundled",
],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
assert!(stdout.contains("no themes matched"), "{stdout:?}");
}
#[test]
fn themes_apply_writes_global_extends_to_rc() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "apply", "catppuccin-mocha"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
let rc = fs::read_to_string(ws.path().join(".colorantrc")).unwrap();
assert!(
rc.contains("extends = catppuccin-mocha"),
"expected global extends: {rc:?}"
);
assert!(
xdg.join("colorant")
.join("themes")
.join("catppuccin-mocha.colorant")
.exists(),
"expected catppuccin-mocha.colorant in themes dir"
);
}
#[test]
fn themes_apply_dark_and_light_writes_per_mode_extends() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (_, _, code) = run_in(
ws.path(),
&[
"themes",
"apply",
"--dark",
"tokyo-night",
"--light",
"catppuccin-latte",
],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
let rc = fs::read_to_string(ws.path().join(".colorantrc")).unwrap();
assert!(rc.contains("extends.dark = tokyo-night"), "{rc:?}");
assert!(rc.contains("extends.light = catppuccin-latte"), "{rc:?}");
assert!(!rc.contains("extends = "), "no global extends: {rc:?}");
}
#[test]
fn themes_apply_preserves_other_base_keys() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let rc_path = ws.path().join(".colorantrc");
fs::write(
&rc_path,
"extends = old\nfg = #ff00ff\n[dark]\ncursor = #abcdef\n",
)
.unwrap();
let (_, _, code) = run_in(
ws.path(),
&["themes", "apply", "catppuccin-mocha"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0);
let rc = fs::read_to_string(&rc_path).unwrap();
assert!(rc.contains("extends = catppuccin-mocha"), "{rc:?}");
assert!(rc.contains("fg = #ff00ff"), "{rc:?}");
assert!(rc.contains("[dark]"), "{rc:?}");
assert!(rc.contains("cursor = #abcdef"), "{rc:?}");
assert!(!rc.contains("extends = old"), "{rc:?}");
}
#[test]
fn themes_apply_per_mode_preserves_existing_dark_section_block() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let rc_path = ws.path().join(".colorantrc");
fs::write(
&rc_path,
"fg = #ff00ff\n[dark]\ncursor = #abcdef\ncolor0 = #001122\n",
)
.unwrap();
let (_, stderr, code) = run_in(
ws.path(),
&[
"themes",
"apply",
"--dark",
"tokyo-night",
"--light",
"catppuccin-latte",
],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stderr: {stderr}");
let rc = fs::read_to_string(&rc_path).unwrap();
assert!(
rc.contains("[dark]\ncursor = #abcdef\ncolor0 = #001122"),
"[dark] block should be intact: {rc:?}"
);
assert!(rc.starts_with("extends.dark = tokyo-night"), "{rc:?}");
}
#[test]
fn themes_list_continues_past_unsynced_remote() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let cache = ws.path().join("cache");
fs::create_dir_all(&cache).unwrap();
let (stdout, stderr, code) = run_in(
ws.path(),
&["themes", "list"],
&[
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
("XDG_CACHE_HOME", cache.to_str().unwrap()),
],
);
assert_eq!(code, 0);
assert!(
stdout.lines().any(|l| l.starts_with("[bundled] ")),
"expected bundled themes on stdout: {stdout:?}"
);
assert!(
stderr.contains("[gogh]") && stderr.contains("not synced"),
"expected gogh warning on stderr: {stderr:?}"
);
}
#[test]
fn themes_apply_errors_on_unknown_theme() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "apply", "definitely-not-a-real-theme"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_ne!(code, 0);
assert!(
stderr.contains("not") && stderr.contains("installed") && stderr.contains("bundled"),
"expected guidance about lookup failure: {stderr:?}"
);
}
#[test]
fn themes_apply_requires_at_least_one_target() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "apply"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_ne!(code, 0);
assert!(
stderr.contains("--dark") || stderr.contains("theme name"),
"{stderr:?}"
);
}
#[test]
fn themes_install_without_name_or_all_errors() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let (_, stderr, code) = run_in(
ws.path(),
&["themes", "install"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_ne!(code, 0);
assert!(
stderr.contains("--all") || stderr.contains("palette name"),
"expected guidance on how to call install: {stderr:?}"
);
}
#[test]
fn doctor_clean_rc_exits_zero() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(themes.join("ayu.colorant"), "fg = #abcdef\n").unwrap();
fs::write(
ws.path().join(".colorantrc"),
"extends = ayu\nfg = #ffffff\n",
)
.unwrap();
let rc = ws.path().join(".colorantrc");
let (stdout, _, code) = run_in(
ws.path(),
&["doctor", rc.to_str().unwrap()],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("Palette(s):"), "{stdout}");
assert!(stdout.contains("No issues found."), "{stdout}");
assert_eq!(
stdout.matches("No parsing errors.").count(),
2,
"expected one confirmation per audited file: {stdout}"
);
assert!(!stdout.contains("(found)"), "{stdout}");
assert!(!stdout.contains("NOT FOUND"), "{stdout}");
}
#[test]
fn doctor_surfaces_palette_parsing_errors() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(
themes.join("dirty.colorant"),
"fg = #abcdef\nforground = #112233\nbg = nope\n",
)
.unwrap();
fs::write(ws.path().join(".colorantrc"), "extends = dirty\n").unwrap();
let rc = ws.path().join(".colorantrc");
let (stdout, _, code) = run_in(
ws.path(),
&["doctor", rc.to_str().unwrap()],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 1, "stdout: {stdout}");
assert!(stdout.contains("unknown key 'forground'"), "{stdout}");
assert!(stdout.contains("invalid color 'nope'"), "{stdout}");
assert_eq!(
stdout.matches("unknown key 'forground'").count(),
1,
"palette should be audited once even when both modes share it: {stdout}"
);
assert!(
stdout.contains("(same palette as above; not re-audited)"),
"{stdout}"
);
assert!(stdout.contains("Found 2 issues."), "{stdout}");
}
#[test]
fn doctor_walks_up_when_no_path_given() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let nested = ws.path().join("a/b/c");
fs::create_dir_all(&nested).unwrap();
fs::write(ws.path().join(".colorantrc"), "fg = #ffffff\n").unwrap();
let (stdout, _, _) = run_in(
&nested,
&["doctor"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert!(stdout.contains("Checking "), "{stdout}");
assert!(stdout.contains(".colorantrc"), "{stdout}");
}
#[test]
fn doctor_no_rc_anywhere_exits_one() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let nested = ws.path().join("a/b");
fs::create_dir_all(&nested).unwrap();
let (stdout, _, code) = run_in(
&nested,
&["doctor"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 1);
assert!(stdout.contains("No .colorantrc found"), "{stdout}");
}
#[test]
fn doctor_reports_every_drop_kind() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(themes.join("good.colorant"), "fg = #abcdef\n").unwrap();
let rc = ws.path().join("dirty.colorantrc");
fs::write(
&rc,
"extends = good\n\
extends.light = bad/name\n\
forground = #112233\n\
bg = nope\n\
[lite]\n\
cursor = #ff00ff\n\
no equals here\n",
)
.unwrap();
let (stdout, _, code) = run_in(
ws.path(),
&["doctor", rc.to_str().unwrap()],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 1, "stdout: {stdout}");
assert!(stdout.contains("invalid theme name 'bad/name'"), "{stdout}");
assert!(stdout.contains("unknown key 'forground'"), "{stdout}");
assert!(stdout.contains("invalid color 'nope'"), "{stdout}");
assert!(stdout.contains("unknown section [lite]"), "{stdout}");
assert!(stdout.contains("malformed line"), "{stdout}");
assert!(stdout.contains("Found 5 issues."), "{stdout}");
}
#[test]
fn doctor_reports_missing_extends_palette() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let rc = ws.path().join(".colorantrc");
fs::write(&rc, "extends = does-not-exist\n").unwrap();
let (stdout, _, code) = run_in(
ws.path(),
&["doctor", rc.to_str().unwrap()],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 1, "stdout: {stdout}");
assert!(stdout.contains("NOT FOUND"), "{stdout}");
assert!(stdout.contains("does-not-exist"), "{stdout}");
assert!(
stdout.contains("Found 2 issue"),
"missing palette is reported once per mode: {stdout}"
);
}
#[test]
fn doctor_explicit_path_that_doesnt_exist_errors_nonzero() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let missing = ws.path().join("nope.colorantrc");
let (_, stderr, code) = run_in(
ws.path(),
&["doctor", missing.to_str().unwrap()],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_ne!(code, 0);
assert!(
stderr.contains("reading") && stderr.contains("nope.colorantrc"),
"expected anyhow context naming the file, got: {stderr:?}"
);
}
#[test]
fn show_prints_resolved_colors_with_rc_and_palette() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(
themes.join("ayu.colorant"),
"fg = #abcdef\nbg = #001122\ncolor0 = #112233\n",
)
.unwrap();
fs::write(
ws.path().join(".colorantrc"),
"extends = ayu\ncursor = #ff00ff\n",
)
.unwrap();
let (stdout, _, code) = run_in(
ws.path(),
&["show"],
&[
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
("COLORANT_MODE", "dark"),
],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("Active theme for"), "{stdout}");
assert!(stdout.contains("Mode: dark"), "{stdout}");
assert!(stdout.contains("#abcdef"), "{stdout}");
assert!(stdout.contains("#001122"), "{stdout}");
assert!(stdout.contains("#ff00ff"), "{stdout}");
assert!(stdout.contains("color0"), "{stdout}");
assert!(stdout.contains("color15"), "{stdout}");
assert!(stdout.contains("(unset)"), "{stdout}");
}
#[test]
fn show_all_prints_both_modes() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(themes.join("dark-pal.colorant"), "fg = #000001\n").unwrap();
fs::write(themes.join("light-pal.colorant"), "fg = #fffffe\n").unwrap();
fs::write(
ws.path().join(".colorantrc"),
"extends.dark = dark-pal\nextends.light = light-pal\n",
)
.unwrap();
let (stdout, _, code) = run_in(
ws.path(),
&["show", "--all"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("Dark mode:"), "{stdout}");
assert!(stdout.contains("Light mode:"), "{stdout}");
assert!(stdout.contains("#000001"), "{stdout}");
assert!(stdout.contains("#fffffe"), "{stdout}");
assert!(!stdout.contains("Mode: dark"), "{stdout}");
}
#[test]
fn show_falls_back_to_default_theme_when_no_rc() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(themes.join("solo.colorant"), "fg = #777777\n").unwrap();
fs::write(
xdg.join("colorant").join("config.toml"),
"default_theme = \"solo\"\n",
)
.unwrap();
let nested = ws.path().join("a/b");
fs::create_dir_all(&nested).unwrap();
let (stdout, _, code) = run_in(
&nested,
&["show"],
&[
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
("COLORANT_MODE", "dark"),
],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("Default theme: solo"), "{stdout}");
assert!(stdout.contains("#777777"), "{stdout}");
}
#[test]
fn show_warns_when_default_theme_palette_is_missing() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
fs::write(
xdg.join("colorant").join("config.toml"),
"default_theme = \"ghost\"\n",
)
.unwrap();
let nested = ws.path().join("a/b");
fs::create_dir_all(&nested).unwrap();
let (stdout, _, code) = run_in(
&nested,
&["show"],
&[
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
("COLORANT_MODE", "dark"),
],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("Default theme: ghost"), "{stdout}");
assert!(
stdout.contains("(palette file not found"),
"expected missing-palette warning, got: {stdout}"
);
assert!(stdout.contains("ghost.colorant"), "{stdout}");
}
#[test]
fn show_with_no_rc_and_no_default_theme_prints_message() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let nested = ws.path().join("a/b");
fs::create_dir_all(&nested).unwrap();
let (stdout, _, code) = run_in(
&nested,
&["show"],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(
stdout.contains("No theme applies in this directory."),
"{stdout}"
);
}
#[test]
fn show_omits_ansi_escapes_when_piped() {
let ws = make_workspace();
let (xdg, themes) = setup_xdg(&ws);
fs::write(themes.join("p.colorant"), "fg = #abcdef\n").unwrap();
fs::write(ws.path().join(".colorantrc"), "extends = p\n").unwrap();
let (stdout, _, _) = run_in(
ws.path(),
&["show"],
&[
("XDG_CONFIG_HOME", xdg.to_str().unwrap()),
("COLORANT_MODE", "dark"),
],
);
assert!(
!stdout.contains("\x1b[38;2;"),
"expected no 24-bit color escapes in piped output: {stdout:?}"
);
assert!(stdout.contains('█'), "{stdout}");
}
#[test]
fn doctor_no_parent_palette_is_not_an_issue() {
let ws = make_workspace();
let (xdg, _) = setup_xdg(&ws);
let rc = ws.path().join(".colorantrc");
fs::write(&rc, "fg = #ffffff\nbg = #000000\n").unwrap();
let (stdout, _, code) = run_in(
ws.path(),
&["doctor", rc.to_str().unwrap()],
&[("XDG_CONFIG_HOME", xdg.to_str().unwrap())],
);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("no parent palette"), "{stdout}");
}