mod color_choice;
mod raw;
#[cfg(test)]
mod tests;
use std::env;
use std::io;
use std::path::{Path, PathBuf};
use std::process;
use anyhow::{anyhow, Context as ResultExt, Result};
use clap::{CommandFactory, Parser};
use crate::cli::raw::{Add, RawCommand, RawOpt};
use crate::config::{EditPlugin, GitReference, RawPlugin, Shell};
use crate::context::{log_error, Context, Output, Verbosity};
use crate::lock::LockMode;
use crate::util::build;
pub fn from_args() -> Opt {
Opt::from_raw_opt(RawOpt::parse())
}
#[derive(Debug)]
pub struct Opt {
pub ctx: Context,
pub command: Command,
}
#[derive(Debug)]
pub enum Command {
Init { shell: Option<Shell> },
Add {
name: String,
plugin: Box<EditPlugin>,
},
Edit,
Remove { name: String },
Lock,
Source,
}
impl Opt {
fn from_raw_opt(raw_opt: RawOpt) -> Self {
let RawOpt {
quiet,
non_interactive,
verbose,
color,
data_dir,
config_dir,
config_file,
profile,
command,
} = raw_opt;
let mut lock_mode = None;
let command = match command {
RawCommand::Init { shell } => Command::Init { shell },
RawCommand::Add(add) => {
let (name, plugin) = EditPlugin::from_add(*add);
Command::Add {
name,
plugin: Box::new(plugin),
}
}
RawCommand::Edit => Command::Edit,
RawCommand::Remove { name } => Command::Remove { name },
RawCommand::Lock { update, reinstall } => {
lock_mode = LockMode::from_lock_flags(update, reinstall);
Command::Lock
}
RawCommand::Source {
relock,
update,
reinstall,
} => {
lock_mode = LockMode::from_source_flags(relock, update, reinstall);
Command::Source
}
RawCommand::Completions { shell } => {
let mut app = RawOpt::command();
clap_complete::generate(shell, &mut app, build::CRATE_NAME, &mut io::stdout());
process::exit(0);
}
RawCommand::Version => {
println!("{} {}", build::CRATE_NAME, build::CRATE_VERBOSE_VERSION);
process::exit(0);
}
};
let verbosity = if quiet {
Verbosity::Quiet
} else if verbose {
Verbosity::Verbose
} else {
Verbosity::Normal
};
let output = Output {
verbosity,
no_color: !color.is_color(),
};
let home = match home::home_dir() {
Some(home) => home,
None => {
let err = anyhow!("failed to determine the current user's home directory");
log_error(output.no_color, &err);
process::exit(1);
}
};
let (config_dir, data_dir, config_file) =
match resolve_paths(&home, config_dir, data_dir, config_file) {
Ok(paths) => paths,
Err(err) => {
log_error(output.no_color, &err);
process::exit(1);
}
};
let lock_file = match profile.as_deref() {
Some("") | None => data_dir.join("plugins.lock"),
Some(p) => data_dir.join(format!("plugins.{p}.lock")),
};
let clone_dir = data_dir.join("repos");
let download_dir = data_dir.join("downloads");
let ctx = Context {
version: build::CRATE_RELEASE.to_string(),
home,
config_dir,
data_dir,
config_file,
lock_file,
clone_dir,
download_dir,
profile,
output,
interactive: !non_interactive,
lock_mode,
};
Self { ctx, command }
}
}
impl EditPlugin {
fn from_add(add: Add) -> (String, Self) {
let Add {
name,
git,
gist,
github,
remote,
local,
proto,
branch,
rev,
tag,
dir,
uses,
apply,
profiles,
hooks,
} = add;
let hooks = hooks.map(|h| h.into_iter().collect());
let reference = match (branch, rev, tag) {
(Some(s), None, None) => Some(GitReference::Branch(s)),
(None, Some(s), None) => Some(GitReference::Rev(s)),
(None, None, Some(s)) => Some(GitReference::Tag(s)),
(None, None, None) => None,
_ => unreachable!(),
};
(
name,
Self::from(RawPlugin {
git,
gist,
github,
remote,
local,
inline: None,
proto,
reference,
dir,
uses,
apply,
profiles,
hooks,
rest: None,
}),
)
}
}
impl LockMode {
fn from_lock_flags(update: bool, reinstall: bool) -> Option<Self> {
match (update, reinstall) {
(false, false) => Some(Self::Normal),
(true, false) => Some(Self::Update),
(false, true) => Some(Self::Reinstall),
(true, true) => unreachable!(),
}
}
fn from_source_flags(relock: bool, update: bool, reinstall: bool) -> Option<Self> {
match (relock, update, reinstall) {
(false, false, false) => None,
(true, false, false) => Some(Self::Normal),
(_, true, false) => Some(Self::Update),
(_, false, true) => Some(Self::Reinstall),
(_, true, true) => unreachable!(),
}
}
}
fn resolve_paths(
home: &Path,
config_dir: Option<PathBuf>,
data_dir: Option<PathBuf>,
config_file: Option<PathBuf>,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let (config_dir, config_file) = match (config_dir, config_file) {
(Some(dir), Some(file)) => (dir, file),
(None, Some(file)) => {
let dir = file
.parent()
.with_context(|| {
format!(
"failed to get parent directory of config file path `{}`",
file.display()
)
})?
.to_path_buf();
(dir, file)
}
(Some(dir), None) => {
let file = dir.join("plugins.toml");
(dir, file)
}
(None, None) => {
let dir = default_config_dir(home);
let file = dir.join("plugins.toml");
(dir, file)
}
};
let data_dir = data_dir.unwrap_or_else(|| default_data_dir(home));
Ok((config_dir, data_dir, config_file))
}
fn default_config_dir(home: &Path) -> PathBuf {
let mut p = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".config"));
p.push("sheldon");
p
}
fn default_data_dir(home: &Path) -> PathBuf {
let mut p = env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".local/share"));
p.push("sheldon");
p
}