diff options
Diffstat (limited to 'alacritty/src/migrate/mod.rs')
-rw-r--r-- | alacritty/src/migrate/mod.rs | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/alacritty/src/migrate/mod.rs b/alacritty/src/migrate/mod.rs new file mode 100644 index 00000000..ffd0d4b4 --- /dev/null +++ b/alacritty/src/migrate/mod.rs @@ -0,0 +1,318 @@ +//! Configuration file migration. + +use std::fmt::Debug; +use std::path::Path; +use std::{fs, mem}; + +use tempfile::NamedTempFile; +use toml_edit::{DocumentMut, Item}; + +use crate::cli::MigrateOptions; +use crate::config; + +mod yaml; + +/// 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(migration) => { + if !options.silent { + println!("{}", migration.success_message(false)); + } + }, + Err(err) => { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + }, + } +} + +/// Migrate a specific configuration file. +fn migrate_config<'a>( + options: &MigrateOptions, + path: &'a Path, + recursion_limit: usize, +) -> Result<Migration<'a>, 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()), + }; + + // Handle legacy YAML files. + if suffix == "yml" { + let new_path = yaml::migrate(options, path, recursion_limit, prefix)?; + return Ok(Migration::Yaml((path, new_path))); + } + + // TOML only does renames, so return early if they are disabled. + if options.skip_renames { + if options.dry_run { + eprintln!("Ignoring TOML file {path:?} since `--skip-renames` was supplied"); + } + return Ok(Migration::Toml(path)); + } + + // Read TOML file and perform all in-file migrations. + let toml = fs::read_to_string(path).map_err(|err| format!("{err}"))?; + let mut migrated = migrate_toml(toml)?; + + // Recursively migrate imports. + migrate_imports(options, path, &mut migrated, recursion_limit)?; + + // Write migrated TOML file. + write_results(options, path, &migrated.to_string())?; + + Ok(Migration::Toml(path)) +} + +/// Migrate TOML config to the latest version. +fn migrate_toml(toml: String) -> Result<DocumentMut, String> { + // Parse TOML file. + let mut document = match toml.parse::<DocumentMut>() { + Ok(document) => document, + Err(err) => return Err(format!("TOML parsing error: {err}")), + }; + + // Move `draw_bold_text_with_bright_colors` to its own section. + move_value(&mut document, &["draw_bold_text_with_bright_colors"], &[ + "colors", + "draw_bold_text_with_bright_colors", + ])?; + + // Move bindings to their own section. + move_value(&mut document, &["key_bindings"], &["keyboard", "bindings"])?; + move_value(&mut document, &["mouse_bindings"], &["mouse", "bindings"])?; + + // Avoid warnings due to introduction of the new `general` section. + move_value(&mut document, &["live_config_reload"], &["general", "live_config_reload"])?; + move_value(&mut document, &["working_directory"], &["general", "working_directory"])?; + move_value(&mut document, &["ipc_socket"], &["general", "ipc_socket"])?; + move_value(&mut document, &["import"], &["general", "import"])?; + move_value(&mut document, &["shell"], &["terminal", "shell"])?; + + Ok(document) +} + +/// Migrate TOML imports to the latest version. +fn migrate_imports( + options: &MigrateOptions, + path: &Path, + document: &mut DocumentMut, + recursion_limit: usize, +) -> Result<(), String> { + // Check if any imports need to be processed. + let imports = match document["general"].get("import").and_then(|i| i.as_array()) { + Some(array) if !array.is_empty() => array, + _ => return Ok(()), + }; + + // Abort once recursion limit is exceeded. + if recursion_limit == 0 { + return Err("Exceeded maximum configuration import depth".into()); + } + + // Migrate each import. + for import in imports.into_iter().filter_map(|item| item.as_str()) { + let normalized_path = config::normalize_import(path, import); + let migration = migrate_config(options, &normalized_path, recursion_limit)?; + if options.dry_run { + println!("{}", migration.success_message(true)); + } + } + + Ok(()) +} + +/// Move a TOML value from one map to another. +fn move_value(document: &mut DocumentMut, origin: &[&str], target: &[&str]) -> Result<(), String> { + // Find and remove the original item. + let (mut origin_key, mut origin_item) = (None, document.as_item_mut()); + for element in origin { + let table = match origin_item.as_table_like_mut() { + Some(table) => table, + None => panic!("Moving from unsupported TOML structure"), + }; + + let (key, item) = match table.get_key_value_mut(element) { + Some((key, item)) => (key, item), + None => return Ok(()), + }; + + dbg!(&key); + origin_key = Some(key); + origin_item = item; + + // Ensure no empty tables are left behind. + if let Some(table) = origin_item.as_table_mut() { + table.set_implicit(true) + } + } + + let origin_key_decor = + origin_key.map(|key| (key.leaf_decor().clone(), key.dotted_decor().clone())); + let origin_item = mem::replace(origin_item, Item::None); + + // Create all dependencies for the new location. + let mut target_item = document.as_item_mut(); + for (i, element) in target.iter().enumerate() { + let table = match target_item.as_table_like_mut() { + Some(table) => table, + None => panic!("Moving into unsupported TOML structure"), + }; + + if i + 1 == target.len() { + table.insert(element, origin_item); + // Move original key decorations. + if let Some((leaf, dotted)) = origin_key_decor { + let mut key = table.key_mut(element).unwrap(); + *key.leaf_decor_mut() = leaf; + *key.dotted_decor_mut() = dotted; + } + + break; + } else { + // Create missing parent tables. + target_item = target_item[element].or_insert(toml_edit::table()); + } + } + + Ok(()) +} + +/// Write migrated TOML to its target location. +fn write_results<P>(options: &MigrateOptions, path: P, toml: &str) -> Result<(), String> +where + P: AsRef<Path> + Debug, +{ + let path = path.as_ref(); + 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 { + // Atomically replace the configuration file. + let tmp = NamedTempFile::new_in(path.parent().unwrap()) + .map_err(|err| format!("could not create temporary file: {err}"))?; + fs::write(tmp.path(), toml).map_err(|err| format!("filesystem error: {err}"))?; + tmp.persist(path).map_err(|err| format!("atomic replacement failed: {err}"))?; + } + Ok(()) +} + +/// Performed migration mode. +enum Migration<'a> { + /// In-place TOML migration. + Toml(&'a Path), + /// YAML to TOML migration. + Yaml((&'a Path, String)), +} + +impl<'a> Migration<'a> { + /// Get the success message for this migration. + fn success_message(&self, import: bool) -> String { + match self { + Self::Yaml((original_path, new_path)) if import => { + format!("Successfully migrated import {original_path:?} to {new_path:?}") + }, + Self::Yaml((original_path, new_path)) => { + format!("Successfully migrated {original_path:?} to {new_path:?}") + }, + Self::Toml(original_path) if import => { + format!("Successfully migrated import {original_path:?}") + }, + Self::Toml(original_path) => format!("Successfully migrated {original_path:?}"), + } + } + + /// Get the file path after migration. + fn new_path(&self) -> String { + match self { + Self::Toml(path) => path.to_string_lossy().into(), + Self::Yaml((_, path)) => path.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_values() { + let input = r#" +# This is a root_value. +# +# Use it with care. +root_value = 3 + +[table] +table_value = 5 + +[preexisting] +not_moved = 9 + "#; + + let mut document = input.parse::<DocumentMut>().unwrap(); + + move_value(&mut document, &["root_value"], &["new_table", "root_value"]).unwrap(); + move_value(&mut document, &["table", "table_value"], &[ + "preexisting", + "subtable", + "new_name", + ]) + .unwrap(); + + let output = document.to_string(); + + let expected = r#" +[preexisting] +not_moved = 9 + +[preexisting.subtable] +new_name = 5 + +[new_table] + +# This is a root_value. +# +# Use it with care. +root_value = 3 + "#; + + assert_eq!(output, expected); + } +} |