npm_run_scripts/config/
types.rs

1//! Configuration type definitions.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8use crate::package::Runner;
9
10/// Sort mode for script display.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum SortMode {
14    /// Sort by most recently used.
15    #[default]
16    Recent,
17    /// Sort alphabetically by name.
18    Alpha,
19    /// Group by category/prefix.
20    Category,
21}
22
23/// Column direction for grid layout.
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum ColumnDirection {
27    /// Fill rows first (1 2 3 4 / 5 6 7 8).
28    #[default]
29    Horizontal,
30    /// Fill columns first (1 4 7 / 2 5 8 / 3 6 9).
31    Vertical,
32}
33
34/// Color theme for the TUI.
35#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum Theme {
38    /// Full color theme.
39    #[default]
40    Default,
41    /// Minimal colors.
42    Minimal,
43    /// No colors (monochrome).
44    None,
45}
46
47/// General configuration settings.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct GeneralConfig {
50    /// Override package manager detection.
51    #[serde(default)]
52    pub runner: Option<Runner>,
53    /// Default sort mode.
54    #[serde(default)]
55    pub default_sort: SortMode,
56    /// Column direction in grid.
57    #[serde(default)]
58    pub column_direction: ColumnDirection,
59    /// Show command preview in description panel.
60    #[serde(default = "default_true")]
61    pub show_command_preview: bool,
62    /// Maximum items to show (0 = unlimited).
63    #[serde(default)]
64    pub max_items: usize,
65}
66
67impl Default for GeneralConfig {
68    fn default() -> Self {
69        Self {
70            runner: None,
71            default_sort: SortMode::default(),
72            column_direction: ColumnDirection::default(),
73            show_command_preview: true,
74            max_items: 0,
75        }
76    }
77}
78
79/// Filter configuration settings.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct FilterConfig {
82    /// Search in descriptions too.
83    #[serde(default = "default_true")]
84    pub search_descriptions: bool,
85    /// Enable fuzzy matching.
86    #[serde(default = "default_true")]
87    pub fuzzy: bool,
88    /// Case sensitive search.
89    #[serde(default)]
90    pub case_sensitive: bool,
91}
92
93impl Default for FilterConfig {
94    fn default() -> Self {
95        Self {
96            search_descriptions: true,
97            fuzzy: true,
98            case_sensitive: false,
99        }
100    }
101}
102
103/// History configuration settings.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct HistoryConfig {
106    /// Enable history tracking.
107    #[serde(default = "default_true")]
108    pub enabled: bool,
109    /// Max projects to remember.
110    #[serde(default = "default_max_projects")]
111    pub max_projects: usize,
112    /// Max scripts per project.
113    #[serde(default = "default_max_scripts")]
114    pub max_scripts: usize,
115}
116
117impl Default for HistoryConfig {
118    fn default() -> Self {
119        Self {
120            enabled: true,
121            max_projects: 100,
122            max_scripts: 50,
123        }
124    }
125}
126
127/// Exclude patterns configuration.
128#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129pub struct ExcludeConfig {
130    /// Glob patterns to exclude.
131    #[serde(default)]
132    pub patterns: Vec<String>,
133}
134
135/// Appearance configuration settings.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AppearanceConfig {
138    /// Color theme.
139    #[serde(default)]
140    pub theme: Theme,
141    /// Show icons.
142    #[serde(default = "default_true")]
143    pub icons: bool,
144    /// Show help footer.
145    #[serde(default = "default_true")]
146    pub show_footer: bool,
147    /// Compact mode (less padding).
148    #[serde(default)]
149    pub compact: bool,
150}
151
152impl Default for AppearanceConfig {
153    fn default() -> Self {
154        Self {
155            theme: Theme::default(),
156            icons: true,
157            show_footer: true,
158            compact: false,
159        }
160    }
161}
162
163/// Keybindings configuration.
164#[derive(Debug, Clone, Default, Serialize, Deserialize)]
165pub struct KeybindingsConfig {
166    /// Quit keys (e.g., ["q", "Ctrl+c"]).
167    #[serde(default)]
168    pub quit: Vec<String>,
169    /// Run script keys.
170    #[serde(default)]
171    pub run: Vec<String>,
172    /// Enter filter mode keys.
173    #[serde(default)]
174    pub filter: Vec<String>,
175}
176
177/// Scripts configuration for custom descriptions and aliases.
178#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179pub struct ScriptsConfig {
180    /// Custom descriptions for scripts (overrides package.json).
181    #[serde(default)]
182    pub descriptions: HashMap<String, String>,
183    /// Script aliases (alias -> script name).
184    #[serde(default)]
185    pub aliases: HashMap<String, String>,
186}
187
188/// Main configuration structure.
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
190pub struct Config {
191    /// General settings.
192    #[serde(default)]
193    pub general: GeneralConfig,
194    /// Filter settings.
195    #[serde(default)]
196    pub filter: FilterConfig,
197    /// History settings.
198    #[serde(default)]
199    pub history: HistoryConfig,
200    /// Exclude patterns.
201    #[serde(default)]
202    pub exclude: ExcludeConfig,
203    /// Appearance settings.
204    #[serde(default)]
205    pub appearance: AppearanceConfig,
206    /// Keybindings settings.
207    #[serde(default)]
208    pub keybindings: KeybindingsConfig,
209    /// Scripts configuration.
210    #[serde(default)]
211    pub scripts: ScriptsConfig,
212}
213
214impl Config {
215    /// Create a new configuration with defaults.
216    pub fn new() -> Self {
217        Self::default()
218    }
219
220    /// Get the config file path for the user's home directory.
221    pub fn user_config_path() -> Option<PathBuf> {
222        dirs::config_dir().map(|p| p.join("nrs").join("config.toml"))
223    }
224
225    /// Merge another config into this one (other takes precedence for set values).
226    pub fn merge(&mut self, other: Config) {
227        // General settings
228        if other.general.runner.is_some() {
229            self.general.runner = other.general.runner;
230        }
231        self.general.default_sort = other.general.default_sort;
232        self.general.column_direction = other.general.column_direction;
233        self.general.show_command_preview = other.general.show_command_preview;
234        if other.general.max_items > 0 {
235            self.general.max_items = other.general.max_items;
236        }
237
238        // Filter settings
239        self.filter = other.filter;
240
241        // History settings
242        self.history = other.history;
243
244        // Exclude patterns - append rather than replace
245        self.exclude.patterns.extend(other.exclude.patterns);
246
247        // Appearance settings
248        self.appearance = other.appearance;
249
250        // Keybindings - only override if not empty
251        if !other.keybindings.quit.is_empty() {
252            self.keybindings.quit = other.keybindings.quit;
253        }
254        if !other.keybindings.run.is_empty() {
255            self.keybindings.run = other.keybindings.run;
256        }
257        if !other.keybindings.filter.is_empty() {
258            self.keybindings.filter = other.keybindings.filter;
259        }
260
261        // Scripts - merge hashmaps
262        self.scripts.descriptions.extend(other.scripts.descriptions);
263        self.scripts.aliases.extend(other.scripts.aliases);
264    }
265}
266
267fn default_true() -> bool {
268    true
269}
270
271fn default_max_projects() -> usize {
272    100
273}
274
275fn default_max_scripts() -> usize {
276    50
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_default_config() {
285        let config = Config::default();
286        assert!(config.filter.fuzzy);
287        assert!(config.filter.search_descriptions);
288        assert!(!config.filter.case_sensitive);
289        assert!(config.history.enabled);
290        assert_eq!(config.history.max_projects, 100);
291        assert_eq!(config.history.max_scripts, 50);
292        assert!(config.appearance.icons);
293        assert!(config.appearance.show_footer);
294        assert!(!config.appearance.compact);
295        assert_eq!(config.appearance.theme, Theme::Default);
296    }
297
298    #[test]
299    fn test_sort_mode_serialization() {
300        let json = serde_json::to_string(&SortMode::Alpha).unwrap();
301        assert_eq!(json, "\"alpha\"");
302
303        let mode: SortMode = serde_json::from_str("\"category\"").unwrap();
304        assert_eq!(mode, SortMode::Category);
305    }
306
307    #[test]
308    fn test_column_direction_serialization() {
309        let json = serde_json::to_string(&ColumnDirection::Vertical).unwrap();
310        assert_eq!(json, "\"vertical\"");
311
312        let dir: ColumnDirection = serde_json::from_str("\"horizontal\"").unwrap();
313        assert_eq!(dir, ColumnDirection::Horizontal);
314    }
315
316    #[test]
317    fn test_theme_serialization() {
318        let json = serde_json::to_string(&Theme::Minimal).unwrap();
319        assert_eq!(json, "\"minimal\"");
320
321        let theme: Theme = serde_json::from_str("\"none\"").unwrap();
322        assert_eq!(theme, Theme::None);
323    }
324
325    #[test]
326    fn test_config_merge() {
327        let mut base = Config::default();
328        base.exclude.patterns.push("test*".to_string());
329
330        let mut override_config = Config::default();
331        override_config.general.runner = Some(Runner::Pnpm);
332        override_config.exclude.patterns.push("lint*".to_string());
333        override_config
334            .scripts
335            .descriptions
336            .insert("dev".to_string(), "Start dev server".to_string());
337
338        base.merge(override_config);
339
340        assert_eq!(base.general.runner, Some(Runner::Pnpm));
341        assert_eq!(base.exclude.patterns.len(), 2);
342        assert!(base.exclude.patterns.contains(&"test*".to_string()));
343        assert!(base.exclude.patterns.contains(&"lint*".to_string()));
344        assert_eq!(
345            base.scripts.descriptions.get("dev"),
346            Some(&"Start dev server".to_string())
347        );
348    }
349}