diff options
Diffstat (limited to 'alacritty/src/migrate.rs')
-rw-r--r-- | alacritty/src/migrate.rs | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/alacritty/src/migrate.rs b/alacritty/src/migrate.rs new file mode 100644 index 00000000..07a10ba4 --- /dev/null +++ b/alacritty/src/migrate.rs @@ -0,0 +1,249 @@ +//! 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 { + 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) { + 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:?}-----^" + ); + } 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}")), + }; + let new_path = migrate_config(options, &import, recursion_limit - 1)?; + 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 -> keyboard.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" + ); + } +} |