#![deny(missing_docs)]
mod ansi;
mod config;
pub mod config_files;
mod doctor;
pub mod error;
pub mod git;
mod init;
pub mod line_prompter;
pub mod output;
pub mod pkg_list;
pub mod pkg_manager;
pub mod prompt;
pub mod schema;
use std::io::IsTerminal;
use clap::Subcommand;
use xshell::Shell;
use crate::{
config::Config,
config_files::{ConfigFileDirs, ConfigFiles},
error::Error,
git::{Git, GitCmd},
output::Output,
prompt::{DryRunPrompter, PreApplyDecision, Prompter, TerminalPrompter, YesPrompter},
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "lower")]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
impl ColorChoice {
pub fn enabled(self, stream_is_terminal: bool) -> bool {
match self {
Self::Always => true,
Self::Never => false,
Self::Auto => std::env::var_os("NO_COLOR").is_none() && stream_is_terminal,
}
}
}
#[derive(clap::Args, Debug)]
pub struct Args {
#[clap(long, global = true, value_enum, default_value_t = ColorChoice::Auto)]
pub color: ColorChoice,
}
#[derive(Subcommand, Debug)]
pub enum Cmd {
Apply {
#[clap(long, short)]
pull_config: bool,
#[clap(long, short = 'y', conflicts_with = "dry_run")]
yes: bool,
#[clap(long, short = 'n')]
dry_run: bool,
#[clap(long)]
allow_dirty: bool,
},
Status {
#[clap(long, short = 'd')]
diff: bool,
#[clap(long, short = 'a')]
all: bool,
},
Pkg {
#[clap(value_name = "PATTERN")]
pattern: Vec<String>,
#[clap(long)]
all: bool,
#[clap(long)]
all_hints: bool,
#[clap(long, short)]
verbose: bool,
},
Repo {
#[command(subcommand)]
command: GitCmd,
},
Init {
url: Option<String>,
#[clap(long, short, requires = "url")]
branch: Option<String>,
#[clap(long)]
apply: bool,
#[clap(long, short = 'y', requires = "apply")]
yes: bool,
},
Doctor,
Schema,
Completions {
shell: clap_complete::Shell,
},
}
impl Cmd {
fn should_update_self(&self, _args: &Args) -> bool {
match self {
Cmd::Apply { pull_config, .. } => *pull_config,
Cmd::Status { .. }
| Cmd::Pkg { .. }
| Cmd::Repo { .. }
| Cmd::Init { .. }
| Cmd::Doctor
| Cmd::Schema
| Cmd::Completions { .. } => false,
}
}
}
fn build_prompter(yes: bool, dry_run: bool, color: bool) -> Result<Box<dyn Prompter>, Error> {
if dry_run {
Ok(Box::new(DryRunPrompter::new(color)))
} else if yes {
Ok(Box::new(YesPrompter))
} else if std::io::stdin().is_terminal() {
Ok(Box::new(TerminalPrompter::new(color)?))
} else {
Err(Error::ApplyNeedsYesOrTty)
}
}
pub fn real_main(
args: &Args,
command: &Cmd,
dirs: &ConfigFileDirs,
output: &mut dyn Output,
) -> Result<(), Error> {
if let Cmd::Completions { .. } = command {
return Ok(());
}
if let Cmd::Init {
url,
branch,
apply,
yes,
} = command
{
return init::run(
url.as_deref(),
branch.as_deref(),
*apply,
*yes,
dirs,
args,
output,
);
}
if let Cmd::Doctor = command {
let sh = Shell::new().unwrap();
return doctor::run(args, dirs, &sh, output);
}
if let Cmd::Schema = command {
return schema::run(&mut std::io::stdout().lock());
}
let sh = Shell::new().unwrap();
let config = Config::load(dirs, &sh, command.should_update_self(args))?;
let mut config_files = ConfigFiles::new(dirs);
match command {
Cmd::Apply {
pull_config: _,
yes,
dry_run,
allow_dirty,
} => {
let stdout_color = args.color.enabled(std::io::stdout().is_terminal());
let mut prompter = build_prompter(*yes, *dry_run, stdout_color)?;
config.push_pkg_health(output)?;
let git = Git::new(dirs.zenops(), &sh);
if git.is_git_repo()? && git.has_uncommitted_changes()? {
config.check_own_status(&sh, output)?;
if *yes && !*allow_dirty {
return Err(Error::DirtyRepoRequiresAllowDirty(
dirs.zenops().to_path_buf(),
));
}
if !*allow_dirty {
git.print_pre_apply_summary(stdout_color)?;
match prompter.confirm_pre_apply()? {
PreApplyDecision::CommitAndPush { message } => {
git.commit_all_and_push(&message)?;
}
PreApplyDecision::Continue => {}
PreApplyDecision::Abort => return Ok(()),
}
}
}
config.update_config_files(&sh, &mut config_files)?;
config_files.apply_changes(output, prompter.as_mut())?;
}
Cmd::Status { diff: _, all: _ } => {
config.push_pkg_health(output)?;
config.check_own_status(&sh, output)?;
config.update_config_files(&sh, &mut config_files)?;
config_files.check_status(output)?;
}
Cmd::Pkg {
pattern,
all,
all_hints,
verbose,
} => {
pkg_list::push(
&config,
pkg_list::Options {
pattern: pattern.clone(),
all: *all,
all_hints: *all_hints,
verbose: *verbose,
},
output,
)?;
}
Cmd::Repo { command } => {
command.passthru_dispatch_in(dirs.zenops(), &sh)?;
}
Cmd::Init { .. } => unreachable!("handled before Config::load"),
Cmd::Doctor => unreachable!("handled before Config::load"),
Cmd::Schema => unreachable!("handled before Config::load"),
Cmd::Completions { .. } => unreachable!("handled before Config::load"),
}
Ok(())
}