use crate::DIRECTORY_CLASS;
use crate::encoding::{read_to_string_system, write_string_system};
use crate::error::{Error, IoReason, Result};
use owo_colors::OwoColorize;
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub fn valid_icon_resource(s: &str) -> bool {
match s.rsplit_once(",") {
Some((exe, pos)) if pos.parse::<u32>().is_ok() => Path::new(exe).is_file(),
None => Path::new(s).is_file(),
_ => false,
}
}
pub struct Ini {
dictionary: HashMap<String, HashMap<String, String>>,
}
macro_rules! accessor {
($set_fn:ident, $section:expr, $key:expr, $in_type:ty) => {
pub fn $set_fn(&mut self, value: $in_type) -> Option<String> {
if value.trim().is_empty() {
self.remove($section, $key)
} else {
self.set($section, $key, value)
}
}
};
($get_fn:ident, $set_fn:ident, $section:expr, $key:expr, $in_type:ty) => {
pub fn $get_fn(&self) -> Result<$in_type> {
self.get_into($section, $key)
}
accessor!($set_fn, $section, $key, $in_type);
};
($set_fn:ident, $section:expr, $key:expr) => {
accessor!($set_fn, $section, $key, String);
};
($get_fn:ident, $set_fn:ident, $section:expr, $key:expr) => {
pub fn $get_fn(&self) -> Option<String> {
self.get($section, $key)
}
accessor!($set_fn, $section, $key);
};
}
impl Ini {
pub fn new() -> Ini {
Ini {
dictionary: HashMap::new(),
}
}
pub fn get(&self, section: &str, key: &str) -> Option<String> {
self.dictionary
.get(section)
.and_then(|map| map.get(key))
.cloned()
}
pub fn get_into<T: FromStr<Err = Error>>(&self, section: &str, key: &str) -> Result<T> {
self.dictionary
.get(section)
.and_then(|map| map.get(key))
.ok_or(Error::NoValue)
.and_then(|s| T::from_str(s))
}
pub fn get_mut(&mut self, section: &str, key: &str) -> Option<&mut String> {
self.dictionary
.get_mut(section)
.and_then(|map| map.get_mut(key))
}
pub fn set<T: Display>(&mut self, section: &str, key: &str, value: T) -> Option<String> {
if let Some(map) = self.dictionary.get_mut(section) {
map.insert(key.to_string(), value.to_string())
} else {
let mut new = HashMap::new();
new.insert(key.to_string(), value.to_string());
self.dictionary.insert(section.to_string(), new);
None
}
}
pub fn remove(&mut self, section: &str, key: &str) -> Option<String> {
self.dictionary
.get_mut(section)
.and_then(|map| map.remove(key))
}
pub fn read_from(path: &mut PathBuf) -> Result<Ini> {
let mut ini = Ini::new();
if path.is_dir() {
path.push("desktop.ini")
};
if !fs::exists(&path).reason(|| "file system error on", Some(path))? {
return Ok(ini);
}
let string = read_to_string_system(path)?;
let mut section = "";
for line in string.lines() {
let line = line.trim();
if line.starts_with("#") || line.is_empty() {
continue;
} else if line.starts_with("[") {
section = line;
} else if let Some((tag, value)) = line.split_once("=") {
ini.set(section, tag, value);
}
}
Ok(ini)
}
pub fn write_to(&self, path: &mut PathBuf) -> Result<()> {
if path.is_dir() {
path.push("desktop.ini")
};
write_string_system(path, &self.to_string())
}
accessor!(info_tip, set_info_tip, "[.ShellClassInfo]", "InfoTip");
accessor!(execution, set_execution, "[.CustomExecution]", "Target");
accessor!(
icon_resource,
set_icon_resource,
"[.ShellClassInfo]",
"IconResource"
);
accessor!(
localized_resource_name,
set_localized_resource_name,
"[.ShellClassInfo]",
"LocalizedResourceName"
);
pub fn confirm_execution(&self) -> Option<bool> {
self.get("[.CustomExecution]", "ConfirmExecution")
.map(|s| s != "0")
}
pub fn set_confirm_execution(&mut self, value: bool) -> Option<String> {
self.set(
"[.CustomExecution]",
"ConfirmExecution",
if value { "1" } else { "0" },
)
}
pub fn set_directory_class(&mut self) -> Option<String> {
self.set("[.ShellClassInfo]", "DirectoryClass", DIRECTORY_CLASS)
}
pub fn tags(&self) -> Option<Vec<String>> {
match self.get("[{F29F85E0-4FF9-1068-AB91-08002B27B3D9}]", "Prop5") {
Some(value) => match value.split_once(",") {
Some((prefix, tags)) if prefix.trim() == "31" => {
Some(tags.split(';').map(|s| s.trim().to_string()).collect())
}
_ => None,
},
None => None,
}
}
pub fn set_tags(&mut self, tags: &[&str]) {
self.set(
"[{F29F85E0-4FF9-1068-AB91-08002B27B3D9}]",
"Prop5",
format!("31,{}", tags.join(";")),
);
}
pub fn add_tags(&mut self, tags: &[String]) {
let mut new_tags = self.tags().unwrap_or_default();
new_tags.extend_from_slice(tags);
self.set(
"[{F29F85E0-4FF9-1068-AB91-08002B27B3D9}]",
"Prop5",
format!("31,{}", new_tags.join(";")),
);
}
pub fn remove_tags(&mut self, tags: &[String]) {
let new_tags: Vec<String> = self
.tags()
.unwrap_or_default()
.into_iter()
.filter(|s| !tags.contains(s))
.collect();
self.set(
"[{F29F85E0-4FF9-1068-AB91-08002B27B3D9}]",
"Prop5",
format!("31,{}", new_tags.join(";")),
);
}
fn ordered(&self) -> Vec<&String> {
let preferred_sections = [
"[.ShellClassInfo]",
"[{F29F85E0-4FF9-1068-AB91-08002B27B3D9}]",
];
let mut sections: Vec<&String> = self.dictionary.keys().collect();
sections.sort();
let mut ordered_sections: Vec<&String> = Vec::with_capacity(sections.len());
for name in preferred_sections {
if let Some(s) = sections.extract_if(.., |s| s == &name).next() {
ordered_sections.push(s)
}
}
ordered_sections.extend(sections);
ordered_sections
}
}
impl Display for Ini {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for section in self.ordered() {
if let Some(map) = self.dictionary.get(section)
&& !map.is_empty()
{
writeln!(f, "\n{section}")?;
for (key, value) in map {
writeln!(f, "{key}={value}")?;
}
}
}
Ok(())
}
}
impl Debug for Ini {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let dot = "ยท ".dimmed().to_string();
writeln!(
f,
"{}\n\n{}",
"INI brief info".bold().cyan(),
"Shell Class Info:".bright_magenta()
)?;
if let Some(s) = self.localized_resource_name() {
writeln!(
f,
" {} {} {s}",
"LocalizedResourceName:".cyan(),
dot.repeat(5)
)?;
}
if let Some(s) = self.info_tip() {
writeln!(
f,
" {} {} {s}",
"InfoTip:".cyan(),
dot.repeat(12)
)?;
}
if let Some(s) = self.icon_resource() {
writeln!(
f,
" {} {} {s}",
"IconResource:".cyan(),
dot.repeat(10)
)?;
}
if let Some(tags) = self.tags() {
let sep = ", ".bright_yellow().bold().to_string();
writeln!(
f,
"{}\n {}",
"Tags:".bright_magenta(),
tags.join(&sep)
)?;
}
if let Some(execution) = self.execution() {
writeln!(
f,
"{}\n {} {} {execution}",
"Execution:".bright_magenta(),
"Target:".cyan(),
dot.repeat(13)
)?;
let confirm_str = if self.confirm_execution().unwrap_or(false) {
"on".green().to_string()
} else {
"off".yellow().to_string()
};
writeln!(
f,
" {} {} {confirm_str}",
"Second Confirmation:".cyan(),
dot.repeat(6),
)?;
}
writeln!(f, "\n{}", "Raw INI file content:".bold().cyan())?;
Display::fmt(&self, f)?;
Ok(())
}
}