Skip to main content

halley_config/parse/
loader.rs

1use rune_cfg::RuneConfig;
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::layout::RuntimeTuning;
7
8use super::keybinds::{
9    apply_explicit_keybind_overrides_entries, parse_inline_keybinds, strip_inline_keybind_block,
10};
11use super::rules::load_rules_section;
12use super::sections::{
13    load_animations_section, load_autostart_section, load_bearings_section, load_clusters_section,
14    load_cursor_section, load_decay_section, load_decorations_section, load_env_section,
15    load_field_section, load_focus_ring_section, load_font_section, load_input_section,
16    load_keybind_sections, load_nodes_section, load_overlays_section, load_physics_section,
17    load_screenshot_section, load_stacking_section, load_tile_section, load_trail_section,
18    load_viewport_section,
19};
20
21impl RuntimeTuning {
22    pub fn from_rune_file(path: &str) -> Option<Self> {
23        let raw = std::fs::read_to_string(path).ok()?;
24        let seed = Self::builtin_defaults();
25        let inline_keybinds = match parse_inline_keybinds(&raw) {
26            Ok(bindings) => bindings,
27            Err(err) => {
28                eprintln!("halley config keybind parse error: {err}");
29                return None;
30            }
31        };
32
33        let cfg = parse_rune_file_with_keybind_fallback(path, &raw)?;
34
35        Self::from_parsed_rune(raw.as_str(), &cfg, inline_keybinds, seed)
36    }
37
38    pub(crate) fn from_rune_str_with_seed(raw: &str, seed: Self) -> Option<Self> {
39        let inline_keybinds = match parse_inline_keybinds(raw) {
40            Ok(bindings) => bindings,
41            Err(err) => {
42                eprintln!("halley config keybind parse error: {err}");
43                return None;
44            }
45        };
46
47        let cfg = RuneConfig::from_str(raw).or_else(|_| {
48            let sanitized = strip_inline_keybind_block(raw);
49            RuneConfig::from_str(sanitized.as_str())
50        });
51        let cfg = cfg.ok()?;
52
53        Self::from_parsed_rune(raw, &cfg, inline_keybinds, seed)
54    }
55
56    pub fn from_rune_str(raw: &str) -> Option<Self> {
57        Self::from_rune_str_with_seed(raw, Self::builtin_defaults())
58    }
59
60    fn from_parsed_rune(
61        raw: &str,
62        cfg: &RuneConfig,
63        inline_keybinds: Vec<(String, String)>,
64        seed: Self,
65    ) -> Option<Self> {
66        let mut out = seed;
67
68        load_autostart_section(raw, &mut out);
69        if let Err(err) = load_rules_section(raw, &mut out) {
70            eprintln!("halley config rules parse error: {err}");
71            return None;
72        }
73        load_env_section(cfg, &mut out);
74        load_input_section(cfg, &mut out);
75        load_cursor_section(cfg, &mut out);
76        load_font_section(cfg, &mut out);
77        load_viewport_section(cfg, &mut out);
78        load_focus_ring_section(cfg, &mut out);
79        load_bearings_section(cfg, &mut out);
80        load_trail_section(cfg, &mut out);
81        load_nodes_section(cfg, &mut out);
82        load_clusters_section(cfg, &mut out);
83        load_tile_section(cfg, &mut out);
84        load_stacking_section(cfg, &mut out);
85        load_decay_section(cfg, &mut out);
86        load_field_section(cfg, &mut out);
87        load_physics_section(cfg, &mut out);
88        load_decorations_section(cfg, &mut out);
89        load_animations_section(cfg, &mut out);
90        load_overlays_section(cfg, &mut out);
91        load_screenshot_section(cfg, &mut out);
92        if let Err(err) = load_keybind_sections(cfg, &mut out) {
93            eprintln!("halley config keybind parse error: {err}");
94            return None;
95        }
96
97        if !inline_keybinds.is_empty() {
98            if let Err(err) = apply_explicit_keybind_overrides_entries(&inline_keybinds, &mut out) {
99                eprintln!("halley config keybind parse error: {err}");
100                return None;
101            }
102        }
103
104        Some(out)
105    }
106}
107
108pub fn from_rune_file(path: &str) -> Option<RuntimeTuning> {
109    RuntimeTuning::from_rune_file(path)
110}
111
112fn parse_rune_file_with_keybind_fallback(path: &str, raw: &str) -> Option<RuneConfig> {
113    RuneConfig::from_file(path).ok().or_else(|| {
114        let sanitized = strip_inline_keybind_block(raw);
115        parse_sanitized_rune_file(path, sanitized.as_str())
116            .or_else(|| RuneConfig::from_str(sanitized.as_str()).ok())
117    })
118}
119
120fn parse_sanitized_rune_file(original_path: &str, sanitized: &str) -> Option<RuneConfig> {
121    let original_path = Path::new(original_path);
122    let temp_dir = sanitized_config_temp_dir(original_path);
123    std::fs::create_dir_all(&temp_dir).ok()?;
124
125    let mut visited = HashMap::new();
126    let temp_path =
127        write_sanitized_config_tree(original_path, Some(sanitized), &temp_dir, &mut visited)?;
128    let cfg = RuneConfig::from_file(temp_path.as_path()).ok();
129    let _ = std::fs::remove_dir_all(&temp_dir);
130    cfg
131}
132
133fn write_sanitized_config_tree(
134    source_path: &Path,
135    raw_override: Option<&str>,
136    temp_dir: &Path,
137    visited: &mut HashMap<PathBuf, PathBuf>,
138) -> Option<PathBuf> {
139    let source_key = absolutize_config_path(source_path);
140    if let Some(existing) = visited.get(&source_key) {
141        return Some(existing.clone());
142    }
143
144    let temp_path = sanitized_config_temp_path(&source_key, temp_dir, visited.len());
145    visited.insert(source_key.clone(), temp_path.clone());
146
147    let raw = match raw_override {
148        Some(raw) => raw.to_string(),
149        None => std::fs::read_to_string(&source_key).ok()?,
150    };
151    let sanitized = strip_inline_keybind_block(&raw);
152    let rewritten = rewrite_gather_paths_to_sanitized_files(
153        sanitized.as_str(),
154        source_key.parent().unwrap_or_else(|| Path::new(".")),
155        temp_dir,
156        visited,
157    );
158
159    std::fs::write(&temp_path, rewritten).ok()?;
160    Some(temp_path)
161}
162
163fn rewrite_gather_paths_to_sanitized_files(
164    content: &str,
165    base_dir: &Path,
166    temp_dir: &Path,
167    visited: &mut HashMap<PathBuf, PathBuf>,
168) -> String {
169    let mut out = String::with_capacity(content.len());
170
171    for line in content.lines() {
172        if let Some(rewritten) = rewrite_gather_line(line, base_dir, temp_dir, visited) {
173            out.push_str(rewritten.as_str());
174        } else {
175            out.push_str(line);
176        }
177        out.push('\n');
178    }
179
180    out
181}
182
183fn rewrite_gather_line(
184    line: &str,
185    base_dir: &Path,
186    temp_dir: &Path,
187    visited: &mut HashMap<PathBuf, PathBuf>,
188) -> Option<String> {
189    let trimmed = line.trim_start();
190    if !trimmed.starts_with("gather") {
191        return None;
192    }
193
194    let indent_len = line.len() - trimmed.len();
195    let after_gather = trimmed.strip_prefix("gather")?.trim_start();
196    let quote = after_gather.chars().next()?;
197    if quote != '"' && quote != '\'' {
198        return None;
199    }
200
201    let close_relative = after_gather[1..].find(quote)?;
202    let raw_path = &after_gather[1..close_relative + 1];
203    let after_path = &after_gather[close_relative + 2..];
204    let import_path = resolve_gather_path_for_halley(raw_path, base_dir);
205
206    if !import_path.exists() {
207        return None;
208    }
209
210    let sanitized_import = write_sanitized_config_tree(&import_path, None, temp_dir, visited)?;
211    Some(format!(
212        "{}gather \"{}\"{}",
213        &line[..indent_len],
214        sanitized_import.to_string_lossy(),
215        after_path
216    ))
217}
218
219fn resolve_gather_path_for_halley(raw_path: &str, base_dir: &Path) -> PathBuf {
220    let mut path = if let Some(rest) = raw_path.strip_prefix("~/") {
221        std::env::var_os("HOME")
222            .map(PathBuf::from)
223            .unwrap_or_else(|| PathBuf::from("~"))
224            .join(rest)
225    } else {
226        PathBuf::from(raw_path)
227    };
228
229    if path.is_relative() {
230        path = base_dir.join(path);
231    }
232
233    absolutize_config_path(&path)
234}
235
236fn absolutize_config_path(path: &Path) -> PathBuf {
237    if path.is_absolute() {
238        path.to_path_buf()
239    } else {
240        std::env::current_dir()
241            .unwrap_or_else(|_| PathBuf::from("."))
242            .join(path)
243    }
244}
245
246fn sanitized_config_temp_dir(original_path: &Path) -> PathBuf {
247    let stem = original_path
248        .file_stem()
249        .and_then(|stem| stem.to_str())
250        .unwrap_or("halley");
251    let unique = SystemTime::now()
252        .duration_since(UNIX_EPOCH)
253        .map(|duration| duration.as_nanos())
254        .unwrap_or_default();
255
256    std::env::temp_dir().join(format!(
257        "{stem}.sanitized.{}.{}",
258        std::process::id(),
259        unique
260    ))
261}
262
263fn sanitized_config_temp_path(source_path: &Path, temp_dir: &Path, index: usize) -> PathBuf {
264    let stem = source_path
265        .file_stem()
266        .and_then(|stem| stem.to_str())
267        .unwrap_or("halley");
268    temp_dir.join(format!("{index}-{stem}.rune"))
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::layout::OverlayColorMode;
275
276    #[test]
277    fn from_rune_file_resolves_gather_when_inline_keybinds_require_sanitized_parse() {
278        let dir = test_temp_dir("gather-inline-keybinds");
279        let import_path = dir.join("colors.rune");
280        let config_path = dir.join("halley.rune");
281
282        std::fs::write(
283            &import_path,
284            r##"pywal_background "#123456"
285
286keybinds:
287  mod "super"
288  "$var.mod+q" "close-focused"
289end
290"##,
291        )
292        .unwrap();
293        std::fs::write(
294            &config_path,
295            r##"gather "colors.rune"
296
297screenshot:
298  background-colour pywal_background
299end
300
301keybinds:
302  mod "super"
303  "$var.mod+r" "reload"
304end
305"##,
306        )
307        .unwrap();
308
309        let tuning = RuntimeTuning::from_rune_file(config_path.to_str().unwrap())
310            .expect("config should parse with gathered colors and inline keybinds");
311
312        assert_eq!(
313            tuning.screenshot.background_color,
314            OverlayColorMode::Fixed {
315                r: 0x12 as f32 / 255.0,
316                g: 0x34 as f32 / 255.0,
317                b: 0x56 as f32 / 255.0,
318            }
319        );
320        assert!(tuning.keybinds.modifier.super_key);
321
322        let _ = std::fs::remove_dir_all(dir);
323    }
324
325    fn test_temp_dir(name: &str) -> PathBuf {
326        let unique = SystemTime::now()
327            .duration_since(UNIX_EPOCH)
328            .map(|duration| duration.as_nanos())
329            .unwrap_or_default();
330        let dir = std::env::temp_dir().join(format!(
331            "halley-config-{name}-{}-{unique}",
332            std::process::id()
333        ));
334        std::fs::create_dir_all(&dir).unwrap();
335        dir
336    }
337}