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