use std::path::Path;
use super::readers::{INDEXED_FALLBACK_THEME, TRUECOLOR_FALLBACK_THEME};
pub(super) const REFERENCE_CONFIG_TOML: &str = include_str!("../../config/config.toml");
pub(super) fn ensure_default_files_in(dir: &Path, truecolor: bool) {
if let Err(e) = std::fs::create_dir_all(dir) {
tracing::warn!(error = %e, dir = %dir.display(), "failed to create config dir");
return;
}
let themes_dir = dir.join("themes");
if let Err(e) = std::fs::create_dir_all(&themes_dir) {
tracing::warn!(error = %e, dir = %themes_dir.display(), "failed to create themes dir");
return;
}
let export_dir = dir.join("export");
if let Err(e) = std::fs::create_dir_all(&export_dir) {
tracing::warn!(error = %e, dir = %export_dir.display(), "failed to create export dir");
return;
}
write_if_absent(&dir.join("config.toml"), &seed_config_toml(truecolor));
write_if_absent(
&dir.join("keybindings.toml"),
include_str!("../../config/keybindings.toml"),
);
write_if_absent(
&export_dir.join("default.css.example"),
include_str!("../../config/export/default.css"),
);
}
fn seed_config_toml(truecolor: bool) -> String {
if truecolor {
return REFERENCE_CONFIG_TOML.to_owned();
}
REFERENCE_CONFIG_TOML.replacen(
&format!("theme = \"{TRUECOLOR_FALLBACK_THEME}\""),
&format!("theme = \"{INDEXED_FALLBACK_THEME}\""),
1,
)
}
fn write_if_absent(path: &Path, contents: &str) {
if path.exists() {
return;
}
if let Err(e) = std::fs::write(path, contents) {
tracing::warn!(error = %e, path = %path.display(), "failed to write default file");
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::config::Config;
fn strip_inline_comment(value: &str) -> &str {
let mut in_string = false;
for (i, c) in value.char_indices() {
match c {
'"' => in_string = !in_string,
'#' if !in_string => return &value[..i],
_ => {}
}
}
value
}
fn scalar_assignments(src: &str) -> HashMap<String, String> {
let mut section = String::new();
let mut out = HashMap::new();
for raw in src.lines() {
let line = raw.trim_start();
let line = line.strip_prefix('#').map_or(line, str::trim_start);
if let Some(rest) = line.strip_prefix('[') {
if let Some(name) = rest.split(']').next() {
section = name.trim().to_string();
}
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
let key = key.trim();
if key.is_empty() || !key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
continue;
}
let value = strip_inline_comment(value.trim()).trim().to_string();
let full = if section.is_empty() {
key.to_string()
} else {
format!("{section}.{key}")
};
out.entry(full).or_insert(value);
}
out
}
#[test]
fn reference_config_documents_every_default_setting() {
let serialized = toml::to_string_pretty(&Config::default()).expect("serialize default");
let defaults = scalar_assignments(&serialized);
let reference = scalar_assignments(REFERENCE_CONFIG_TOML);
for (key, default_value) in &defaults {
if default_value == "[]" {
continue;
}
match reference.get(key) {
Some(reference_value) => assert_eq!(
reference_value, default_value,
"config.toml lists `{key} = {reference_value}` but the default is \
`{default_value}` — update the reference line",
),
None => panic!(
"config.toml has no line for `{key}` (default `{default_value}`) — document it",
),
}
}
}
#[test]
fn seed_keeps_reference_verbatim_on_truecolor() {
assert_eq!(seed_config_toml(true), REFERENCE_CONFIG_TOML);
}
#[test]
fn seed_swaps_theme_for_indexed_terminals() {
assert!(REFERENCE_CONFIG_TOML.contains(&format!("theme = \"{TRUECOLOR_FALLBACK_THEME}\"")));
let seeded = seed_config_toml(false);
assert!(seeded.contains(&format!("theme = \"{INDEXED_FALLBACK_THEME}\"")));
assert!(!seeded.contains(&format!("theme = \"{TRUECOLOR_FALLBACK_THEME}\"")));
}
}