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