use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default = "default_theme")]
pub theme: String,
#[serde(default = "default_speed")]
pub speed: u64,
#[serde(default = "default_background")]
pub background: bool,
#[serde(default = "default_order")]
pub order: String,
#[serde(default = "default_loop")]
pub loop_playback: bool,
#[serde(default = "default_ignore_patterns")]
pub ignore_patterns: Vec<String>,
#[serde(default)]
pub speed_rules: Vec<String>,
}
fn default_theme() -> String {
"tokyo-night".to_string()
}
fn default_speed() -> u64 {
30
}
fn default_background() -> bool {
true
}
fn default_order() -> String {
"random".to_string()
}
fn default_loop() -> bool {
false
}
fn default_ignore_patterns() -> Vec<String> {
Vec::new()
}
impl Default for Config {
fn default() -> Self {
Self {
theme: default_theme(),
speed: default_speed(),
background: default_background(),
order: default_order(),
loop_playback: default_loop(),
ignore_patterns: default_ignore_patterns(),
speed_rules: Vec::new(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if !config_path.exists() {
return Ok(Self::default());
}
let contents = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
toml::from_str(&contents)
.with_context(|| format!("Failed to parse config file: {}", config_path.display()))
}
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
let contents = if config_path.exists() {
let existing = fs::read_to_string(&config_path).with_context(|| {
format!("Failed to read config file: {}", config_path.display())
})?;
let mut doc = existing
.parse::<toml_edit::DocumentMut>()
.with_context(|| {
format!("Failed to parse config file: {}", config_path.display())
})?;
doc["theme"] = toml_edit::value(self.theme.as_str());
doc["speed"] = toml_edit::value(self.speed as i64);
doc["background"] = toml_edit::value(self.background);
doc["order"] = toml_edit::value(self.order.as_str());
doc["loop"] = toml_edit::value(self.loop_playback);
let mut array = toml_edit::Array::new();
for pattern in &self.ignore_patterns {
array.push(pattern.as_str());
}
doc["ignore_patterns"] = toml_edit::value(array);
let mut speed_array = toml_edit::Array::new();
for rule in &self.speed_rules {
speed_array.push(rule.as_str());
}
doc["speed_rules"] = toml_edit::value(speed_array);
doc.to_string()
} else {
let patterns_str = if self.ignore_patterns.is_empty() {
"[]".to_string()
} else {
let patterns: Vec<String> = self
.ignore_patterns
.iter()
.map(|p| format!("\"{}\"", p))
.collect();
format!("[{}]", patterns.join(", "))
};
let speed_rules_str = if self.speed_rules.is_empty() {
"[]".to_string()
} else {
let rules: Vec<String> = self
.speed_rules
.iter()
.map(|r| format!("\"{}\"", r))
.collect();
format!("[{}]", rules.join(", "))
};
format!(
"# gitlogue configuration file\n\
# All settings are optional and will use defaults if not specified\n\
\n\
# Theme to use for syntax highlighting\n\
theme = \"{}\"\n\
\n\
# Typing speed in milliseconds per character\n\
speed = {}\n\
\n\
# Show background colors (set to false for transparent background)\n\
background = {}\n\
\n\
# Commit playback order: random, asc, or desc\n\
order = \"{}\"\n\
\n\
# Loop the animation continuously\n\
loop = {}\n\
\n\
# Ignore patterns (gitignore syntax)\n\
# Examples: [\"*.png\", \"*.ipynb\", \"dist/**\"]\n\
ignore_patterns = {}\n\
\n\
# Speed rules for different file types (pattern:milliseconds)\n\
# Examples: [\"*.java:50\", \"*.xml:5\", \"*.rs:30\"]\n\
speed_rules = {}\n",
self.theme,
self.speed,
self.background,
self.order,
self.loop_playback,
patterns_str,
speed_rules_str
)
};
fs::write(&config_path, contents)
.with_context(|| format!("Failed to write config file: {}", config_path.display()))
}
pub fn config_path() -> Result<PathBuf> {
let config_dir = dirs::home_dir()
.context("Failed to determine home directory")?
.join(".config")
.join("gitlogue");
fs::create_dir_all(&config_dir).with_context(|| {
format!(
"Failed to create config directory: {}",
config_dir.display()
)
})?;
Ok(config_dir.join("config.toml"))
}
#[allow(dead_code)]
pub fn themes_dir() -> Result<PathBuf> {
let config_dir = dirs::home_dir()
.context("Failed to determine home directory")?
.join(".config")
.join("gitlogue")
.join("themes");
fs::create_dir_all(&config_dir).with_context(|| {
format!(
"Failed to create themes directory: {}",
config_dir.display()
)
})?;
Ok(config_dir)
}
}