1pub mod addignore;
9pub mod adopt;
10pub mod down;
11pub mod fill;
12pub mod init;
13pub mod list;
14pub mod probe;
15pub mod status;
16pub mod tutorial;
17pub mod up;
18
19#[cfg(test)]
20mod tests;
21
22use serde::Serialize;
23
24pub fn handler_symbol(handler: &str) -> &'static str {
28 match handler {
29 "symlink" => "➞",
30 "shell" => "⚙",
31 "path" => "+",
32 "homebrew" => "⚙",
33 "install" => "×",
34 _ => "?",
35 }
36}
37
38pub fn status_style(deployed: bool) -> &'static str {
40 if deployed {
41 "deployed"
42 } else {
43 "pending"
44 }
45}
46
47pub fn handler_description(handler: &str, rel_path: &str, user_target: Option<&str>) -> String {
49 match handler {
50 "symlink" => {
51 user_target
58 .map(str::to_string)
59 .unwrap_or_else(|| "<symlink>".to_string())
60 }
61 "shell" => "shell profile".into(),
62 "path" => format!("$PATH/{rel_path}"),
63 "install" => "run script".into(),
64 "homebrew" => "brew install".into(),
65 _ => String::new(),
66 }
67}
68
69#[derive(Debug, Clone, Serialize)]
71pub struct DisplayFile {
72 pub name: String,
73 pub symbol: String,
74 pub description: String,
75 pub status: String,
76 pub status_label: String,
77 pub handler: String,
78 #[serde(skip_serializing_if = "Option::is_none", default)]
84 pub note_ref: Option<u32>,
85}
86
87#[derive(Debug, Clone, Serialize)]
89pub struct DisplayPack {
90 pub name: String,
91 pub files: Vec<DisplayFile>,
92 pub summary_status: String,
98 pub summary_count: usize,
101}
102
103impl DisplayPack {
104 pub fn new(name: String, files: Vec<DisplayFile>) -> Self {
106 let (summary_status, summary_count) = aggregate_status(&files);
107 DisplayPack {
108 name,
109 files,
110 summary_status,
111 summary_count,
112 }
113 }
114
115 pub fn recompute_summary(&mut self) {
119 let (status, count) = aggregate_status(&self.files);
120 self.summary_status = status;
121 self.summary_count = count;
122 }
123}
124
125fn aggregate_status(files: &[DisplayFile]) -> (String, usize) {
129 let mut errors = 0usize;
130 let mut pendings = 0usize;
131 let mut deployeds = 0usize;
132 for f in files {
133 match f.status.as_str() {
134 "error" | "broken" => errors += 1,
135 "pending" | "warning" | "stale" => pendings += 1,
136 "deployed" => deployeds += 1,
137 _ => {}
138 }
139 }
140 if errors > 0 {
141 ("error".into(), errors)
142 } else if pendings > 0 {
143 ("pending".into(), pendings)
144 } else {
145 ("deployed".into(), deployeds)
146 }
147}
148
149#[derive(Debug, Clone, Serialize)]
153pub struct DisplayNote {
154 pub body: String,
155 #[serde(skip_serializing_if = "Option::is_none", default)]
156 pub hint: Option<String>,
157}
158
159#[derive(Debug, Clone, Serialize)]
161pub struct DisplayClaimant {
162 pub pack: String,
164 pub source: String,
166}
167
168#[derive(Debug, Clone, Serialize)]
170pub struct DisplayConflict {
171 pub kind: String,
174 pub target: String,
176 pub claimants: Vec<DisplayClaimant>,
177}
178
179impl DisplayConflict {
180 pub fn from_conflict(c: &crate::conflicts::Conflict, home: &std::path::Path) -> Self {
183 let kind = match c.kind {
184 crate::conflicts::ConflictKind::SymlinkTarget => "symlink",
185 crate::conflicts::ConflictKind::PathExecutable => "path",
186 };
187 let target = match c.kind {
188 crate::conflicts::ConflictKind::SymlinkTarget => shorten_path(&c.target, home),
189 crate::conflicts::ConflictKind::PathExecutable => c
190 .target
191 .file_name()
192 .map(|n| n.to_string_lossy().into_owned())
193 .unwrap_or_else(|| c.target.display().to_string()),
194 };
195 let claimants = c
196 .claimants
197 .iter()
198 .map(|cl| DisplayClaimant {
199 pack: cl.pack.clone(),
200 source: pack_relative_source(&cl.source, &cl.pack),
201 })
202 .collect();
203 DisplayConflict {
204 kind: kind.into(),
205 target,
206 claimants,
207 }
208 }
209}
210
211fn shorten_path(p: &std::path::Path, home: &std::path::Path) -> String {
212 if let Ok(rel) = p.strip_prefix(home) {
213 format!("~/{}", rel.display())
214 } else {
215 p.display().to_string()
216 }
217}
218
219fn pack_relative_source(source: &std::path::Path, pack: &str) -> String {
222 let s = source.to_string_lossy();
223 let marker = format!("/{pack}/");
224 if let Some(idx) = s.rfind(&marker) {
225 let rel = &s[idx + 1..];
226 return rel.to_string();
227 }
228 let fname = source
230 .file_name()
231 .map(|n| n.to_string_lossy().into_owned())
232 .unwrap_or_default();
233 format!("{pack}/{fname}")
234}
235
236#[derive(Debug, Clone, Serialize)]
239pub struct PackStatusResult {
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub message: Option<String>,
242 pub dry_run: bool,
243 pub packs: Vec<DisplayPack>,
244 #[serde(skip_serializing_if = "Vec::is_empty")]
248 pub warnings: Vec<String>,
249 #[serde(skip_serializing_if = "Vec::is_empty")]
253 pub notes: Vec<DisplayNote>,
254 #[serde(skip_serializing_if = "Vec::is_empty")]
256 pub conflicts: Vec<DisplayConflict>,
257 #[serde(skip_serializing_if = "Vec::is_empty")]
261 pub ignored_packs: Vec<String>,
262 pub view_mode: String,
265 pub group_mode: String,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
273pub enum ViewMode {
274 #[default]
275 Full,
276 Short,
277}
278
279impl ViewMode {
280 pub fn as_str(self) -> &'static str {
281 match self {
282 ViewMode::Full => "full",
283 ViewMode::Short => "short",
284 }
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
290pub enum GroupMode {
291 #[default]
292 Name,
293 Status,
294}
295
296impl GroupMode {
297 pub fn as_str(self) -> &'static str {
298 match self {
299 GroupMode::Name => "name",
300 GroupMode::Status => "status",
301 }
302 }
303}