pub mod addignore;
pub mod adopt;
pub mod down;
pub mod fill;
pub mod git_alias;
pub mod git_filters;
pub mod init;
pub mod list;
pub mod probe;
pub mod prompts;
pub mod refresh;
pub mod secret;
pub mod status;
pub mod template_clean;
pub mod template_install_filter;
pub mod transform;
pub mod tutorial;
pub mod up;
#[cfg(test)]
mod tests;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct MessageResult {
pub message: String,
pub details: Vec<String>,
}
pub fn handler_symbol(handler: &str) -> &'static str {
match handler {
"symlink" => "➞",
"shell" => "⚙",
"path" => "+",
"homebrew" => "⚙",
"install" => "×",
"skip" => "·",
"gate" => "·",
_ => "?",
}
}
pub fn status_style(deployed: bool) -> &'static str {
if deployed {
"deployed"
} else {
"pending"
}
}
pub fn handler_description(handler: &str, rel_path: &str, user_target: Option<&str>) -> String {
match handler {
"symlink" => {
user_target
.map(str::to_string)
.unwrap_or_else(|| "<symlink>".to_string())
}
"shell" => "shell profile".into(),
"path" => format!("$PATH/{rel_path}"),
"install" => "run script".into(),
"homebrew" => "brew install".into(),
"skip" => "not deployed".into(),
"gate" => "not deployed".into(),
_ => String::new(),
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DisplayFile {
pub name: String,
pub symbol: String,
pub description: String,
pub status: String,
pub status_label: String,
pub handler: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub note_ref: Option<u32>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DisplayPack {
pub name: String,
pub files: Vec<DisplayFile>,
pub summary_status: String,
pub summary_count: usize,
}
impl DisplayPack {
pub fn new(name: String, files: Vec<DisplayFile>) -> Self {
let (summary_status, summary_count) = aggregate_status(&files);
DisplayPack {
name,
files,
summary_status,
summary_count,
}
}
pub fn recompute_summary(&mut self) {
let (status, count) = aggregate_status(&self.files);
self.summary_status = status;
self.summary_count = count;
}
}
fn aggregate_status(files: &[DisplayFile]) -> (String, usize) {
let mut errors = 0usize;
let mut pendings = 0usize;
let mut deployeds = 0usize;
for f in files {
match f.status.as_str() {
"error" | "broken" => errors += 1,
"pending" | "warning" | "stale" => pendings += 1,
"deployed" => deployeds += 1,
_ => {}
}
}
if errors > 0 {
("error".into(), errors)
} else if pendings > 0 {
("pending".into(), pendings)
} else {
("deployed".into(), deployeds)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DisplayNote {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub hint: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DisplayClaimant {
pub pack: String,
pub source: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DisplayConflict {
pub kind: String,
pub target: String,
pub claimants: Vec<DisplayClaimant>,
}
impl DisplayConflict {
pub fn from_conflict(c: &crate::conflicts::Conflict, home: &std::path::Path) -> Self {
let kind = match c.kind {
crate::conflicts::ConflictKind::SymlinkTarget => "symlink",
crate::conflicts::ConflictKind::PathExecutable => "path",
};
let target = match c.kind {
crate::conflicts::ConflictKind::SymlinkTarget => shorten_path(&c.target, home),
crate::conflicts::ConflictKind::PathExecutable => c
.target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| c.target.display().to_string()),
};
let claimants = c
.claimants
.iter()
.map(|cl| DisplayClaimant {
pack: cl.pack.clone(),
source: pack_relative_source(&cl.source, &cl.pack),
})
.collect();
DisplayConflict {
kind: kind.into(),
target,
claimants,
}
}
}
fn shorten_path(p: &std::path::Path, home: &std::path::Path) -> String {
if let Ok(rel) = p.strip_prefix(home) {
format!("~/{}", rel.display())
} else {
p.display().to_string()
}
}
fn pack_relative_source(source: &std::path::Path, pack: &str) -> String {
let s = source.to_string_lossy();
let marker = format!("/{pack}/");
if let Some(idx) = s.rfind(&marker) {
let rel = &s[idx + 1..];
return rel.to_string();
}
let fname = source
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
format!("{pack}/{fname}")
}
#[derive(Debug, Clone, Serialize)]
pub struct PackStatusResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
pub dry_run: bool,
pub packs: Vec<DisplayPack>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<DisplayNote>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub conflicts: Vec<DisplayConflict>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ignored_packs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub inactive_packs: Vec<String>,
pub view_mode: String,
pub group_mode: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ViewMode {
#[default]
Full,
Short,
}
impl ViewMode {
pub fn as_str(self) -> &'static str {
match self {
ViewMode::Full => "full",
ViewMode::Short => "short",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GroupMode {
#[default]
Name,
Status,
}
impl GroupMode {
pub fn as_str(self) -> &'static str {
match self {
GroupMode::Name => "name",
GroupMode::Status => "status",
}
}
}