1pub mod addignore;
9pub mod adopt;
10pub mod down;
11pub mod fill;
12pub mod git_alias;
13pub mod git_filters;
14pub mod init;
15pub mod list;
16pub mod probe;
17pub mod prompts;
18pub mod refresh;
19pub mod secret;
20pub mod status;
21pub mod template_clean;
22pub mod template_install_filter;
23pub mod transform;
24pub mod tutorial;
25pub mod up;
26
27#[cfg(test)]
28mod tests;
29
30use serde::Serialize;
31
32#[derive(Debug, Clone, Serialize)]
39pub struct MessageResult {
40 pub message: String,
41 pub details: Vec<String>,
42}
43
44pub fn handler_symbol(handler: &str) -> &'static str {
48 match handler {
49 "symlink" => "➞",
50 "shell" => "⚙",
51 "path" => "+",
52 "homebrew" => "⚙",
53 "install" => "×",
54 _ => "?",
55 }
56}
57
58pub fn status_style(deployed: bool) -> &'static str {
60 if deployed {
61 "deployed"
62 } else {
63 "pending"
64 }
65}
66
67pub fn handler_description(handler: &str, rel_path: &str, user_target: Option<&str>) -> String {
69 match handler {
70 "symlink" => {
71 user_target
78 .map(str::to_string)
79 .unwrap_or_else(|| "<symlink>".to_string())
80 }
81 "shell" => "shell profile".into(),
82 "path" => format!("$PATH/{rel_path}"),
83 "install" => "run script".into(),
84 "homebrew" => "brew install".into(),
85 _ => String::new(),
86 }
87}
88
89#[derive(Debug, Clone, Serialize)]
91pub struct DisplayFile {
92 pub name: String,
93 pub symbol: String,
94 pub description: String,
95 pub status: String,
96 pub status_label: String,
97 pub handler: String,
98 #[serde(skip_serializing_if = "Option::is_none", default)]
104 pub note_ref: Option<u32>,
105}
106
107#[derive(Debug, Clone, Serialize)]
109pub struct DisplayPack {
110 pub name: String,
111 pub files: Vec<DisplayFile>,
112 pub summary_status: String,
118 pub summary_count: usize,
121}
122
123impl DisplayPack {
124 pub fn new(name: String, files: Vec<DisplayFile>) -> Self {
126 let (summary_status, summary_count) = aggregate_status(&files);
127 DisplayPack {
128 name,
129 files,
130 summary_status,
131 summary_count,
132 }
133 }
134
135 pub fn recompute_summary(&mut self) {
139 let (status, count) = aggregate_status(&self.files);
140 self.summary_status = status;
141 self.summary_count = count;
142 }
143}
144
145fn aggregate_status(files: &[DisplayFile]) -> (String, usize) {
149 let mut errors = 0usize;
150 let mut pendings = 0usize;
151 let mut deployeds = 0usize;
152 for f in files {
153 match f.status.as_str() {
154 "error" | "broken" => errors += 1,
155 "pending" | "warning" | "stale" => pendings += 1,
156 "deployed" => deployeds += 1,
157 _ => {}
158 }
159 }
160 if errors > 0 {
161 ("error".into(), errors)
162 } else if pendings > 0 {
163 ("pending".into(), pendings)
164 } else {
165 ("deployed".into(), deployeds)
166 }
167}
168
169#[derive(Debug, Clone, Serialize)]
173pub struct DisplayNote {
174 pub body: String,
175 #[serde(skip_serializing_if = "Option::is_none", default)]
176 pub hint: Option<String>,
177}
178
179#[derive(Debug, Clone, Serialize)]
181pub struct DisplayClaimant {
182 pub pack: String,
184 pub source: String,
186}
187
188#[derive(Debug, Clone, Serialize)]
190pub struct DisplayConflict {
191 pub kind: String,
194 pub target: String,
196 pub claimants: Vec<DisplayClaimant>,
197}
198
199impl DisplayConflict {
200 pub fn from_conflict(c: &crate::conflicts::Conflict, home: &std::path::Path) -> Self {
203 let kind = match c.kind {
204 crate::conflicts::ConflictKind::SymlinkTarget => "symlink",
205 crate::conflicts::ConflictKind::PathExecutable => "path",
206 };
207 let target = match c.kind {
208 crate::conflicts::ConflictKind::SymlinkTarget => shorten_path(&c.target, home),
209 crate::conflicts::ConflictKind::PathExecutable => c
210 .target
211 .file_name()
212 .map(|n| n.to_string_lossy().into_owned())
213 .unwrap_or_else(|| c.target.display().to_string()),
214 };
215 let claimants = c
216 .claimants
217 .iter()
218 .map(|cl| DisplayClaimant {
219 pack: cl.pack.clone(),
220 source: pack_relative_source(&cl.source, &cl.pack),
221 })
222 .collect();
223 DisplayConflict {
224 kind: kind.into(),
225 target,
226 claimants,
227 }
228 }
229}
230
231fn shorten_path(p: &std::path::Path, home: &std::path::Path) -> String {
232 if let Ok(rel) = p.strip_prefix(home) {
233 format!("~/{}", rel.display())
234 } else {
235 p.display().to_string()
236 }
237}
238
239fn pack_relative_source(source: &std::path::Path, pack: &str) -> String {
242 let s = source.to_string_lossy();
243 let marker = format!("/{pack}/");
244 if let Some(idx) = s.rfind(&marker) {
245 let rel = &s[idx + 1..];
246 return rel.to_string();
247 }
248 let fname = source
250 .file_name()
251 .map(|n| n.to_string_lossy().into_owned())
252 .unwrap_or_default();
253 format!("{pack}/{fname}")
254}
255
256#[derive(Debug, Clone, Serialize)]
259pub struct PackStatusResult {
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub message: Option<String>,
262 pub dry_run: bool,
263 pub packs: Vec<DisplayPack>,
264 #[serde(skip_serializing_if = "Vec::is_empty")]
268 pub warnings: Vec<String>,
269 #[serde(skip_serializing_if = "Vec::is_empty")]
273 pub notes: Vec<DisplayNote>,
274 #[serde(skip_serializing_if = "Vec::is_empty")]
276 pub conflicts: Vec<DisplayConflict>,
277 #[serde(skip_serializing_if = "Vec::is_empty")]
281 pub ignored_packs: Vec<String>,
282 pub view_mode: String,
285 pub group_mode: String,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
293pub enum ViewMode {
294 #[default]
295 Full,
296 Short,
297}
298
299impl ViewMode {
300 pub fn as_str(self) -> &'static str {
301 match self {
302 ViewMode::Full => "full",
303 ViewMode::Short => "short",
304 }
305 }
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
310pub enum GroupMode {
311 #[default]
312 Name,
313 Status,
314}
315
316impl GroupMode {
317 pub fn as_str(self) -> &'static str {
318 match self {
319 GroupMode::Name => "name",
320 GroupMode::Status => "status",
321 }
322 }
323}