use std::{fs, path::PathBuf};
#[derive(Debug, Clone)]
pub struct Config {
pub language: String,
pub default_width: u32,
pub default_game: String,
pub show_card_by_default: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
language: "en".to_string(),
default_width: 0,
default_game: String::new(),
show_card_by_default: false,
}
}
}
impl Config {
pub fn load() -> Self {
let Some(path) = config_path() else {
return Self::default();
};
match fs::read_to_string(&path) {
Ok(text) => parse(&text),
Err(_) => Self::default(),
}
}
}
fn parse(text: &str) -> Config {
let mut c = Config::default();
for line in text.lines() {
let line = strip_inline_comment(line).trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((key, val)) = line.split_once('=') else {
continue;
};
let val = val.trim().trim_matches('"');
match key.trim() {
"language" => c.language = val.to_string(),
"default_width" => c.default_width = val.parse().unwrap_or(0),
"default_game" => c.default_game = val.to_string(),
"show_card_by_default" => c.show_card_by_default = parse_bool(val),
_ => {}
}
}
c
}
pub fn config_path() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("zukan").join("config"))
}
fn strip_inline_comment(line: &str) -> &str {
let mut in_quotes = false;
let bytes = line.as_bytes();
let mut cut = line.len();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'"' => in_quotes = !in_quotes,
b'#' if !in_quotes && i > 0 && bytes[i - 1].is_ascii_whitespace() => {
cut = i;
break;
}
_ => {}
}
i += 1;
}
&line[..cut]
}
fn parse_bool(val: &str) -> bool {
matches!(
val.trim().to_ascii_lowercase().as_str(),
"true" | "yes" | "on" | "1"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sane() {
let c = Config::default();
assert_eq!(c.language, "en");
assert_eq!(c.default_width, 0);
assert!(c.default_game.is_empty());
assert!(!c.show_card_by_default);
}
#[test]
fn parse_partial_uses_defaults_for_missing() {
let c = parse("language = ja");
assert_eq!(c.language, "ja");
assert_eq!(c.default_width, 0);
assert!(!c.show_card_by_default);
}
#[test]
fn parse_full_config() {
let text = "\
language = zh\n\
default_width = 32\n\
default_game = mhwilds\n\
show_card_by_default = true";
let c = parse(text);
assert_eq!(c.language, "zh");
assert_eq!(c.default_width, 32);
assert_eq!(c.default_game, "mhwilds");
assert!(c.show_card_by_default);
}
#[test]
fn unknown_keys_are_ignored() {
let c = parse("language = en\nfuture_key = 42");
assert_eq!(c.language, "en");
}
#[test]
fn quoted_values_have_quotes_stripped() {
let c = parse("language = \"en\"");
assert_eq!(c.language, "en");
}
#[test]
fn comments_and_blank_lines_skipped() {
let text = "\
# this is a comment\n\
\n\
language = ja\n\
# another comment\n\
default_width = 24";
let c = parse(text);
assert_eq!(c.language, "ja");
assert_eq!(c.default_width, 24);
}
#[test]
fn bad_field_falls_back_to_default() {
let c = parse("default_width = abc\nshow_card_by_default = maybe\nlanguage = zh");
assert_eq!(c.default_width, 0);
assert!(!c.show_card_by_default);
assert_eq!(c.language, "zh");
}
#[test]
fn inline_comments_are_stripped() {
let text = "\
language = zh # en | ja | zh\n\
default_width = 0 # built-in defaults\n\
show_card_by_default = yes # show the info card";
let c = parse(text);
assert_eq!(c.language, "zh");
assert_eq!(c.default_width, 0);
assert!(c.show_card_by_default);
}
#[test]
fn hash_inside_value_is_not_a_comment() {
let c = parse(r#"default_game = "foo#bar""#);
assert_eq!(c.default_game, "foo#bar");
}
#[test]
fn bool_accepts_common_spellings() {
assert!(parse("show_card_by_default = true").show_card_by_default);
assert!(parse("show_card_by_default = yes").show_card_by_default);
assert!(parse("show_card_by_default = on").show_card_by_default);
assert!(parse("show_card_by_default = 1").show_card_by_default);
assert!(parse("show_card_by_default = TRUE").show_card_by_default);
assert!(!parse("show_card_by_default = false").show_card_by_default);
assert!(!parse("show_card_by_default = no").show_card_by_default);
assert!(!parse("show_card_by_default = 0").show_card_by_default);
assert!(!parse("show_card_by_default = maybe").show_card_by_default);
}
}