use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter};
use std::ops::AddAssign;
use std::rc::Rc;
use std::{fs, path};
use anyhow::{Error, Result, anyhow};
use nu_ansi_term::{Color, Style};
use crate::config::Config;
use crate::config::ExistingFilePolicy;
use crate::filesystem::{AbsPath, JoinOnRoot, Symlink, expand_to_path};
use crate::lock::{Lock, State};
#[derive(Debug)]
pub struct Table {
pub symp_toml_file: AbsPath,
pub packages: Vec<Rc<PackageData>>,
pub links: Vec<LinkData>,
}
impl Table {
fn new(symp_toml_file: &AbsPath) -> Result<Self> {
let symp_toml_file = symp_toml_file.clone();
let links = Vec::new();
let packages = Vec::new();
let table = Table {
symp_toml_file,
packages,
links,
};
Ok(table)
}
pub fn load(symp_toml_file: &AbsPath) -> Result<Self> {
let config: Config = toml::from_str(fs::read_to_string(symp_toml_file)?.as_str())?;
let mut table = Table::new(symp_toml_file)?;
let symp_toml_dir = symp_toml_file.parent_or_root();
let defaults = config.defaults.packages;
let packages = config.packages;
for (package_name, package) in packages.into_iter() {
let existing_file_policy = package
.existing_file_policy
.unwrap_or(defaults.existing_file_policy);
let raw_source_root = package
.source_root
.unwrap_or_else(|| defaults.source_root.clone());
let raw_destination_root = package
.destination_root
.unwrap_or_else(|| defaults.destination_root.clone());
let profiles = package.profiles;
let package_data = Rc::new(PackageData::new(
package_name,
existing_file_policy,
raw_source_root,
raw_destination_root,
&symp_toml_dir,
profiles,
)?);
for link in package.links {
let raw_source = link.source;
let raw_dest = link.destination;
let link_data = LinkData::new(package_data.clone(), raw_source, raw_dest)?;
if !table.links.contains(&link_data) {
table.links.push(link_data);
}
}
table.packages.push(package_data);
}
Ok(table)
}
pub fn check_packages_exist<'a>(
&self,
package_names: impl Iterator<Item = &'a String>,
) -> Result<()> {
let mut missing_package_names = Vec::new();
for package_name in package_names {
if !self
.packages
.iter()
.any(|pkg| pkg.package_name.eq(package_name))
{
missing_package_names.push(package_name.clone());
}
}
if missing_package_names.is_empty() {
Ok(())
} else {
Err(anyhow!(
"Packages not found: {}",
missing_package_names.join(", ")
))
}
}
pub fn validate_table_packages<'a>(
&self,
package_names: impl Iterator<Item = &'a String>,
) -> Result<()> {
let package_names: Vec<_> = package_names.collect();
let source_roots: Vec<_> = self
.packages
.iter()
.filter(|pkg| package_names.contains(&&pkg.package_name))
.map(|pkg| pkg.source_root.clone())
.collect();
let mut destination_path_counts: HashMap<AbsPath, usize> = HashMap::new();
for link in self
.links
.iter()
.filter(|&link| package_names.contains(&&link.package_data.package_name))
{
destination_path_counts
.entry(link.symlink.destination.clone())
.and_modify(|count| count.add_assign(1))
.or_insert(1);
}
let destination_counts = destination_path_counts;
let mut sorted_links: Vec<_> = self
.links
.iter()
.filter(|&link| package_names.contains(&&link.package_data.package_name))
.collect();
sorted_links.sort();
let sorted_links = sorted_links;
if sorted_links.is_empty() {
return Ok(());
}
let mut validation_errors: HashMap<&String, Vec<Error>> = HashMap::new();
for link in sorted_links {
let package_name = &link.package_data.package_name;
if !validation_errors.keys().any(|&name| name.eq(package_name)) {
validation_errors.insert(package_name, Vec::new());
}
if !link.symlink.source.exists() {
validation_errors
.get_mut(package_name)
.unwrap()
.push(anyhow!("{}: Source does not exist", format_link(link)));
}
if self.symp_toml_file.starts_with(&link.symlink.destination) {
validation_errors
.get_mut(package_name)
.unwrap()
.push(anyhow!(
"{}: Destination is parent of config file ({})",
format_link(link),
self.symp_toml_file.display()
))
}
if destination_counts
.get(&link.symlink.destination)
.unwrap()
.gt(&1)
{
validation_errors
.get_mut(package_name)
.unwrap()
.push(anyhow!(
"{}: Destination has multiple sources",
format_link(link)
))
}
for other in destination_counts.keys() {
if link.symlink.destination.starts_with(other) && link.symlink.destination.ne(other)
{
validation_errors
.get_mut(package_name)
.unwrap()
.push(anyhow!(
"{}: Destination is child of another destination ({})",
format_link(link),
format_dest(other),
))
}
}
for source_root in source_roots.iter() {
if link.symlink.destination.starts_with(source_root) {
validation_errors
.get_mut(package_name)
.unwrap()
.push(anyhow!(
"{}: Destination is child of source root ({})",
format_link(link),
format_source_root(source_root)
))
}
}
}
let validation_errors = validation_errors;
if validation_errors.values().any(|errors| !errors.is_empty()) {
let mut error_text = "Invalid symp.toml file (see below)\n".to_string();
for (package_name, errors) in validation_errors {
error_text.push('\n');
let package = self
.packages
.iter()
.find(|&pkg| pkg.package_name.eq(package_name))
.unwrap();
error_text.push_str(format!("{}\n", format_package(package)).as_str());
for error in errors {
error_text.push_str(format!(" {}\n", error).as_str());
}
}
Err(anyhow!(error_text))
} else {
Ok(())
}
}
pub fn print_status<'a>(
&self,
package_names: impl Iterator<Item = &'a String>,
lock: &Lock,
) -> Result<()> {
let package_names: Vec<_> = package_names.collect();
let mut sorted_links: Vec<_> = self
.links
.iter()
.filter(|&link| package_names.contains(&&link.package_data.package_name))
.collect();
sorted_links.sort();
let sorted_links = sorted_links;
if sorted_links.is_empty() {
return Ok(());
}
let lock_package_names = lock.all_package_names();
let mut current = &sorted_links[0].package_data;
println!();
println!("{}", format_package(current));
for link in sorted_links {
if current.ne(&link.package_data) {
current = &link.package_data;
println!("{}", format_package(current));
}
let lock_state = lock
.links
.get(&link.package_data.package_name)
.and_then(|lock_links| {
lock_links.iter().find(|&lock_link| {
lock_link.symlink.source.eq(&link.symlink.source)
&& lock_link.symlink.destination.eq(&link.symlink.destination)
})
})
.map(|lock_link| lock_link.state);
let status = {
if !lock_package_names.contains(&link.package_data.package_name) {
Status::Ignored
} else if link.symlink.exists() {
Status::Synced
} else if let Some(state) = lock_state {
match state {
State::Synced => Status::Broken,
State::Added => Status::Added,
State::MarkedForRemoval => Status::Ignored,
}
} else {
Status::New
}
};
println!(" {} {}", status, format_link(link));
}
Ok(())
}
pub fn get_package_names_from_profile(&self, profile: &str) -> Result<Vec<String>> {
let mut package_names = Vec::new();
for package in self.packages.iter() {
if package.profiles.contains(profile) {
package_names.push(package.package_name.to_string());
}
}
if package_names.is_empty() {
Err(anyhow!("Profile not found: {}.", profile))
} else {
Ok(package_names)
}
}
}
#[derive(Debug)]
pub struct PackageData {
pub package_name: String,
pub existing_file_policy: ExistingFilePolicy,
pub source_root: AbsPath,
pub destination_root: AbsPath,
pub profiles: HashSet<String>,
}
impl PartialEq for PackageData {
fn eq(&self, other: &Self) -> bool {
self.package_name.eq(&other.package_name)
}
}
impl Eq for PackageData {}
impl PartialOrd for PackageData {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PackageData {
fn cmp(&self, other: &Self) -> Ordering {
self.package_name.cmp(&other.package_name)
}
}
impl PackageData {
fn new(
package_name: String,
existing_file_policy: ExistingFilePolicy,
raw_source_root: String,
raw_destination_root: String,
symp_toml_dir: &AbsPath,
profiles: HashSet<String>,
) -> Result<Self> {
let source_root = AbsPath::from_owned_path(path::absolute(
symp_toml_dir.join(expand_to_path(&raw_source_root)?),
)?)?;
let destination_root = AbsPath::from_owned_path(path::absolute(
symp_toml_dir.join(expand_to_path(&raw_destination_root)?),
)?)?;
let package_data = PackageData {
package_name,
existing_file_policy,
source_root,
destination_root,
profiles,
};
Ok(package_data)
}
}
#[derive(Debug)]
pub struct LinkData {
pub package_data: Rc<PackageData>,
pub symlink: Symlink,
}
impl PartialEq for LinkData {
fn eq(&self, other: &Self) -> bool {
self.package_data.eq(&other.package_data) && self.symlink.eq(&other.symlink)
}
}
impl Eq for LinkData {}
impl PartialOrd for LinkData {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LinkData {
fn cmp(&self, other: &Self) -> Ordering {
self.package_data
.cmp(&other.package_data)
.then(self.symlink.cmp(&other.symlink))
}
}
impl LinkData {
fn new(package_data: Rc<PackageData>, raw_source: String, raw_dest: String) -> Result<Self> {
let source = expand_to_path(&raw_source)?.join_on_root(&package_data.source_root);
let destination = expand_to_path(&raw_dest)?.join_on_root(&package_data.destination_root);
let symlink = Symlink::new(source, destination);
let link_data = LinkData {
package_data,
symlink,
};
Ok(link_data)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Synced,
Added,
New,
Broken,
Ignored,
}
impl Display for Status {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Status::Synced => write!(
f,
"{}",
Color::Green.normal().paint(format!("{:>7}", "SYNCED"))
),
Status::Added => write!(
f,
"{}",
Color::Yellow.normal().paint(format!("{:>7}", "ADDED"))
),
Status::New => write!(f, "{}", Color::Cyan.normal().paint(format!("{:>7}", "NEW"))),
Status::Broken => write!(
f,
"{}",
Color::Red.normal().paint(format!("{:>7}", "BROKEN"))
),
Status::Ignored => write!(
f,
"{}",
Color::Fixed(245)
.normal()
.paint(format!("{:>7}", "IGNORED"))
),
}
}
}
fn format_link(link: &LinkData) -> String {
format!(
"({} -> {})",
Color::Rgb(91, 206, 250)
.normal()
.paint(format!("{}", link.symlink.source.display())),
Color::Rgb(245, 169, 184)
.normal()
.paint(format!("{}", link.symlink.destination.display())),
)
}
fn format_package(package: &PackageData) -> String {
format!(
"{} [{} -> {}]",
Style::new().bold().underline().paint(&package.package_name),
Color::Rgb(91, 206, 250)
.italic()
.paint(format!("{}", package.source_root.display())),
Color::Rgb(245, 169, 184)
.italic()
.paint(format!("{}", package.destination_root.display())),
)
}
fn format_dest(destination: &AbsPath) -> String {
format!(
"{}",
Color::Rgb(245, 169, 184)
.normal()
.paint(format!("{}", destination.display()))
)
}
fn format_source_root(source_root: &AbsPath) -> String {
format!(
"{}",
Color::Rgb(91, 206, 250)
.italic()
.paint(format!("{}", source_root.display()))
)
}