1use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Clone, Debug)]
8pub struct Config {
9 pub theme: String,
10 pub tab_width: usize,
12 pub clipboard_sync: bool,
14 pub relative_number: bool,
16 pub wrap_lines: bool,
18 pub update_check: bool,
20 pub undo_caching: bool,
22 pub gpu_graphics: bool,
24 pub gpu_hyperlinks: bool,
26 pub gpu_acc: bool,
28 pub key_hints: bool,
30 pub lsp_enabled: bool,
32 pub lsp_servers: HashMap<String, String>,
37 pub pet_enabled: bool,
39 pub pet_path: String,
40 pub pet_x: u16,
41 pub pet_y: u16,
42 pub pet_width_cells: u16,
43 pub pet_speed: u16,
45}
46
47impl Default for Config {
48 fn default() -> Self {
49 Self {
50 theme: "ocean".into(),
51 tab_width: 4,
52 clipboard_sync: true,
53 relative_number: false,
54 wrap_lines: true,
55 update_check: true,
56 undo_caching: false,
57 gpu_graphics: true,
58 gpu_hyperlinks: true,
59 gpu_acc: true,
60 key_hints: true,
61 lsp_enabled: true,
62 lsp_servers: HashMap::new(),
63 pet_enabled: false,
64 pet_path: String::new(),
65 pet_x: 2,
66 pet_y: 2,
67 pet_width_cells: 12,
68 pet_speed: 100,
69 }
70 }
71}
72
73pub fn lsp_lang_catalog() -> &'static [(&'static str, &'static str, &'static str)] {
75 &[
77 ("rust", "Rust", "rust-analyzer"),
78 ("python", "Python", "pyright-langserver --stdio"),
79 ("typescript", "TypeScript", "typescript-language-server --stdio"),
80 ("javascript", "JavaScript", "typescript-language-server --stdio"),
81 ("c", "C / C++", "clangd"),
82 ("go", "Go", "gopls"),
83 ("java", "Java", "jdtls"),
84 ("lua", "Lua", "lua-language-server"),
85 ("json", "JSON", "vscode-json-language-server --stdio"),
86 ("yaml", "YAML", "yaml-language-server --stdio"),
87 ("toml", "TOML", "taplo lsp stdio"),
88 ("markdown", "Markdown", "marksman server"),
89 ("bash", "Bash", "bash-language-server start"),
90 ("zig", "Zig", "zls"),
91 ]
92}
93
94fn config_path() -> PathBuf {
95 let home = std::env::var("HOME")
96 .or_else(|_| std::env::var("USERPROFILE"))
97 .unwrap_or_else(|_| ".".to_string());
98 PathBuf::from(home).join(".xei.toml")
99}
100
101pub fn load() -> Config {
102 let mut cfg = Config::default();
103 let Ok(content) = fs::read_to_string(config_path()) else {
104 return cfg;
105 };
106 for line in content.lines() {
107 let line = line.trim();
108 if line.is_empty() || line.starts_with('#') {
109 continue;
110 }
111 let Some((k, v)) = line.split_once('=') else {
112 continue;
113 };
114 let k = k.trim();
115 let v = v.trim().trim_matches('"').trim_matches('\'');
116 match k {
117 "theme" => {
118 if !v.is_empty() {
119 cfg.theme = v.to_string();
120 }
121 }
122 "tab_width" | "tabstop" => {
123 if let Ok(n) = v.parse::<usize>() {
124 if n > 0 && n <= 16 {
125 cfg.tab_width = n;
126 }
127 }
128 }
129 "clipboard_sync" => {
130 cfg.clipboard_sync = matches!(v, "true" | "1" | "yes" | "on");
131 }
132 "relative_number" | "relativenumber" => {
133 cfg.relative_number = matches!(v, "true" | "1" | "yes" | "on");
134 }
135 "wrap_lines" | "wrap" => {
136 cfg.wrap_lines = matches!(v, "true" | "1" | "yes" | "on");
137 }
138 "update_check" => {
139 cfg.update_check = matches!(v, "true" | "1" | "yes" | "on");
140 }
141 "undo_caching" => {
142 cfg.undo_caching = matches!(v, "true" | "1" | "yes" | "on");
143 }
144 "gpu_graphics" => {
145 cfg.gpu_graphics = matches!(v, "true" | "1" | "yes" | "on");
146 }
147 "gpu_hyperlinks" => {
148 cfg.gpu_hyperlinks = matches!(v, "true" | "1" | "yes" | "on");
149 }
150 "gpu_acc" | "gpu_acceleration" | "graphics" => {
151 cfg.gpu_acc = matches!(
152 v,
153 "true" | "1" | "yes" | "on" | "auto" | "kitty" | "ghostty"
154 );
155 }
156 "key_hints" | "which_key" | "chord_hints" => {
157 cfg.key_hints = matches!(v, "true" | "1" | "yes" | "on");
158 }
159 "lsp_enabled" | "lsp" => {
160 cfg.lsp_enabled = matches!(v, "true" | "1" | "yes" | "on");
161 }
162 "pet_enabled" | "pet" => {
163 cfg.pet_enabled = matches!(v, "true" | "1" | "yes" | "on");
164 }
165 "pet_path" => {
166 cfg.pet_path = v.to_string();
167 }
168 "pet_x" => {
169 if let Ok(n) = v.parse::<u16>() {
170 cfg.pet_x = n.min(10_000);
172 }
173 }
174 "pet_y" => {
175 if let Ok(n) = v.parse::<u16>() {
176 cfg.pet_y = n.min(10_000);
177 }
178 }
179 "pet_width_cells" | "pet_width" => {
180 if let Ok(n) = v.parse::<u16>() {
181 cfg.pet_width_cells = n.clamp(4, 80);
182 }
183 }
184 "pet_speed" => {
185 if let Ok(n) = v.parse::<u16>() {
186 cfg.pet_speed = n.clamp(25, 400);
187 }
188 }
189 k if k.starts_with("lsp.") => {
190 let lang = k.trim_start_matches("lsp.").trim().to_lowercase();
191 if !lang.is_empty() {
192 if matches!(v, "" | "off" | "none" | "false" | "0") {
194 cfg.lsp_servers.insert(lang, String::new());
195 } else {
196 cfg.lsp_servers.insert(lang, v.to_string());
197 }
198 }
199 }
200 _ => {}
201 }
202 }
203 cfg
204}
205
206pub fn save(cfg: &Config) {
207 let mut content = format!(
208 "# 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",
209 cfg.theme,
210 cfg.tab_width,
211 cfg.clipboard_sync,
212 cfg.relative_number,
213 if cfg.wrap_lines { "true" } else { "false" },
214 if cfg.update_check { "true" } else { "false" },
215 if cfg.undo_caching { "true" } else { "false" },
216 if cfg.gpu_graphics { "true" } else { "false" },
217 if cfg.gpu_hyperlinks { "true" } else { "false" },
218 if cfg.gpu_acc { "true" } else { "false" },
219 if cfg.key_hints { "true" } else { "false" },
220 if cfg.lsp_enabled { "true" } else { "false" },
221 if cfg.pet_enabled { "true" } else { "false" },
222 cfg.pet_path.replace('"', ""),
223 cfg.pet_x,
224 cfg.pet_y,
225 cfg.pet_width_cells,
226 cfg.pet_speed.clamp(25, 400),
227 );
228 content.push_str("\n# LSP servers (empty / off = disabled; omit = built-in default)\n");
229 let mut seen = std::collections::HashSet::new();
231 for (key, _label, _default) in lsp_lang_catalog() {
232 if let Some(cmd) = cfg.lsp_servers.get(*key) {
233 seen.insert(key.to_string());
234 if cmd.is_empty() {
235 content.push_str(&format!("lsp.{key} = \"off\"\n"));
236 } else {
237 content.push_str(&format!("lsp.{key} = \"{}\"\n", cmd.replace('"', "")));
238 }
239 }
240 }
241 let mut extras: Vec<_> = cfg
242 .lsp_servers
243 .iter()
244 .filter(|(k, _)| !seen.contains(k.as_str()))
245 .collect();
246 extras.sort_by(|a, b| a.0.cmp(b.0));
247 for (k, cmd) in extras {
248 if cmd.is_empty() {
249 content.push_str(&format!("lsp.{k} = \"off\"\n"));
250 } else {
251 content.push_str(&format!("lsp.{k} = \"{}\"\n", cmd.replace('"', "")));
252 }
253 }
254 let _ = fs::write(config_path(), content);
255}
256
257pub fn save_theme(name: &str) {
258 let mut cfg = load();
259 cfg.theme = name.to_string();
260 save(&cfg);
261}
262
263pub fn load_theme() -> Option<String> {
264 let cfg = load();
265 if cfg.theme.is_empty() {
266 None
267 } else {
268 Some(cfg.theme)
269 }
270}