aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2024-09-23 02:15:52 +0200
committerChristian Duerr <contact@christianduerr.com>2024-10-02 21:38:07 +0200
commit3db09595f31bd9f2f211d43d96f0acb887a68991 (patch)
tree43ecfa0cff7705516085a869c664d1972591d0a2
parent51089cfeed1adfd741d8dcbe6c9cb9376f88a576 (diff)
downloadalacritty-3db09595f31bd9f2f211d43d96f0acb887a68991.tar.gz
alacritty-3db09595f31bd9f2f211d43d96f0acb887a68991.zip
Add migration support for TOML config changes
This patch allows running `alacritty migrate` to automatically apply configuration changes made to the TOML format, like moving `ipc_socket` to `general.ipc_socket`. This should reduce the friction of moving around individual options significantly, while also persisting the format of the existing TOML file thanks to `toml_edit`. The YAML migration has been simplified significantly to only switch the format of the file from YAML to TOML. The new TOML features are used for everything else.
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock48
-rw-r--r--alacritty/Cargo.toml4
-rw-r--r--alacritty/src/config/mod.rs33
-rw-r--r--alacritty/src/migrate.rs274
-rw-r--r--alacritty/src/migrate/mod.rs318
-rw-r--r--alacritty/src/migrate/yaml.rs87
-rw-r--r--alacritty_config_derive/src/config_deserialize/de_struct.rs1
-rw-r--r--alacritty_config_derive/tests/config.rs19
9 files changed, 476 insertions, 309 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b767eef..dc858911 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Notable changes to the `alacritty_terminal` crate are documented in its
### Added
- Support relative path imports from config files
+- `alacritty migrate` support for TOML configuration changes
### Changed
diff --git a/Cargo.lock b/Cargo.lock
index 566dbc08..39a672fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -58,7 +58,9 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
+ "tempfile",
"toml",
+ "toml_edit 0.22.21",
"unicode-width",
"windows-sys 0.52.0",
"winit",
@@ -891,9 +893,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.2.6"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
@@ -1749,9 +1751,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "0.6.6"
+version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
+checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
@@ -1878,6 +1880,19 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1931,14 +1946,14 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
- "toml_edit 0.22.15",
+ "toml_edit 0.22.21",
]
[[package]]
name = "toml_datetime"
-version = "0.6.6"
+version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
@@ -1956,15 +1971,15 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.22.15"
+version = "0.22.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1"
+checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
- "winnow 0.6.13",
+ "winnow 0.6.18",
]
[[package]]
@@ -2333,6 +2348,15 @@ dependencies = [
]
[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2574,9 +2598,9 @@ dependencies = [
[[package]]
name = "winnow"
-version = "0.6.13"
+version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
+checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]
diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml
index e8739049..ab44b046 100644
--- a/alacritty/Cargo.toml
+++ b/alacritty/Cargo.toml
@@ -34,10 +34,12 @@ libc = "0.2"
log = { version = "0.4", features = ["std", "serde"] }
notify = "6.1.1"
parking_lot = "0.12.0"
-serde = { version = "1", features = ["derive"] }
serde_json = "1"
+serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9.25"
+tempfile = "3.12.0"
toml = "0.8.2"
+toml_edit = "0.22.21"
unicode-width = "0.1"
winit = { version = "0.30.4", default-features = false, features = ["rwh_06", "serde"] }
diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs
index 9ee5215c..ba9d674d 100644
--- a/alacritty/src/config/mod.rs
+++ b/alacritty/src/config/mod.rs
@@ -301,7 +301,7 @@ pub fn imports(
let mut import_paths = Vec::new();
for import in imports {
- let mut path = match import {
+ let path = match import {
Value::String(path) => PathBuf::from(path),
_ => {
import_paths.push(Err("Invalid import element type: expected path string".into()));
@@ -309,23 +309,32 @@ pub fn imports(
},
};
- // Resolve paths relative to user's home directory.
- if let (Ok(stripped), Some(home_dir)) = (path.strip_prefix("~/"), home::home_dir()) {
- path = home_dir.join(stripped);
- }
-
- if path.is_relative() {
- if let Some(base_path) = base_path.parent() {
- path = base_path.join(path)
- }
- }
+ let normalized = normalize_import(base_path, path);
- import_paths.push(Ok(path));
+ import_paths.push(Ok(normalized));
}
Ok(import_paths)
}
+/// Normalize import paths.
+pub fn normalize_import(base_config_path: &Path, import_path: impl Into<PathBuf>) -> PathBuf {
+ let mut import_path = import_path.into();
+
+ // Resolve paths relative to user's home directory.
+ if let (Ok(stripped), Some(home_dir)) = (import_path.strip_prefix("~/"), home::home_dir()) {
+ import_path = home_dir.join(stripped);
+ }
+
+ if import_path.is_relative() {
+ if let Some(base_config_dir) = base_config_path.parent() {
+ import_path = base_config_dir.join(import_path)
+ }
+ }
+
+ import_path
+}
+
/// Prune the nulls from the YAML to ensure TOML compatibility.
fn prune_yaml_nulls(value: &mut serde_yaml::Value, warn_pruned: bool) {
fn walk(value: &mut serde_yaml::Value, warn_pruned: bool) -> bool {
diff --git a/alacritty/src/migrate.rs b/alacritty/src/migrate.rs
deleted file mode 100644
index 39779ba2..00000000
--- a/alacritty/src/migrate.rs
+++ /dev/null
@@ -1,274 +0,0 @@
-//! 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, path, 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,
- base_path: &Path,
- recursion_limit: usize,
-) -> Result<(), String> {
- let imports = match config::imports(config, base_path, 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"])?;
-
- // Avoid warnings due to introduction of the new `general` section.
- move_value(config_table, &["live_config_reload"], &["general", "live_config_reload"])?;
- move_value(config_table, &["working_directory"], &["general", "working_directory"])?;
- move_value(config_table, &["ipc_socket"], &["general", "ipc_socket"])?;
- move_value(config_table, &["import"], &["general", "import"])?;
- move_value(config_table, &["shell"], &["terminal", "shell"])?;
-
- 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"
- );
- }
-}
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);
+ }
+}
diff --git a/alacritty/src/migrate/yaml.rs b/alacritty/src/migrate/yaml.rs
new file mode 100644
index 00000000..9607e95e
--- /dev/null
+++ b/alacritty/src/migrate/yaml.rs
@@ -0,0 +1,87 @@
+//! Migration of legacy YAML files to TOML.
+
+use std::path::Path;
+
+use toml::Value;
+
+use crate::cli::MigrateOptions;
+use crate::config;
+use crate::migrate::{migrate_config, migrate_toml, write_results};
+
+/// Migrate a legacy YAML config to TOML.
+pub fn migrate(
+ options: &MigrateOptions,
+ path: &Path,
+ recursion_limit: usize,
+ prefix: &str,
+) -> Result<String, 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!("YAML parsing error: {err}")),
+ };
+
+ // Migrate config imports.
+ if !options.skip_imports {
+ migrate_imports(options, &mut config, path, recursion_limit)?;
+ }
+
+ // Convert to TOML format.
+ let mut toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?;
+ let new_path = format!("{prefix}.toml");
+
+ // Apply TOML migration, without recursing through imports.
+ toml = migrate_toml(toml)?.to_string();
+
+ // Write migrated TOML config.
+ write_results(options, &new_path, &toml)?;
+
+ Ok(new_path)
+}
+
+/// Migrate the imports of a config.
+fn migrate_imports(
+ options: &MigrateOptions,
+ config: &mut Value,
+ base_path: &Path,
+ recursion_limit: usize,
+) -> Result<(), String> {
+ let imports = match config::imports(config, base_path, 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 migration = migrate_config(options, &import, recursion_limit - 1)?;
+
+ // Print success message.
+ if options.dry_run {
+ println!("{}", migration.success_message(true));
+ }
+
+ new_imports.push(Value::String(migration.new_path()));
+ }
+
+ // Update the imports field.
+ if let Some(import) = config.get_mut("import") {
+ *import = Value::Array(new_imports);
+ }
+
+ Ok(())
+}
diff --git a/alacritty_config_derive/src/config_deserialize/de_struct.rs b/alacritty_config_derive/src/config_deserialize/de_struct.rs
index d2a7dd82..ad38863e 100644
--- a/alacritty_config_derive/src/config_deserialize/de_struct.rs
+++ b/alacritty_config_derive/src/config_deserialize/de_struct.rs
@@ -155,6 +155,7 @@ fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result
if let Some(warning) = parsed.param {
message = format!("{}; {}", message, warning.value());
}
+ message.push_str("\nUse `alacritty migrate` to automatically resolve it");
// Append stream to log deprecation/removal warning.
match_assignment_stream.extend(quote! {
diff --git a/alacritty_config_derive/tests/config.rs b/alacritty_config_derive/tests/config.rs
index 27a968ed..be140cbe 100644
--- a/alacritty_config_derive/tests/config.rs
+++ b/alacritty_config_derive/tests/config.rs
@@ -1,4 +1,4 @@
-use std::sync::{Arc, Mutex};
+use std::sync::{Arc, Mutex, OnceLock};
use log::{Level, Log, Metadata, Record};
use serde::Deserialize;
@@ -83,10 +83,8 @@ struct NewType(usize);
#[test]
fn config_deserialize() {
- let logger = unsafe {
- LOGGER = Some(Logger::default());
- LOGGER.as_mut().unwrap()
- };
+ static LOGGER: OnceLock<Logger> = OnceLock::new();
+ let logger = LOGGER.get_or_init(Logger::default);
log::set_logger(logger).unwrap();
log::set_max_level(log::LevelFilter::Warn);
@@ -134,15 +132,16 @@ fn config_deserialize() {
]);
let warn_logs = logger.warn_logs.lock().unwrap();
assert_eq!(warn_logs.as_slice(), [
- "Config warning: field1 has been deprecated; use field2 instead",
- "Config warning: enom_error has been deprecated",
- "Config warning: gone has been removed; it's gone",
+ "Config warning: field1 has been deprecated; use field2 instead\nUse `alacritty migrate` \
+ to automatically resolve it",
+ "Config warning: enom_error has been deprecated\nUse `alacritty migrate` to automatically \
+ resolve it",
+ "Config warning: gone has been removed; it's gone\nUse `alacritty migrate` to \
+ automatically resolve it",
"Unused config key: field3",
]);
}
-static mut LOGGER: Option<Logger> = None;
-
/// Logger storing all messages for later validation.
#[derive(Default)]
struct Logger {