1use crate::error::{FsearchError, FsearchResult};
15use serde::{Deserialize, Serialize};
16use std::path::PathBuf;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default)]
24pub struct Config {
25 pub default_depth: u32,
28 pub default_method: u8,
30 pub case_insensitive: bool,
32 pub include_dirs: bool,
34 pub binary_check_bytes: usize,
36 pub max_line_length: usize,
38 pub threads: usize,
40
41 pub hash_algorithm: String,
44 pub hash_buffer_size: usize,
46 pub dup_min_size: u64,
48 pub dup_max_size: u64,
50
51 pub verbose: bool,
54 pub show_size: bool,
56 pub show_modified: bool,
58 pub max_results: usize,
60
61 pub color_index: String,
63 pub color_path: String,
64 pub color_line_num: String,
65 pub color_line_text: String,
66 pub color_header: String,
67 pub color_count: String,
68 pub color_error: String,
69 pub color_warn: String,
70 pub color_info: String,
71 pub color_pattern: String,
72 pub color_dup_group: String,
73 pub color_dup_path: String,
74 pub color_dup_size: String,
75
76 pub exclude_dirs: String,
79 pub default_include: String,
81}
82
83impl Default for Config {
84 fn default() -> Self {
85 Self {
86 default_depth: 1,
87 default_method: 1,
88 case_insensitive: true,
89 include_dirs: true,
90 binary_check_bytes: 1024,
91 max_line_length: 10_000,
92 threads: 0,
93 hash_algorithm: "sha256".into(),
94 hash_buffer_size: 65_536, dup_min_size: 1,
96 dup_max_size: 0,
97 verbose: false,
98 show_size: false,
99 show_modified: false,
100 max_results: 0,
101 color_index: "#FF88FF".into(),
102 color_path: "#FFFF00".into(),
103 color_line_num: "#FF4444".into(),
104 color_line_text: "#00FFFF".into(),
105 color_header: "#FFFFFF".into(),
106 color_count: "#00FFFF".into(),
107 color_error: "#FF3333".into(),
108 color_warn: "#FFAA00".into(),
109 color_info: "#00FF88".into(),
110 color_pattern: "#FF00FF".into(),
111 color_dup_group: "#FF8800".into(),
112 color_dup_path: "#FFFF00".into(),
113 color_dup_size: "#88FF88".into(),
114 exclude_dirs: ".git,node_modules,.svn,__pycache__,.hg,target,.cache".into(),
115 default_include: "".into(),
116 }
117 }
118}
119
120impl Config {
121 pub fn load() -> Self {
123 if let Ok(cfg) = Self::load_from_path(PathBuf::from("fsearch.toml")) {
124 return cfg;
125 }
126 if let Some(dir) = dirs::config_dir() {
127 if let Ok(cfg) = Self::load_from_path(dir.join("fsearch").join("config.toml")) {
128 return cfg;
129 }
130 }
131 Self::default()
132 }
133
134 pub fn load_from_path(path: PathBuf) -> FsearchResult<Self> {
136 let text = std::fs::read_to_string(&path).map_err(|e| FsearchError::Io {
137 path: path.display().to_string(),
138 source: e,
139 })?;
140 toml::from_str(&text).map_err(|e| FsearchError::ConfigParse {
141 path: path.display().to_string(),
142 source: e,
143 })
144 }
145
146 pub fn write_default() -> FsearchResult<PathBuf> {
148 let dir = dirs::config_dir()
149 .ok_or_else(|| FsearchError::Config("cannot find user config directory".into()))?
150 .join("fsearch");
151 std::fs::create_dir_all(&dir).map_err(|e| FsearchError::Io {
152 path: dir.display().to_string(),
153 source: e,
154 })?;
155 let path = dir.join("config.toml");
156 let raw = toml::to_string_pretty(&Self::default())
157 .map_err(|e| FsearchError::Config(e.to_string()))?;
158 let annotated = format!(
159 "# fsearch configuration ({})\n\
160 # All values are defaults. Remove the leading '#' to override.\n\n",
161 path.display()
162 ) + &raw
163 .lines()
164 .map(|l| {
165 if l.starts_with('[') || l.is_empty() {
166 l.to_string()
167 } else {
168 format!("# {l}")
169 }
170 })
171 .collect::<Vec<_>>()
172 .join("\n");
173 std::fs::write(&path, annotated).map_err(|e| FsearchError::Io {
174 path: path.display().to_string(),
175 source: e,
176 })?;
177 Ok(path)
178 }
179
180 pub fn excluded_dirs(&self) -> Vec<String> {
182 split_csv(&self.exclude_dirs)
183 }
184}
185
186pub(crate) fn split_csv(s: &str) -> Vec<String> {
188 s.split(',')
189 .map(|p| p.trim().to_string())
190 .filter(|p| !p.is_empty())
191 .collect()
192}