use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::theme::Theme;
const APP_NAME: &str = "markdown-reader";
const CONFIG_FILE: &str = "config.toml";
fn default_mermaid_max_height() -> u32 {
30
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TreePosition {
#[default]
Left,
Right,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MermaidMode {
#[default]
Auto,
Text,
Image,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SearchPreview {
#[default]
FullLine,
Snippet,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub theme: Theme,
#[serde(default)]
pub show_line_numbers: bool,
#[serde(default)]
pub tree_position: TreePosition,
#[serde(default)]
pub search_preview: SearchPreview,
#[serde(default)]
pub mermaid_mode: MermaidMode,
#[serde(default = "default_mermaid_max_height")]
pub mermaid_max_height: u32,
}
impl Default for Config {
fn default() -> Self {
Self {
theme: Theme::default(),
show_line_numbers: false,
tree_position: TreePosition::default(),
search_preview: SearchPreview::default(),
mermaid_mode: MermaidMode::default(),
mermaid_max_height: default_mermaid_max_height(),
}
}
}
impl Config {
pub fn load() -> Self {
let Some(path) = config_path() else {
return Self::default();
};
let Ok(text) = fs::read_to_string(&path) else {
return Self::default();
};
toml::from_str(&text).unwrap_or_default()
}
pub fn save(&self) {
let Some(path) = config_path() else {
return;
};
if let Some(parent) = path.parent()
&& fs::create_dir_all(parent).is_err()
{
return;
}
let Ok(text) = toml::to_string_pretty(self) else {
return;
};
let _ = fs::write(&path, text);
}
pub fn mermaid_mode_label(mode: MermaidMode) -> &'static str {
match mode {
MermaidMode::Auto => "Auto",
MermaidMode::Text => "Text only",
MermaidMode::Image => "Image only",
}
}
}
fn config_path() -> Option<PathBuf> {
let mut path = dirs::config_dir()?;
path.push(APP_NAME);
path.push(CONFIG_FILE);
Some(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search_preview_default_round_trips() {
let config = Config::default();
let serialized = toml::to_string_pretty(&config).expect("serialization failed");
let deserialized: Config = toml::from_str(&serialized).expect("deserialization failed");
assert_eq!(deserialized.search_preview, SearchPreview::FullLine);
}
#[test]
fn search_preview_missing_field_defaults_to_full_line() {
let toml_str = r#"theme = "default""#;
let config: Config = toml::from_str(toml_str).expect("deserialization failed");
assert_eq!(config.search_preview, SearchPreview::default());
}
#[test]
fn mermaid_max_height_config_roundtrip() {
let config = Config {
mermaid_max_height: 25,
..Config::default()
};
let serialized = toml::to_string_pretty(&config).expect("serialization failed");
let deserialized: Config = toml::from_str(&serialized).expect("deserialization failed");
assert_eq!(deserialized.mermaid_max_height, 25);
}
#[test]
fn mermaid_max_height_missing_field_defaults_to_30() {
let toml_str = r#"theme = "default""#;
let config: Config = toml::from_str(toml_str).expect("deserialization failed");
assert_eq!(config.mermaid_max_height, 30);
}
#[test]
fn mermaid_mode_round_trips() {
let config = Config {
mermaid_mode: MermaidMode::Text,
..Config::default()
};
let serialized = toml::to_string_pretty(&config).expect("serialization failed");
let deserialized: Config = toml::from_str(&serialized).expect("deserialization failed");
assert_eq!(deserialized.mermaid_mode, MermaidMode::Text);
}
#[test]
fn mermaid_mode_missing_field_defaults_to_auto() {
let toml_str = r#"theme = "default""#;
let config: Config = toml::from_str(toml_str).expect("deserialization failed");
assert_eq!(config.mermaid_mode, MermaidMode::Auto);
}
}