use std::io::Write;
use clap::builder::NonEmptyStringValueParser;
use itertools::Itertools;
use tracing::instrument;
use crate::cli_util::{
get_new_config_file_path, run_ui_editor, serialize_config_value, user_error,
write_config_value_to_file, CommandError, CommandHelper,
};
use crate::config::{AnnotatedValue, ConfigSource};
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
#[command(group = clap::ArgGroup::new("config_level").multiple(false).required(true))]
pub(crate) struct ConfigArgs {
#[arg(long, group = "config_level")]
user: bool,
#[arg(long, group = "config_level")]
repo: bool,
}
impl ConfigArgs {
fn get_source_kind(&self) -> ConfigSource {
if self.user {
ConfigSource::User
} else if self.repo {
ConfigSource::Repo
} else {
panic!("No config_level provided");
}
}
}
#[derive(clap::Subcommand, Clone, Debug)]
pub(crate) enum ConfigCommand {
#[command(visible_alias("l"))]
List(ConfigListArgs),
#[command(visible_alias("g"))]
Get(ConfigGetArgs),
#[command(visible_alias("s"))]
Set(ConfigSetArgs),
#[command(visible_alias("e"))]
Edit(ConfigEditArgs),
#[command(visible_alias("p"))]
Path(ConfigPathArgs),
}
#[derive(clap::Args, Clone, Debug)]
#[command(group(clap::ArgGroup::new("specific").args(&["repo", "user"])))]
pub(crate) struct ConfigListArgs {
#[arg(value_parser = NonEmptyStringValueParser::new())]
pub name: Option<String>,
#[arg(long, conflicts_with = "specific")]
pub include_defaults: bool,
#[arg(long)]
pub include_overridden: bool,
#[arg(long)]
user: bool,
#[arg(long)]
repo: bool,
}
impl ConfigListArgs {
fn get_source_kind(&self) -> Option<ConfigSource> {
if self.user {
Some(ConfigSource::User)
} else if self.repo {
Some(ConfigSource::Repo)
} else {
None
}
}
}
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
pub(crate) struct ConfigGetArgs {
#[arg(required = true)]
name: String,
}
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct ConfigSetArgs {
#[arg(required = true)]
name: String,
#[arg(required = true)]
value: String,
#[clap(flatten)]
config_args: ConfigArgs,
}
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct ConfigEditArgs {
#[clap(flatten)]
pub config_args: ConfigArgs,
}
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct ConfigPathArgs {
#[clap(flatten)]
pub config_args: ConfigArgs,
}
#[instrument(skip_all)]
pub(crate) fn cmd_config(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &ConfigCommand,
) -> Result<(), CommandError> {
match subcommand {
ConfigCommand::List(sub_args) => cmd_config_list(ui, command, sub_args),
ConfigCommand::Get(sub_args) => cmd_config_get(ui, command, sub_args),
ConfigCommand::Set(sub_args) => cmd_config_set(ui, command, sub_args),
ConfigCommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args),
ConfigCommand::Path(sub_args) => cmd_config_path(ui, command, sub_args),
}
}
#[instrument(skip_all)]
pub(crate) fn cmd_config_list(
ui: &mut Ui,
command: &CommandHelper,
args: &ConfigListArgs,
) -> Result<(), CommandError> {
ui.request_pager();
let name_path = args
.name
.as_ref()
.map_or(vec![], |name| name.split('.').collect_vec());
let values = command.resolved_config_values(&name_path)?;
let mut wrote_values = false;
for AnnotatedValue {
path,
value,
source,
is_overridden,
} in &values
{
if *is_overridden && !args.include_overridden {
continue;
}
if let Some(target_source) = args.get_source_kind() {
if target_source != *source {
continue;
}
}
if !args.include_defaults && *source == ConfigSource::Default {
continue;
}
writeln!(
ui.stdout(),
"{}{}={}",
if *is_overridden { "# " } else { "" },
path.join("."),
serialize_config_value(value)
)?;
wrote_values = true;
}
if !wrote_values {
if let Some(name) = &args.name {
writeln!(ui.warning(), "No matching config key for {name}")?;
} else {
writeln!(ui.warning(), "No config to list")?;
}
}
Ok(())
}
#[instrument(skip_all)]
pub(crate) fn cmd_config_get(
ui: &mut Ui,
command: &CommandHelper,
args: &ConfigGetArgs,
) -> Result<(), CommandError> {
let value = command
.settings()
.config()
.get_string(&args.name)
.map_err(|err| match err {
config::ConfigError::Type {
origin,
unexpected,
expected,
key,
} => {
let expected = format!("a value convertible to {expected}");
let mut buf = String::new();
use std::fmt::Write;
write!(buf, "invalid type: {unexpected}, expected {expected}").unwrap();
if let Some(key) = key {
write!(buf, " for key `{key}`").unwrap();
}
if let Some(origin) = origin {
write!(buf, " in {origin}").unwrap();
}
CommandError::ConfigError(buf.to_string())
}
err => err.into(),
})?;
writeln!(ui.stdout(), "{value}")?;
Ok(())
}
#[instrument(skip_all)]
pub(crate) fn cmd_config_set(
_ui: &mut Ui,
command: &CommandHelper,
args: &ConfigSetArgs,
) -> Result<(), CommandError> {
let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
if config_path.is_dir() {
return Err(user_error(format!(
"Can't set config in path {path} (dirs not supported)",
path = config_path.display()
)));
}
write_config_value_to_file(&args.name, &args.value, &config_path)
}
#[instrument(skip_all)]
pub(crate) fn cmd_config_edit(
_ui: &mut Ui,
command: &CommandHelper,
args: &ConfigEditArgs,
) -> Result<(), CommandError> {
let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
run_ui_editor(command.settings(), &config_path)
}
#[instrument(skip_all)]
pub(crate) fn cmd_config_path(
ui: &mut Ui,
command: &CommandHelper,
args: &ConfigPathArgs,
) -> Result<(), CommandError> {
let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
writeln!(
ui.stdout(),
"{}",
config_path
.to_str()
.ok_or_else(|| user_error("The config path is not valid UTF-8"))?
)?;
Ok(())
}