alacritty/alacritty/src/migrate.rs

267 lines
7.9 KiB
Rust

//! Configuration file migration.
use std::fs;
use std::path::Path;
use toml::map::Entry;
use toml::{Table, Value};
use crate::cli::MigrateOptions;
use crate::config;
/// Handle migration.
pub fn migrate(options: MigrateOptions) {
// Find configuration file path.
let config_path = options
.config_file
.clone()
.or_else(|| config::installed_config("toml"))
.or_else(|| config::installed_config("yml"));
// Abort if system has no installed configuration.
let config_path = match config_path {
Some(config_path) => config_path,
None => {
eprintln!("No configuration file found");
std::process::exit(1);
},
};
// If we're doing a wet run, perform a dry run first for safety.
if !options.dry_run {
#[allow(clippy::redundant_clone)]
let mut options = options.clone();
options.silent = true;
options.dry_run = true;
if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) {
eprintln!("Configuration file migration failed:");
eprintln!(" {config_path:?}: {err}");
std::process::exit(1);
}
}
// Migrate the root config.
match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) {
Ok(new_path) => {
if !options.silent {
println!("Successfully migrated {config_path:?} to {new_path:?}");
}
},
Err(err) => {
eprintln!("Configuration file migration failed:");
eprintln!(" {config_path:?}: {err}");
std::process::exit(1);
},
}
}
/// Migrate a specific configuration file.
fn migrate_config(
options: &MigrateOptions,
path: &Path,
recursion_limit: usize,
) -> Result<String, String> {
// Ensure configuration file has an extension.
let path_str = path.to_string_lossy();
let (prefix, suffix) = match path_str.rsplit_once('.') {
Some((prefix, suffix)) => (prefix, suffix),
None => return Err("missing file extension".to_string()),
};
// Abort if config is already toml.
if suffix == "toml" {
return Err("already in TOML format".to_string());
}
// Try to parse the configuration file.
let mut config = match config::deserialize_config(path, !options.dry_run) {
Ok(config) => config,
Err(err) => return Err(format!("parsing error: {err}")),
};
// Migrate config imports.
if !options.skip_imports {
migrate_imports(options, &mut config, recursion_limit)?;
}
// Migrate deprecated field names to their new location.
if !options.skip_renames {
migrate_renames(&mut config)?;
}
// Convert to TOML format.
let toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?;
let new_path = format!("{prefix}.toml");
if options.dry_run && !options.silent {
// Output new content to STDOUT.
println!(
"\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^\n"
);
} else if !options.dry_run {
// Write the new toml configuration.
fs::write(&new_path, toml).map_err(|err| format!("filesystem error: {err}"))?;
}
Ok(new_path)
}
/// Migrate the imports of a config.
fn migrate_imports(
options: &MigrateOptions,
config: &mut Value,
recursion_limit: usize,
) -> Result<(), String> {
let imports = match config::imports(config, recursion_limit) {
Ok(imports) => imports,
Err(err) => return Err(format!("import error: {err}")),
};
// Migrate the individual imports.
let mut new_imports = Vec::new();
for import in imports {
let import = match import {
Ok(import) => import,
Err(err) => return Err(format!("import error: {err}")),
};
// Keep yaml import if path does not exist.
if !import.exists() {
if options.dry_run {
eprintln!("Keeping yaml config for nonexistent import: {import:?}");
}
new_imports.push(Value::String(import.to_string_lossy().into()));
continue;
}
let new_path = migrate_config(options, &import, recursion_limit - 1)?;
// Print new import path.
if options.dry_run {
println!("Successfully migrated import {import:?} to {new_path:?}");
}
new_imports.push(Value::String(new_path));
}
// Update the imports field.
if let Some(import) = config.get_mut("import") {
*import = Value::Array(new_imports);
}
Ok(())
}
/// Migrate deprecated fields.
fn migrate_renames(config: &mut Value) -> Result<(), String> {
let config_table = match config.as_table_mut() {
Some(config_table) => config_table,
None => return Ok(()),
};
// draw_bold_text_with_bright_colors -> colors.draw_bold_text_with_bright_colors
move_value(config_table, &["draw_bold_text_with_bright_colors"], &[
"colors",
"draw_bold_text_with_bright_colors",
])?;
// key_bindings -> keyboard.bindings
move_value(config_table, &["key_bindings"], &["keyboard", "bindings"])?;
// mouse_bindings -> mouse.bindings
move_value(config_table, &["mouse_bindings"], &["mouse", "bindings"])?;
Ok(())
}
/// Move a toml value from one map to another.
fn move_value(config_table: &mut Table, origin: &[&str], target: &[&str]) -> Result<(), String> {
if let Some(value) = remove_node(config_table, origin)? {
if !insert_node_if_empty(config_table, target, value)? {
return Err(format!(
"conflict: both `{}` and `{}` are set",
origin.join("."),
target.join(".")
));
}
}
Ok(())
}
/// Remove a node from a tree of tables.
fn remove_node(table: &mut Table, path: &[&str]) -> Result<Option<Value>, String> {
if path.len() == 1 {
Ok(table.remove(path[0]))
} else {
let next_table_value = match table.get_mut(path[0]) {
Some(next_table_value) => next_table_value,
None => return Ok(None),
};
let next_table = match next_table_value.as_table_mut() {
Some(next_table) => next_table,
None => return Err(format!("invalid `{}` table", path[0])),
};
remove_node(next_table, &path[1..])
}
}
/// Try to insert a node into a tree of tables.
///
/// Returns `false` if the node already exists.
fn insert_node_if_empty(table: &mut Table, path: &[&str], node: Value) -> Result<bool, String> {
if path.len() == 1 {
match table.entry(path[0]) {
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(node);
Ok(true)
},
Entry::Occupied(_) => Ok(false),
}
} else {
let next_table_value = table.entry(path[0]).or_insert_with(|| Value::Table(Table::new()));
let next_table = match next_table_value.as_table_mut() {
Some(next_table) => next_table,
None => return Err(format!("invalid `{}` table", path[0])),
};
insert_node_if_empty(next_table, &path[1..], node)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn move_values() {
let input = r#"
root_value = 3
[table]
table_value = 5
[preexisting]
not_moved = 9
"#;
let mut value: Value = toml::from_str(input).unwrap();
let table = value.as_table_mut().unwrap();
move_value(table, &["root_value"], &["new_table", "root_value"]).unwrap();
move_value(table, &["table", "table_value"], &["preexisting", "subtable", "new_name"])
.unwrap();
let output = toml::to_string(table).unwrap();
assert_eq!(
output,
"[new_table]\nroot_value = 3\n\n[preexisting]\nnot_moved = \
9\n\n[preexisting.subtable]\nnew_name = 5\n\n[table]\n"
);
}
}