use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct Config {
pub theme: String,
pub tab_width: usize,
pub clipboard_sync: bool,
pub relative_number: bool,
pub wrap_lines: bool,
pub update_check: bool,
pub undo_caching: bool,
pub gpu_graphics: bool,
pub gpu_hyperlinks: bool,
pub gpu_acc: bool,
pub key_hints: bool,
pub lsp_enabled: bool,
pub lsp_servers: HashMap<String, String>,
pub pet_enabled: bool,
pub pet_path: String,
pub pet_x: u16,
pub pet_y: u16,
pub pet_width_cells: u16,
pub pet_speed: u16,
}
impl Default for Config {
fn default() -> Self {
Self {
theme: "ocean".into(),
tab_width: 4,
clipboard_sync: true,
relative_number: false,
wrap_lines: true,
update_check: true,
undo_caching: false,
gpu_graphics: true,
gpu_hyperlinks: true,
gpu_acc: true,
key_hints: true,
lsp_enabled: true,
lsp_servers: HashMap::new(),
pet_enabled: false,
pet_path: String::new(),
pet_x: 2,
pet_y: 2,
pet_width_cells: 12,
pet_speed: 100,
}
}
}
pub fn lsp_lang_catalog() -> &'static [(&'static str, &'static str, &'static str)] {
&[
("rust", "Rust", "rust-analyzer"),
("python", "Python", "pyright-langserver --stdio"),
("typescript", "TypeScript", "typescript-language-server --stdio"),
("javascript", "JavaScript", "typescript-language-server --stdio"),
("c", "C / C++", "clangd"),
("go", "Go", "gopls"),
("java", "Java", "jdtls"),
("lua", "Lua", "lua-language-server"),
("json", "JSON", "vscode-json-language-server --stdio"),
("yaml", "YAML", "yaml-language-server --stdio"),
("toml", "TOML", "taplo lsp stdio"),
("markdown", "Markdown", "marksman server"),
("bash", "Bash", "bash-language-server start"),
("zig", "Zig", "zls"),
]
}
fn config_path() -> PathBuf {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".xei.toml")
}
pub fn load() -> Config {
let mut cfg = Config::default();
let Ok(content) = fs::read_to_string(config_path()) else {
return cfg;
};
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((k, v)) = line.split_once('=') else {
continue;
};
let k = k.trim();
let v = v.trim().trim_matches('"').trim_matches('\'');
match k {
"theme" => {
if !v.is_empty() {
cfg.theme = v.to_string();
}
}
"tab_width" | "tabstop" => {
if let Ok(n) = v.parse::<usize>() {
if n > 0 && n <= 16 {
cfg.tab_width = n;
}
}
}
"clipboard_sync" => {
cfg.clipboard_sync = matches!(v, "true" | "1" | "yes" | "on");
}
"relative_number" | "relativenumber" => {
cfg.relative_number = matches!(v, "true" | "1" | "yes" | "on");
}
"wrap_lines" | "wrap" => {
cfg.wrap_lines = matches!(v, "true" | "1" | "yes" | "on");
}
"update_check" => {
cfg.update_check = matches!(v, "true" | "1" | "yes" | "on");
}
"undo_caching" => {
cfg.undo_caching = matches!(v, "true" | "1" | "yes" | "on");
}
"gpu_graphics" => {
cfg.gpu_graphics = matches!(v, "true" | "1" | "yes" | "on");
}
"gpu_hyperlinks" => {
cfg.gpu_hyperlinks = matches!(v, "true" | "1" | "yes" | "on");
}
"gpu_acc" | "gpu_acceleration" | "graphics" => {
cfg.gpu_acc = matches!(
v,
"true" | "1" | "yes" | "on" | "auto" | "kitty" | "ghostty"
);
}
"key_hints" | "which_key" | "chord_hints" => {
cfg.key_hints = matches!(v, "true" | "1" | "yes" | "on");
}
"lsp_enabled" | "lsp" => {
cfg.lsp_enabled = matches!(v, "true" | "1" | "yes" | "on");
}
"pet_enabled" | "pet" => {
cfg.pet_enabled = matches!(v, "true" | "1" | "yes" | "on");
}
"pet_path" => {
cfg.pet_path = v.to_string();
}
"pet_x" => {
if let Ok(n) = v.parse::<u16>() {
cfg.pet_x = n.min(10_000);
}
}
"pet_y" => {
if let Ok(n) = v.parse::<u16>() {
cfg.pet_y = n.min(10_000);
}
}
"pet_width_cells" | "pet_width" => {
if let Ok(n) = v.parse::<u16>() {
cfg.pet_width_cells = n.clamp(4, 80);
}
}
"pet_speed" => {
if let Ok(n) = v.parse::<u16>() {
cfg.pet_speed = n.clamp(25, 400);
}
}
k if k.starts_with("lsp.") => {
let lang = k.trim_start_matches("lsp.").trim().to_lowercase();
if !lang.is_empty() {
if matches!(v, "" | "off" | "none" | "false" | "0") {
cfg.lsp_servers.insert(lang, String::new());
} else {
cfg.lsp_servers.insert(lang, v.to_string());
}
}
}
_ => {}
}
}
cfg
}
pub fn save(cfg: &Config) {
let mut content = format!(
"# xei config\ntheme = \"{}\"\ntab_width = {}\nclipboard_sync = {}\nrelative_number = {}\nwrap_lines = {}\nupdate_check = {}\nundo_caching = {}\ngpu_graphics = {}\ngpu_hyperlinks = {}\ngpu_acc = {}\nkey_hints = {}\nlsp_enabled = {}\npet_enabled = {}\npet_path = \"{}\"\npet_x = {}\npet_y = {}\npet_width_cells = {}\npet_speed = {}\n",
cfg.theme,
cfg.tab_width,
cfg.clipboard_sync,
cfg.relative_number,
if cfg.wrap_lines { "true" } else { "false" },
if cfg.update_check { "true" } else { "false" },
if cfg.undo_caching { "true" } else { "false" },
if cfg.gpu_graphics { "true" } else { "false" },
if cfg.gpu_hyperlinks { "true" } else { "false" },
if cfg.gpu_acc { "true" } else { "false" },
if cfg.key_hints { "true" } else { "false" },
if cfg.lsp_enabled { "true" } else { "false" },
if cfg.pet_enabled { "true" } else { "false" },
cfg.pet_path.replace('"', ""),
cfg.pet_x,
cfg.pet_y,
cfg.pet_width_cells,
cfg.pet_speed.clamp(25, 400),
);
content.push_str("\n# LSP servers (empty / off = disabled; omit = built-in default)\n");
let mut seen = std::collections::HashSet::new();
for (key, _label, _default) in lsp_lang_catalog() {
if let Some(cmd) = cfg.lsp_servers.get(*key) {
seen.insert(key.to_string());
if cmd.is_empty() {
content.push_str(&format!("lsp.{key} = \"off\"\n"));
} else {
content.push_str(&format!("lsp.{key} = \"{}\"\n", cmd.replace('"', "")));
}
}
}
let mut extras: Vec<_> = cfg
.lsp_servers
.iter()
.filter(|(k, _)| !seen.contains(k.as_str()))
.collect();
extras.sort_by(|a, b| a.0.cmp(b.0));
for (k, cmd) in extras {
if cmd.is_empty() {
content.push_str(&format!("lsp.{k} = \"off\"\n"));
} else {
content.push_str(&format!("lsp.{k} = \"{}\"\n", cmd.replace('"', "")));
}
}
let _ = fs::write(config_path(), content);
}
pub fn save_theme(name: &str) {
let mut cfg = load();
cfg.theme = name.to_string();
save(&cfg);
}
pub fn load_theme() -> Option<String> {
let cfg = load();
if cfg.theme.is_empty() {
None
} else {
Some(cfg.theme)
}
}