use super::symlinks::get_symlinks;
use crate::{
cursors::generic_cursor::GenericCursor,
formats::{crs::parse_crs_installer, inf::parse_inf_installer},
fs_utils::resolve_icase,
warn,
};
use std::{
fs::{self, File},
io::{self, Write},
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow, bail};
use documented::DocumentedVariants;
use fast_image_resize::ResizeAlg;
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
#[derive(Debug, PartialEq, Eq)]
pub struct CursorMapping {
pub r#type: CursorType,
pub path: PathBuf,
}
#[derive(Debug, PartialEq, Eq, Clone, DocumentedVariants)]
pub enum CursorType {
Arrow,
Hand,
Watch,
LeftPtrWatch,
Help,
Text,
Pencil,
Crosshair,
Forbidden,
NsResize,
EwResize,
NwseResize,
NeswResize,
Move,
CenterPtr,
}
impl CursorType {
pub const NUM_VARIANTS: usize = 15;
pub const VARIANTS: [Self; Self::NUM_VARIANTS] = [
Self::Arrow,
Self::Hand,
Self::Watch,
Self::LeftPtrWatch,
Self::Help,
Self::Text,
Self::Pencil,
Self::Crosshair,
Self::Forbidden,
Self::NsResize,
Self::EwResize,
Self::NwseResize,
Self::NeswResize,
Self::Move,
Self::CenterPtr,
];
}
#[derive(Debug)]
pub struct TypedCursor {
inner: GenericCursor,
r#type: CursorType,
}
impl TypedCursor {
pub fn from_mapping(mapping: CursorMapping) -> Result<Self> {
let CursorMapping { path, r#type } = mapping;
let path = if path.exists() {
path
} else {
resolve_icase(&path)?.ok_or_else(|| {
anyhow!(
"cursor path, path={} not found in parent (case-insensitive)",
path.display()
)
})?
};
let inner = GenericCursor::from_path(&path)
.with_context(|| format!("while reading path={} as generic cursor", path.display()))?;
Ok(Self { inner, r#type })
}
fn save_as_xcursor(&self, dir: &Path) -> Result<()> {
let aliases = get_symlinks(&self.r#type);
let primary = dir.join(aliases[0]);
match fs::symlink_metadata(&primary) {
Ok(metadata) if metadata.is_symlink() => fs::remove_file(&primary)?,
Ok(_) => (),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
self.inner.save_as_xcursor(primary)?;
#[cfg(unix)]
for symlink in &aliases[1..] {
use std::os::unix;
let path = dir.join(symlink);
match unix::fs::symlink(aliases[0], &path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
fs::remove_file(&path)?;
unix::fs::symlink(aliases[0], &path)
}
Err(e) => Err(e),
}
.with_context(|| {
format!(
"failed to create symlink {} pointing to {}",
path.display(),
aliases[0]
)
})?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct CursorTheme {
cursors: Vec<TypedCursor>,
name: String,
}
impl CursorTheme {
pub fn new(cursors: Vec<TypedCursor>, name: String) -> Result<Self> {
if cursors.is_empty() {
bail!("can't create theme with no cursors (empty)");
}
if cursors.len() > CursorType::NUM_VARIANTS {
bail!(
"too many cursors; expected {} max for theme, got {}",
CursorType::NUM_VARIANTS,
cursors.len(),
);
}
let mut seen = Vec::new();
for cursor in &cursors {
if seen.contains(&cursor.r#type) {
bail!("duplicate cursor type: {:?}", cursor.r#type);
}
seen.push(cursor.r#type.clone());
}
Ok(Self { cursors, name })
}
pub fn from_installer_file(installer_file: impl AsRef<Path>) -> Result<Self> {
let installer_file = installer_file.as_ref();
let ext = installer_file.extension().ok_or_else(|| {
anyhow!(
"no extension for installer_file={}",
installer_file.display()
)
})?;
let (name, mappings) = if ext.eq_ignore_ascii_case("inf") {
parse_inf_installer(installer_file)?
} else if ext.eq_ignore_ascii_case("crs") {
(String::new(), parse_crs_installer(installer_file)?)
} else {
bail!("unsupported installer file extension ext={}", ext.display())
};
let typed_cursors: Vec<_> = mappings
.into_iter()
.map(TypedCursor::from_mapping)
.collect::<Result<_>>()?;
Self::new(typed_cursors, name)
}
pub fn add_scale(&mut self, scale_factor: f64, algorithm: ResizeAlg) -> Result<()> {
self.cursors
.par_iter_mut()
.try_for_each(|c| c.inner.add_scale(scale_factor, algorithm))?;
Ok(())
}
pub fn save_as_x11_theme(&self, dir: &Path) -> Result<()> {
let name = if self.name.is_empty() {
warn!("blank theme name, using a placeholder name");
String::from("placeholder_theme_name")
} else {
self.name.clone()
};
let sanitized = name.replace(['/', '\\', '.'], "_");
let theme_dir = dir.join(sanitized);
let cursor_dir = theme_dir.join("cursors");
fs::create_dir_all(&cursor_dir)
.with_context(|| format!("failed to write cursor_dir={}", cursor_dir.display()))?;
#[cfg(windows)]
{
warn!(
"symlinks won't be created as we're on windows, a \
bash script for usage on linux will be created instead"
);
self.write_symlink_script(&cursor_dir)?;
}
self.cursors
.par_iter()
.try_for_each(|c| c.save_as_xcursor(&cursor_dir))?;
let mut f = File::create(theme_dir.join("index.theme"))?;
writeln!(
&mut f,
"# https://specifications.freedesktop.org/icon-theme/latest/#id-1.5.3.2"
)?;
writeln!(&mut f, "[Icon Theme]")?;
if self.name.is_empty() {
writeln!(&mut f, "# Name=theme_name")?;
} else {
writeln!(&mut f, "Name={}", self.name)?;
}
writeln!(
&mut f,
"Comment=made with currust; edit index.theme to change this"
)?;
writeln!(&mut f, "# Inherits=fallback_theme")?;
Ok(())
}
#[cfg(windows)]
fn write_symlink_script(&self, cursor_dir: &Path) -> Result<()> {
let dir_display = cursor_dir.display();
if !cursor_dir.exists() {
bail!("dir={dir_display} doesn't exist");
}
let mut f = File::create(cursor_dir.join("write_symlinks.sh"))?;
writeln!(&mut f, "#!/usr/bin/env bash\n")?;
for filenames in self.cursors.iter().map(|c| get_symlinks(&c.r#type)) {
let src = filenames[0];
let symlinks = &filenames[1..];
for dst in symlinks {
writeln!(&mut f, "ln -s {src} {dst}")?;
}
}
Ok(())
}
}