use std::collections::HashMap;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use serde::Deserialize;
use super::error::{ConfigError, IoResultExt};
use super::load::{detect_config_version, inject_auto_select_blocks};
use super::schema::CONFIG_VERSION;
pub fn upgrade_config(path: &PathBuf) -> Result<(), ConfigError> {
let contents = fs::read_to_string(path).with_path(path)?;
let version = detect_config_version(&contents);
if version == CONFIG_VERSION {
return Err(ConfigError::AlreadyCurrent { path: path.clone() });
}
if version == "0.3" || version == "0.4" {
let mut warned_time = false;
let mut warned_numeric = false;
for line in contents.lines() {
let trimmed = line.trim();
if !trimmed.contains('=') {
continue;
}
if !warned_time && trimmed.starts_with("time") && !trimmed.starts_with("time-style") {
eprintln!(
"lx: warning: `time = \"...\"` is removed in config \
version 0.5; use `modified`, `changed`, `accessed`, \
or `created` booleans instead. Upgrading anyway."
);
warned_time = true;
}
if !warned_numeric && trimmed.starts_with("numeric") {
eprintln!(
"lx: warning: `numeric = ...` is removed in config \
version 0.5; UID and GID are first-class columns now. \
Use `uid = true, gid = true, no-user = true, \
no-group = true` for the old behaviour. Upgrading anyway."
);
warned_numeric = true;
}
if warned_time && warned_numeric {
break;
}
}
}
if version == "0.3" || version == "0.4" || version == "0.5" {
let backup = path.with_extension("toml.bak");
fs::copy(path, &backup).with_path(&backup)?;
let old_version_line = format!("version = \"{version}\"");
let new_version_line = format!("version = \"{CONFIG_VERSION}\"");
let mut updated = contents.replacen(&old_version_line, &new_version_line, 1);
let has_term_detection = updated.lines().any(|line| {
let l = line.trim_start();
l.starts_with("env.TERM ")
|| l.starts_with("env.TERM=")
|| l.starts_with("env.COLORTERM ")
|| l.starts_with("env.COLORTERM=")
});
if version == "0.5" && updated.contains("[personality.default]") && !has_term_detection {
updated = inject_auto_select_blocks(&updated);
eprintln!(
"Note: added auto-selection [[when]] blocks to \
[personality.default] so capable terminals get the \
lx-256 / lx-24bit themes automatically. Edit or \
delete to opt out."
);
}
let (rewritten, rewrites) = rewrite_colour_scale_to_gradient(&updated);
updated = rewritten;
if rewrites > 0 {
eprintln!(
"Note: rewrote {rewrites} `colour-scale = \"...\"` line{} \
to `gradient = \"...\"`. See `man lx` and \
`docs/UPGRADING.md` for the new vocabulary.",
if rewrites == 1 { "" } else { "s" },
);
}
fs::write(path, &updated).with_path(path)?;
eprintln!("Original config saved to {}", backup.display());
eprintln!(
"Upgraded {} from version {version} to {CONFIG_VERSION}",
path.display()
);
return Ok(());
}
let legacy: LegacyConfig = toml::from_str(&contents).map_err(|source| ConfigError::Parse {
path: path.clone(),
source,
})?;
let mut out = String::new();
writeln!(out, "version = \"{CONFIG_VERSION}\"").unwrap();
if !legacy.format.is_empty() {
out.push_str("\n[format]\n");
for (name, columns) in &legacy.format {
let cols = columns
.iter()
.map(|c| format!("\"{c}\""))
.collect::<Vec<_>>()
.join(", ");
writeln!(out, "{name} = [{cols}]").unwrap();
}
}
if version == "0.1" && !legacy.defaults.is_empty() {
out.push_str("\n[personality.default]\n");
for (key, value) in &legacy.defaults {
writeln!(out, "{key} = {value}").unwrap();
}
let has_lx = legacy.personality.contains_key("lx");
if has_lx {
out.push_str("\n[personality.lx]\n");
out.push_str("inherits = \"default\"\n");
if let Some(lx_p) = legacy.personality.get("lx") {
for (key, value) in lx_p {
if key != "inherits" {
writeln!(out, "{key} = {value}").unwrap();
}
}
}
} else {
out.push_str("\n[personality.lx]\ninherits = \"default\"\n");
}
}
for (name, settings) in &legacy.personality {
if version == "0.1" && name == "lx" {
continue; }
writeln!(out, "\n[personality.{name}]").unwrap();
for (key, value) in settings {
writeln!(out, "{key} = {value}").unwrap();
}
}
let backup = path.with_extension("toml.bak");
fs::copy(path, &backup).with_path(&backup)?;
fs::write(path, &out).with_path(path)?;
eprintln!("Original config saved to {}", backup.display());
eprintln!(
"Upgraded {} from version {version} to {CONFIG_VERSION}",
path.display()
);
eprintln!("Note: comments were not preserved. You may want to review the result.");
Ok(())
}
fn rewrite_colour_scale_to_gradient(contents: &str) -> (String, usize) {
let mut out = String::with_capacity(contents.len());
let mut count = 0;
for line in contents.split_inclusive('\n') {
let (body, newline) = match line.strip_suffix('\n') {
Some(b) => (b, "\n"),
None => (line, ""),
};
let trimmed = body.trim_start();
let indent_len = body.len() - trimmed.len();
let indent = &body[..indent_len];
let key_match = trimmed
.strip_prefix("colour-scale")
.or_else(|| trimmed.strip_prefix("color-scale"));
if let Some(rest) = key_match {
let after = rest.trim_start();
if let Some(value_part) = after.strip_prefix('=') {
let value_part = value_part.trim_start();
let (val_with_quotes, comment) = match value_part.find('#') {
Some(i) => (value_part[..i].trim_end(), &value_part[i..]),
None => (value_part.trim_end(), ""),
};
let stripped = val_with_quotes
.trim_start_matches(['"', '\''])
.trim_end_matches(['"', '\'']);
let new_value = match stripped {
"none" => Some("none"),
"16" | "256" => Some("all"),
_ => None,
};
if let Some(v) = new_value {
out.push_str(indent);
out.push_str("gradient = \"");
out.push_str(v);
out.push('"');
if !comment.is_empty() {
out.push(' ');
out.push_str(comment);
}
out.push_str(newline);
count += 1;
continue;
}
}
}
out.push_str(line);
}
(out, count)
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct LegacyConfig {
#[serde(default)]
defaults: HashMap<String, toml::Value>,
#[serde(default)]
format: HashMap<String, Vec<String>>,
#[serde(default)]
personality: HashMap<String, HashMap<String, toml::Value>>,
}
#[cfg(test)]
mod rewrite_test {
use super::rewrite_colour_scale_to_gradient;
#[test]
fn rewrites_none() {
let (out, n) =
rewrite_colour_scale_to_gradient("[personality.default]\ncolour-scale = \"none\"\n");
assert_eq!(n, 1);
assert!(out.contains("gradient = \"none\""));
assert!(!out.contains("colour-scale"));
}
#[test]
fn rewrites_16_to_all() {
let (out, n) =
rewrite_colour_scale_to_gradient("[personality.default]\ncolour-scale = \"16\"\n");
assert_eq!(n, 1);
assert!(out.contains("gradient = \"all\""));
}
#[test]
fn rewrites_256_to_all() {
let (out, n) =
rewrite_colour_scale_to_gradient("[personality.default]\ncolour-scale = \"256\"\n");
assert_eq!(n, 1);
assert!(out.contains("gradient = \"all\""));
}
#[test]
fn rewrites_color_scale_alias() {
let (out, n) =
rewrite_colour_scale_to_gradient("[personality.default]\ncolor-scale = \"none\"\n");
assert_eq!(n, 1);
assert!(out.contains("gradient = \"none\""));
}
#[test]
fn preserves_indent_and_trailing_comment() {
let (out, n) =
rewrite_colour_scale_to_gradient(" colour-scale = \"16\" # nice gradient\n");
assert_eq!(n, 1);
assert!(out.contains(" gradient = \"all\" # nice gradient"));
}
#[test]
fn does_not_rewrite_unknown_value() {
let (out, n) = rewrite_colour_scale_to_gradient("colour-scale = \"surprise\"\n");
assert_eq!(n, 0);
assert!(out.contains("colour-scale"));
}
#[test]
fn rewrites_inside_when_block() {
let input = "[[personality.lx.when]]\nenv.SSH_CONNECTION = true\ncolour-scale = \"none\"\n";
let (out, n) = rewrite_colour_scale_to_gradient(input);
assert_eq!(n, 1);
assert!(out.contains("gradient = \"none\""));
}
}