pub mod keys;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SivtrConfig {
pub general: GeneralConfig,
pub editor: EditorConfig,
pub history: HistoryConfig,
pub copy: CopyConfig,
pub hotkey: HotkeyConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub open_mode: OpenMode,
pub preserve_colors: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OpenMode {
#[default]
Tui,
Editor,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct EditorConfig {
pub command: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HistoryConfig {
pub auto_save: bool,
pub max_entries: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct CopyConfig {
pub prompts: Vec<String>,
#[serde(rename = "prompt_presets", skip_serializing)]
pub legacy_prompt_presets: Vec<String>,
}
impl CopyConfig {
pub fn prompt_values(&self) -> impl Iterator<Item = &String> {
self.legacy_prompt_presets.iter().chain(self.prompts.iter())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HotkeyConfig {
pub chord: String,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
open_mode: OpenMode::Tui,
preserve_colors: true,
}
}
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
auto_save: true,
max_entries: 0, }
}
}
impl Default for HotkeyConfig {
fn default() -> Self {
Self {
chord: "alt+y".to_string(),
}
}
}
impl SivtrConfig {
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read config file: {}", path.display()))?;
let config: SivtrConfig = toml::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path.display()))?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write config file: {}", path.display()))?;
Ok(())
}
pub fn init_default() -> Result<PathBuf> {
let path = Self::config_path()?;
if !path.exists() {
let config = Self::default();
config.save()?;
}
Ok(path)
}
pub fn config_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("Cannot determine config directory"))?;
let current = config_dir.join("sivtr").join("config.toml");
if current.exists() {
return Ok(current);
}
let legacy = config_dir.join("sift").join("config.toml");
if legacy.exists() {
return Ok(legacy);
}
Ok(current)
}
}
pub fn to_toml_string(config: &SivtrConfig) -> Result<String> {
toml::to_string_pretty(config).context("Failed to serialize config to TOML")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_copy_prompt_config() {
let config = SivtrConfig {
copy: CopyConfig {
prompts: vec!["arrow".to_string(), "mysh>".to_string(), "dev>".to_string()],
legacy_prompt_presets: vec!["cmd".to_string()],
},
..SivtrConfig::default()
};
let toml = to_toml_string(&config).unwrap();
assert!(toml.contains("[copy]"));
assert!(toml.contains("prompts = ["));
assert!(toml.contains("\"arrow\""));
assert!(toml.contains("\"mysh>\""));
assert!(toml.contains("\"dev>\""));
assert!(!toml.contains("prompt_presets"));
}
#[test]
fn serializes_hotkey_config() {
let config = SivtrConfig {
hotkey: HotkeyConfig {
chord: "alt+y".to_string(),
},
..SivtrConfig::default()
};
let toml = to_toml_string(&config).unwrap();
assert!(toml.contains("[hotkey]"));
assert!(toml.contains("chord = \"alt+y\""));
}
}