use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::package::Runner;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortMode {
#[default]
Recent,
Alpha,
Category,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ColumnDirection {
#[default]
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
#[default]
Default,
Minimal,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
#[serde(default)]
pub runner: Option<Runner>,
#[serde(default)]
pub default_sort: SortMode,
#[serde(default)]
pub column_direction: ColumnDirection,
#[serde(default = "default_true")]
pub show_command_preview: bool,
#[serde(default)]
pub max_items: usize,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
runner: None,
default_sort: SortMode::default(),
column_direction: ColumnDirection::default(),
show_command_preview: true,
max_items: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterConfig {
#[serde(default = "default_true")]
pub search_descriptions: bool,
#[serde(default = "default_true")]
pub fuzzy: bool,
#[serde(default)]
pub case_sensitive: bool,
}
impl Default for FilterConfig {
fn default() -> Self {
Self {
search_descriptions: true,
fuzzy: true,
case_sensitive: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_max_projects")]
pub max_projects: usize,
#[serde(default = "default_max_scripts")]
pub max_scripts: usize,
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
enabled: true,
max_projects: 100,
max_scripts: 50,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExcludeConfig {
#[serde(default)]
pub patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppearanceConfig {
#[serde(default)]
pub theme: Theme,
#[serde(default = "default_true")]
pub icons: bool,
#[serde(default = "default_true")]
pub show_footer: bool,
#[serde(default)]
pub compact: bool,
}
impl Default for AppearanceConfig {
fn default() -> Self {
Self {
theme: Theme::default(),
icons: true,
show_footer: true,
compact: false,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct KeybindingsConfig {
#[serde(default)]
pub quit: Vec<String>,
#[serde(default)]
pub run: Vec<String>,
#[serde(default)]
pub filter: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScriptsConfig {
#[serde(default)]
pub descriptions: HashMap<String, String>,
#[serde(default)]
pub aliases: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub general: GeneralConfig,
#[serde(default)]
pub filter: FilterConfig,
#[serde(default)]
pub history: HistoryConfig,
#[serde(default)]
pub exclude: ExcludeConfig,
#[serde(default)]
pub appearance: AppearanceConfig,
#[serde(default)]
pub keybindings: KeybindingsConfig,
#[serde(default)]
pub scripts: ScriptsConfig,
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn user_config_path() -> Option<PathBuf> {
dirs::config_dir().map(|p| p.join("nrs").join("config.toml"))
}
pub fn merge(&mut self, other: Config) {
if other.general.runner.is_some() {
self.general.runner = other.general.runner;
}
self.general.default_sort = other.general.default_sort;
self.general.column_direction = other.general.column_direction;
self.general.show_command_preview = other.general.show_command_preview;
if other.general.max_items > 0 {
self.general.max_items = other.general.max_items;
}
self.filter = other.filter;
self.history = other.history;
self.exclude.patterns.extend(other.exclude.patterns);
self.appearance = other.appearance;
if !other.keybindings.quit.is_empty() {
self.keybindings.quit = other.keybindings.quit;
}
if !other.keybindings.run.is_empty() {
self.keybindings.run = other.keybindings.run;
}
if !other.keybindings.filter.is_empty() {
self.keybindings.filter = other.keybindings.filter;
}
self.scripts.descriptions.extend(other.scripts.descriptions);
self.scripts.aliases.extend(other.scripts.aliases);
}
}
fn default_true() -> bool {
true
}
fn default_max_projects() -> usize {
100
}
fn default_max_scripts() -> usize {
50
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.filter.fuzzy);
assert!(config.filter.search_descriptions);
assert!(!config.filter.case_sensitive);
assert!(config.history.enabled);
assert_eq!(config.history.max_projects, 100);
assert_eq!(config.history.max_scripts, 50);
assert!(config.appearance.icons);
assert!(config.appearance.show_footer);
assert!(!config.appearance.compact);
assert_eq!(config.appearance.theme, Theme::Default);
}
#[test]
fn test_sort_mode_serialization() {
let json = serde_json::to_string(&SortMode::Alpha).unwrap();
assert_eq!(json, "\"alpha\"");
let mode: SortMode = serde_json::from_str("\"category\"").unwrap();
assert_eq!(mode, SortMode::Category);
}
#[test]
fn test_column_direction_serialization() {
let json = serde_json::to_string(&ColumnDirection::Vertical).unwrap();
assert_eq!(json, "\"vertical\"");
let dir: ColumnDirection = serde_json::from_str("\"horizontal\"").unwrap();
assert_eq!(dir, ColumnDirection::Horizontal);
}
#[test]
fn test_theme_serialization() {
let json = serde_json::to_string(&Theme::Minimal).unwrap();
assert_eq!(json, "\"minimal\"");
let theme: Theme = serde_json::from_str("\"none\"").unwrap();
assert_eq!(theme, Theme::None);
}
#[test]
fn test_config_merge() {
let mut base = Config::default();
base.exclude.patterns.push("test*".to_string());
let mut override_config = Config::default();
override_config.general.runner = Some(Runner::Pnpm);
override_config.exclude.patterns.push("lint*".to_string());
override_config
.scripts
.descriptions
.insert("dev".to_string(), "Start dev server".to_string());
base.merge(override_config);
assert_eq!(base.general.runner, Some(Runner::Pnpm));
assert_eq!(base.exclude.patterns.len(), 2);
assert!(base.exclude.patterns.contains(&"test*".to_string()));
assert!(base.exclude.patterns.contains(&"lint*".to_string()));
assert_eq!(
base.scripts.descriptions.get("dev"),
Some(&"Start dev server".to_string())
);
}
}