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