use std::collections::HashMap;
use std::path::PathBuf;
use log::*;
use super::error::{ConfigError, IoResultExt};
use super::load::{find_config_path, find_drop_in_dir};
use super::schema::{ConditionalOverride, PersonalityDef};
use super::store::config;
pub fn resolve_personality(name: &str) -> Result<Option<PersonalityDef>, ConfigError> {
let mut chain: Vec<PersonalityDef> = Vec::new();
let mut visited: Vec<String> = Vec::new();
let mut current = Some(name.to_string());
while let Some(ref pname) = current {
if visited.contains(pname) {
visited.push(pname.clone());
let chain_str = visited.join(" \u{2192} ");
return Err(ConfigError::InheritanceCycle { chain: chain_str });
}
visited.push(pname.clone());
let Some(def) = lookup_personality(pname) else {
if chain.is_empty() {
return Ok(None); }
return Err(ConfigError::MissingParent {
child: visited[visited.len() - 2].clone(),
parent: pname.clone(),
});
};
let next = def.inherits.clone();
chain.push(def);
current = next;
}
let mut effective = PersonalityDef::default();
for def in chain.into_iter().rev() {
if def.format.is_some() {
effective.format = def.format;
}
if def.columns.is_some() {
effective.columns = def.columns;
}
for (key, value) in def.settings {
effective.settings.insert(key, value);
}
effective.when.extend(def.when);
}
for cond in &effective.when {
if cond.matches() {
debug!("conditional override matched: env = {:?}", cond.env);
for (key, value) in &cond.settings {
effective.settings.insert(key.clone(), value.clone());
}
}
}
Ok(Some(effective))
}
fn lookup_personality(name: &str) -> Option<PersonalityDef> {
if let Some(cfg) = config()
&& let Some(p) = cfg.personality.get(name) {
return Some(p.clone());
}
compiled_personality(name)
}
fn compiled_personality(name: &str) -> Option<PersonalityDef> {
use toml::Value::{Boolean, String as Str};
match name {
"default" => Some(PersonalityDef {
settings: HashMap::from([
("gradient".into(), Str("all".into())),
("group-dirs".into(), Str("none".into())),
("icons".into(), Str("never".into())),
("classify".into(), Str("never".into())),
("theme".into(), Str("exa".into())),
]),
when: vec![
ConditionalOverride {
env: HashMap::from([
("TERM".into(), toml::Value::String("*-256color".into())),
]),
settings: HashMap::from([
("theme".into(), toml::Value::String("lx-256".into())),
]),
},
ConditionalOverride {
env: HashMap::from([
("COLORTERM".into(), toml::Value::Array(vec![
toml::Value::String("truecolor".into()),
toml::Value::String("24bit".into()),
])),
]),
settings: HashMap::from([
("theme".into(), toml::Value::String("lx-24bit".into())),
]),
},
],
..Default::default()
}),
"lx" => Some(PersonalityDef {
inherits: Some("default".into()),
..Default::default()
}),
"ll" => Some(PersonalityDef {
inherits: Some("lx".into()),
format: Some("long2".into()),
settings: HashMap::from([
("group-dirs".into(), Str("first".into())),
]),
..Default::default()
}),
"lll" => Some(PersonalityDef {
inherits: Some("lx".into()),
format: Some("long3".into()),
settings: HashMap::from([
("group-dirs".into(), Str("first".into())),
("header".into(), Boolean(true)),
("time-style".into(), Str("long-iso".into())),
]),
..Default::default()
}),
"la" => Some(PersonalityDef {
inherits: Some("ll".into()),
settings: HashMap::from([
("all".into(), Boolean(true)),
]),
..Default::default()
}),
"tree" => Some(PersonalityDef {
inherits: Some("default".into()),
format: Some("long2".into()),
settings: HashMap::from([
("tree".into(), Boolean(true)),
("group-dirs".into(), Str("first".into())),
]),
..Default::default()
}),
"ls" => Some(PersonalityDef {
settings: HashMap::from([
("grid".into(), Boolean(true)),
("across".into(), Boolean(true)),
]),
..Default::default()
}),
_ => None,
}
}
const COMPILED_PERSONALITIES: &[&str] = &[
"default", "lx", "ll", "lll", "la", "tree", "ls",
];
pub fn all_personality_names() -> Vec<String> {
let mut names: Vec<String> = COMPILED_PERSONALITIES.iter()
.map(|s| (*s).into())
.collect();
if let Some(cfg) = config() {
for name in cfg.personality.keys() {
if !names.iter().any(|n| n == name) {
names.push(name.clone());
}
}
}
names.sort();
names
}
fn format_personality_toml(name: &str) -> Option<String> {
let def = lookup_personality(name)?;
let mut lines = vec![format!("[personality.{name}]")];
if let Some(ref inherits) = def.inherits {
lines.push(format!("inherits = \"{inherits}\""));
}
if let Some(ref format) = def.format {
lines.push(format!("format = \"{format}\""));
}
if let Some(ref columns) = def.columns {
let entries: Vec<String> = columns.to_csv()
.split(',')
.map(|s| format!("\"{}\"", s.trim()))
.collect();
lines.push(format!("columns = [{}]", entries.join(", ")));
}
let mut keys: Vec<_> = def.settings.keys().collect();
keys.sort();
for key in keys {
let value = &def.settings[key];
match value {
toml::Value::String(s) => lines.push(format!("{key} = \"{s}\"")),
toml::Value::Boolean(b) => lines.push(format!("{key} = {b}")),
toml::Value::Integer(i) => lines.push(format!("{key} = {i}")),
toml::Value::Float(f) => lines.push(format!("{key} = {f}")),
_ => lines.push(format!("{key} = {value}")),
}
}
Some(lines.join("\n"))
}
pub fn dump_personality(name: &str) -> Result<(), ConfigError> {
if let Some(toml) = format_personality_toml(name) {
println!("{toml}");
Ok(())
} else {
Err(ConfigError::NotFound {
kind: "personality",
kind_plural: "personalities",
name: name.to_string(),
candidates: all_personality_names().join(", "),
})
}
}
pub fn dump_personality_all() {
let names = all_personality_names();
let mut first = true;
for name in &names {
if let Some(toml) = format_personality_toml(name) {
if !first { println!(); }
println!("{toml}");
first = false;
}
}
}
pub fn save_personality_as(
name: &str,
inherits: Option<&str>,
settings: &HashMap<String, toml::Value>,
) -> Result<(), ConfigError> {
use chrono::Local;
let mut lines = vec![
format!("# Generated by lx --save-as on {}", Local::now().format("%Y-%m-%d")),
String::new(),
format!("[personality.{name}]"),
];
if let Some(parent) = inherits {
lines.push(format!("inherits = \"{parent}\""));
}
let mut keys: Vec<_> = settings.keys().collect();
keys.sort();
for key in keys {
let value = &settings[key];
match value {
toml::Value::String(s) => lines.push(format!("{key} = \"{s}\"")),
toml::Value::Boolean(b) => lines.push(format!("{key} = {b}")),
toml::Value::Integer(i) => lines.push(format!("{key} = {i}")),
toml::Value::Float(f) => lines.push(format!("{key} = {f}")),
_ => lines.push(format!("{key} = {value}")),
}
}
lines.push(String::new());
let toml_content = lines.join("\n");
let conf_dir = find_drop_in_dir(find_config_path().as_deref())
.unwrap_or_else(|| {
let xdg = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").expect("HOME not set");
PathBuf::from(home).join(".config")
});
xdg.join("lx").join("conf.d")
});
std::fs::create_dir_all(&conf_dir).with_path(&conf_dir)?;
let file_path = conf_dir.join(format!("{name}.toml"));
if file_path.exists() {
let backup = file_path.with_extension("toml.bak");
std::fs::rename(&file_path, &backup).with_path(&file_path)?;
eprintln!("lx: backed up {} → {}", file_path.display(), backup.display());
}
std::fs::write(&file_path, &toml_content).with_path(&file_path)?;
eprintln!("lx: saved personality '{name}' to {}", file_path.display());
Ok(())
}