use eyre::{Result, bail, eyre};
use toml_edit::DocumentMut;
use crate::config::settings::{SETTINGS_META, SettingsFile, SettingsType, parse_url_replacements};
use crate::toml::dedup_toml_array;
use crate::{config, duration, file};
#[derive(Debug, usage_rs::Args)]
#[usage(visible_aliases = ["create"], after_long_help = AFTER_LONG_HELP, verbatim_doc_comment)]
pub(super) struct SettingsSet {
#[usage()]
pub setting: String,
pub value: Option<String>,
#[usage(long, short)]
pub local: bool,
}
impl SettingsSet {
pub(super) fn run(self) -> Result<()> {
match self.value {
Some(value) => set(&self.setting, &value, false, self.local),
None => {
let (key, value) = self.setting.split_once('=').ok_or_else(|| {
eyre!(
"Usage: mise settings set <KEY>=<VALUE> or mise settings set <KEY> <VALUE>"
)
})?;
set(key, value, false, self.local)
}
}
}
}
pub(super) fn set(mut key: &str, value: &str, add: bool, local: bool) -> Result<()> {
let meta = match SETTINGS_META.get(key) {
Some(meta) => meta,
None => {
bail!("Unknown setting: {}", key);
}
};
if meta.env_only {
bail!(
"{key} cannot be set in a config file: mise reads it before config files load. Use the {} environment variable instead.",
meta.env.unwrap_or("matching MISE_*")
);
}
let value = match meta.type_ {
SettingsType::Bool => parse_bool(value)?,
SettingsType::Integer => parse_i64(value)?,
SettingsType::Duration => parse_duration(value)?,
SettingsType::Url | SettingsType::Path | SettingsType::String => value.into(),
SettingsType::ListString => parse_list_by_comma(value)?,
SettingsType::ListPath => parse_list_by_os_path_separator(value)?,
SettingsType::SetString => parse_set_by_comma(value)?,
SettingsType::IndexMap => parse_indexmap_by_json(value)?,
SettingsType::BoolOrString => parse_bool(value).unwrap_or_else(|_| value.into()),
};
let path = if local {
config::local_toml_config_path()
} else {
config::global_config_path()
};
file::create_dir_all(path.parent().unwrap())?;
let raw = file::read_to_string(&path).unwrap_or_default();
let mut config: DocumentMut = raw.parse()?;
if !config.contains_key("settings") {
let mut settings = toml_edit::Table::new();
settings.set_implicit(true);
config["settings"] = toml_edit::Item::Table(settings);
}
if let Some(settings) = config["settings"].as_table_like_mut() {
let settings: &mut dyn toml_edit::TableLike =
if let Some((parent_key, child_key)) = key.split_once('.') {
key = child_key;
settings
.entry(parent_key)
.or_insert({
let mut t = toml_edit::Table::new();
t.set_implicit(true);
toml_edit::Item::Table(t)
})
.as_table_like_mut()
.ok_or_else(|| eyre!("Setting [{parent_key}] is not a table"))?
} else {
settings
};
let value = match settings.get(key) {
Some(current) if add && current.as_array().is_some() => {
let array = current.as_array().unwrap();
let mut new_array = array.clone();
new_array.extend(value.as_array().unwrap().iter().cloned());
match meta.type_ {
SettingsType::SetString => dedup_toml_array(&new_array).into(),
_ => new_array.into(),
}
}
_ => value,
};
settings.insert(key, value.into());
let _: SettingsFile = toml::from_str(&config.to_string())?;
file::write(path, config.to_string())?;
}
Ok(())
}
fn parse_list_by_comma(value: &str) -> Result<toml_edit::Value> {
if value.is_empty() || value == "[]" {
return Ok(toml_edit::Array::new().into());
}
Ok(value.split(',').map(|s| s.trim().to_string()).collect())
}
fn parse_list_by_os_path_separator(value: &str) -> Result<toml_edit::Value> {
if value.is_empty() || value == "[]" {
return Ok(toml_edit::Array::new().into());
}
Ok(std::env::split_paths(value)
.map(|p| p.to_string_lossy().trim().to_string())
.filter(|s| !s.is_empty())
.collect())
}
fn parse_set_by_comma(value: &str) -> Result<toml_edit::Value> {
let value = value.trim();
if value.is_empty() || value == "[]" {
return Ok(toml_edit::Array::new().into());
}
let array: toml_edit::Array = value
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
Ok(dedup_toml_array(&array).into())
}
fn parse_bool(value: &str) -> Result<toml_edit::Value> {
match value.to_lowercase().as_str() {
"1" | "true" | "yes" | "y" => Ok(true.into()),
"0" | "false" | "no" | "n" => Ok(false.into()),
_ => Err(eyre!("{} must be true or false", value)),
}
}
fn parse_i64(value: &str) -> Result<toml_edit::Value> {
match value.parse::<i64>() {
Ok(value) => Ok(value.into()),
Err(_) => Err(eyre!("{} must be a number", value)),
}
}
fn parse_duration(value: &str) -> Result<toml_edit::Value> {
duration::parse_duration(value)?;
Ok(value.into())
}
fn parse_indexmap_by_json(value: &str) -> Result<toml_edit::Value> {
let index_map = parse_url_replacements(value)
.map_err(|e| eyre!("Failed to parse JSON for IndexMap: {}", e))?;
Ok(toml_edit::Value::InlineTable({
let mut table = toml_edit::InlineTable::new();
for (k, v) in index_map {
table.insert(&k, toml_edit::Value::String(toml_edit::Formatted::new(v)));
}
table
}))
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise settings idiomatic_version_file=true</bold>
"#
);
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn entries(value: toml_edit::Value) -> Vec<String> {
value
.as_array()
.expect("a path list parses to an array")
.iter()
.map(|v| v.as_str().expect("array of strings").to_string())
.collect()
}
fn parse(value: &str) -> Vec<String> {
entries(parse_list_by_os_path_separator(value).unwrap())
}
fn expect(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[cfg(windows)]
#[test]
fn windows_drive_letter_is_not_a_separator() {
assert_eq!(parse(r"C:\a"), expect(&[r"C:\a"]));
assert_eq!(
parse(r"C:\a\one;C:\b\two"),
expect(&[r"C:\a\one", r"C:\b\two"])
);
}
#[cfg(windows)]
#[test]
fn windows_quoted_paths_are_unquoted() {
assert_eq!(
parse(r#""C:\Program Files\one";"C:\Program Files\two""#),
expect(&[r"C:\Program Files\one", r"C:\Program Files\two"])
);
assert_eq!(parse(r#""C:\a b";C:\c"#), expect(&[r"C:\a b", r"C:\c"]));
}
#[cfg(not(windows))]
#[test]
fn unix_still_splits_on_colon() {
assert_eq!(parse("/a/one:/b/two"), expect(&["/a/one", "/b/two"]));
assert_eq!(parse("/a"), expect(&["/a"]));
}
#[test]
fn empty_input_parses_to_an_empty_array() {
assert!(parse("").is_empty());
assert!(parse("[]").is_empty());
}
#[test]
fn surrounding_whitespace_is_trimmed() {
#[cfg(windows)]
let (input, expected) = (r" C:\a ; C:\b ", expect(&[r"C:\a", r"C:\b"]));
#[cfg(not(windows))]
let (input, expected) = (" /a : /b ", expect(&["/a", "/b"]));
assert_eq!(parse(input), expected);
}
#[test]
fn empty_segments_are_dropped() {
#[cfg(windows)]
let (input, expected) = (r";C:\a;;C:\b;", expect(&[r"C:\a", r"C:\b"]));
#[cfg(not(windows))]
let (input, expected) = (":/a::/b:", expect(&["/a", "/b"]));
assert_eq!(parse(input), expected);
}
}