use crate::themes::theme::{CursorMapping, CursorType};
use std::{collections::HashMap, fs, path::Path};
use anyhow::{Result, anyhow, bail};
use configparser::ini::Ini;
const ROOT_KEYS: &[&str] = &["hkcr", "hkcu", "hklm", "hku", "hkcc"];
pub fn parse_inf_installer(
inf_path: &Path,
theme_dir: &Path,
) -> Result<(String, Vec<CursorMapping>)> {
let inf_string = fs::read_to_string(inf_path)?;
let inf = Ini::new()
.read(inf_string)
.map_err(|e| anyhow!("failed to read inf, error e={e}"))?;
let addreg = inf
.get("defaultinstall")
.ok_or_else(|| anyhow!("no defaultinstall section found"))?
.get("addreg")
.ok_or_else(|| anyhow!("no addreg key found in defaultinstall"))?
.as_ref()
.ok_or_else(|| anyhow!("no value for addreg key"))?;
let reg_section = addreg
.split_once(',')
.map_or(addreg.as_str(), |s| s.0)
.to_ascii_lowercase();
let reg = inf
.get(®_section)
.ok_or_else(|| anyhow!("no {reg_section} section found"))?;
if reg.keys().len() != 1 {
eprintln!(
"[warning] expected {reg_section} to have one key, instead \
has {}, only the first key will be parsed (reg={:?})",
reg.keys().len(),
reg
);
}
if reg.values().next() != Some(&None) {
bail!(
"expected no value (None) for reg, instead got {:?}",
reg.values().next()
)
}
let Some(reg) = reg.keys().next() else {
bail!("no cursor mappings found in reg (0 keys)");
};
let subs = inf.get("strings");
let expanded_reg = expand_reg(reg, subs)?;
let mut reg_info = expanded_reg.split(',');
let root_key = reg_info.next();
let _ = reg_info.next();
if !root_key.is_some_and(|k| ROOT_KEYS.contains(&k)) {
bail!("root_key={root_key:?} not in accepted ROOT_KEYS={ROOT_KEYS:?}");
}
let name = reg_info
.next()
.ok_or_else(|| anyhow!("couldn't parse theme name; reg_info doesn't have enough info"))?
.strip_prefix('"')
.unwrap_or_default()
.strip_suffix('"')
.map(str::to_string)
.ok_or_else(|| anyhow!("expected theme name to be quoted"))?;
reg_info.next();
let mut paths: Vec<_> = reg_info
.map(|s| {
s.rsplit_once('\\')
.ok_or_else(|| anyhow!("failed to extract filename from path, s={s}"))
.map(|s| s.1)
})
.collect::<Result<_>>()?;
if paths.len() != 17 {
eprintln!(
"[warning] expected 17 paths, instead got {} paths",
paths.len()
);
}
let end = paths.len() - 1;
paths[end] = paths[paths.len() - 1]
.strip_suffix('"')
.ok_or_else(|| anyhow!("expected closing quotation for paths, didn't find it"))?;
let mappings: Vec<_> = paths
.into_iter()
.zip(0..15)
.map(|(p, i)| CursorMapping {
r#type: index_to_cursor_type(i),
path: theme_dir.join(p),
})
.collect();
Ok((name, mappings))
}
#[rustfmt::skip]
const fn index_to_cursor_type(index: usize) -> CursorType {
use CursorType::*;
match index {
0 => Arrow, 1 => Help,
2 => LeftPtrWatch, 3 => Watch,
4 => Crosshair, 5 => Text,
6 => Pencil, 7 => Forbidden,
8 => NsResize, 9 => EwResize,
10 => NwseResize, 11 => NeswResize,
12 => Move, 13 => CenterPtr,
14 => Hand, _ => unreachable!(),
}
}
fn expand_reg(reg: &str, subs: Option<&HashMap<String, Option<String>>>) -> Result<String> {
let Some(subs) = subs else {
let empty: HashMap<String, String> = HashMap::new();
return expand(reg, &empty);
};
let subs: HashMap<_, _> = subs
.iter()
.filter_map(dequote_value)
.map(|(k, v)| (format!("%{k}%"), v))
.collect();
expand(reg, &subs)
}
fn dequote_value(entry: (&String, &Option<String>)) -> Option<(String, String)> {
match entry {
(k, Some(v)) => Some((
k.clone(),
v.strip_suffix('"')
.unwrap_or_default()
.strip_prefix('"')
.unwrap_or_default()
.to_string(),
)),
(k, None) => {
eprintln!("[warning] key={k} has value None");
None
}
}
}
fn expand(value: &str, subs: &HashMap<String, String>) -> Result<String> {
let mut expanded_value = value.to_string();
let value_ilen = i64::try_from(value.len())?;
let sub_ranges: Vec<_> = value.match_indices('%').map(|(i, _)| i).collect();
if !sub_ranges.len().is_multiple_of(2) {
bail!(
"unclosed delimiter in value={value}: the number of found \
percentage (%) delimiters (len()={}) aren't a multiple of 2",
sub_ranges.len()
);
}
for &[start, end] in sub_ranges.as_chunks::<2>().0 {
let sub_key = value[start..=end].to_string();
let sub_value = subs
.get(&sub_key)
.map(String::as_str)
.or_else(|| if sub_key == "%%" { Some("%") } else { None })
.or_else(|| {
if sub_key.chars().all(|c| c.is_ascii_digit() || c == '%') {
Some(&sub_key)
} else {
None
}
})
.ok_or_else(|| {
anyhow!("no substitution exists for sub_key={sub_key} for value={value}")
})?;
let offset = i64::try_from(expanded_value.len())? - value_ilen;
let (istart, iend) = (i64::try_from(start)?, i64::try_from(end)?);
let (start, end) = (
usize::try_from(istart + offset)?,
usize::try_from(iend + offset)?,
);
expanded_value.replace_range(start..=end, sub_value);
}
Ok(expanded_value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::from_root;
#[test]
fn good_inf() {
macro_rules! make_mappings {
($root:expr; $($variant:ident => $filename_suffix:literal),+ $(,)?) => {[
$(
CursorMapping {
r#type: crate::themes::theme::CursorType::$variant,
path: $root.join(concat!("Neuro ", $filename_suffix, ".ani")),
},
)+
]}
}
let theme_dir = Path::new(from_root!("/testing/fixtures/neuro"));
let inf_path = theme_dir.join("Install.inf");
let (theme_name, mappings) = parse_inf_installer(&inf_path, theme_dir).unwrap();
assert_eq!(theme_name, "Neuro-sama Cursor");
let expected_mappings = make_mappings!(
theme_dir; Arrow => "normal",
Help => "help", LeftPtrWatch => "work",
Watch => "busy", Crosshair => "precision",
Text => "text", Pencil => "hand",
Forbidden => "unavailable", NsResize => "vert",
EwResize => "horz", NwseResize => "dgn1",
NeswResize => "dgn2", Move => "move",
CenterPtr => "alt", Hand => "link",
);
assert_eq!(mappings, expected_mappings);
}
}