use std::path::PathBuf;
use serde::Deserialize;
#[must_use]
pub fn config_dir() -> PathBuf {
if let Some(dir) = std::env::var_os("TERMI_CONFIG_DIR") {
return PathBuf::from(dir);
}
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("termi")
}
#[must_use]
pub fn themes_dir() -> PathBuf {
config_dir().join("themes")
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub theme: String,
pub tab_width: usize,
pub expand_tabs: bool,
pub line_numbers: bool,
pub relative_line_numbers: bool,
pub auto_indent: bool,
pub word_wrap: bool,
pub highlight_current_line: bool,
pub scrolloff: usize,
pub syntax_highlighting: bool,
pub show_tabs: bool,
pub watch_files: bool,
pub trim_trailing_whitespace: bool,
pub system_clipboard: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
theme: "dark".to_string(),
tab_width: 4,
expand_tabs: true,
line_numbers: true,
relative_line_numbers: false,
auto_indent: true,
word_wrap: false,
highlight_current_line: true,
scrolloff: 3,
syntax_highlighting: true,
show_tabs: true,
watch_files: true,
trim_trailing_whitespace: false,
system_clipboard: true,
}
}
}
impl Config {
#[must_use]
pub fn load() -> (Self, Option<String>) {
let path = config_dir().join("config.toml");
if !path.is_file() {
return (Self::default(), None);
}
match crate::filesystem::read_file(&path).and_then(|text| Ok(Self::parse(&text)?)) {
Ok(config) => (config, None),
Err(error) => (
Self::default(),
Some(format!("{}: {error}", path.display())),
),
}
}
pub fn parse(text: &str) -> Result<Self, toml::de::Error> {
let mut config: Self = toml::from_str(text)?;
config.sanitise();
Ok(config)
}
fn sanitise(&mut self) {
self.tab_width = self.tab_width.clamp(1, 16);
self.scrolloff = self.scrolloff.min(32);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_partial_file_keeps_the_remaining_defaults() {
let config = Config::parse("theme = \"light\"\ntab_width = 2").expect("valid config");
assert_eq!(config.theme, "light");
assert_eq!(config.tab_width, 2);
assert!(config.auto_indent);
assert!(config.line_numbers);
}
#[test]
fn an_empty_file_is_the_default_config() {
let config = Config::parse("").expect("valid config");
assert_eq!(config.theme, Config::default().theme);
}
#[test]
fn unknown_keys_are_reported_rather_than_ignored() {
let error = Config::parse("tab_wdith = 4").expect_err("typo must be rejected");
assert!(error.to_string().contains("tab_wdith"));
}
#[test]
fn dangerous_values_are_clamped() {
let config = Config::parse("tab_width = 0\nscrolloff = 9999").expect("valid config");
assert_eq!(config.tab_width, 1);
assert_eq!(config.scrolloff, 32);
}
}