use super::symlinks::get_symlinks;
use crate::{
cursors::generic_cursor::GenericCursor,
formats::{crs::parse_crs_installer, inf::parse_inf_installer},
fs_utils::{find_extensions_icase, find_icase},
};
use std::{
fs::{self, File},
io::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow, bail};
use fast_image_resize::ResizeAlg;
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
#[derive(Debug, PartialEq)]
pub struct CursorMapping {
pub r#type: CursorType,
pub path: PathBuf,
}
#[derive(Debug, PartialEq, Clone)]
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;
}
#[derive(Debug)]
pub struct TypedCursor {
inner: GenericCursor,
r#type: CursorType,
aliases: &'static [&'static str],
}
impl TypedCursor {
fn new(xcursor: GenericCursor, r#type: CursorType) -> Self {
let aliases = get_symlinks(&r#type);
Self {
inner: xcursor,
r#type,
aliases,
}
}
fn from_mapping(mapping: CursorMapping) -> Result<Self> {
let path = mapping.path;
let path = if path.exists() {
path
} else {
find_icase(&path)?.ok_or_else(|| {
anyhow!(
"cursor path, path={} not found in parent (case-insensitive)",
path.display()
)
})?
};
Ok(Self::new(
GenericCursor::from_path(&path).with_context(|| {
format!("while reading path={} as generic cursor", path.display())
})?,
mapping.r#type,
))
}
fn save_as_xcursor(&self, dir: &Path) -> Result<()> {
self.inner.save_as_xcursor(dir.join(self.aliases[0]))?;
#[cfg(unix)]
for symlink in &self.aliases[1..] {
use std::{io, os::unix};
match unix::fs::symlink(self.aliases[0], dir.join(symlink)) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
Err(e) => Err(e).with_context(|| {
format!(
"failed to create symlink {} pointing to {}",
dir.join(symlink).display(),
self.aliases[0]
)
}),
}?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct CursorTheme {
cursors: Vec<TypedCursor>,
name: String,
}
impl CursorTheme {
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_theme_dir<P: AsRef<Path>>(theme_dir: P) -> Result<Self> {
let theme_dir = theme_dir.as_ref();
let installers: Vec<_> = find_extensions_icase(theme_dir, &["inf", "crs"])?.collect();
if installers.len() > 1 {
bail!("found more than one installer (INF/CRS) file");
}
let Some(installer) = installers.first().cloned() else {
bail!("no installer (INF/CRS) file found");
};
let (name, mappings) = if installer
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("inf"))
{
parse_inf_installer(&installer, theme_dir)
.with_context(|| format!("while attempting to parse {}", installer.display()))?
} else {
(String::new(), parse_crs_installer(&installer, theme_dir)?)
};
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 theme_dir = dir.join(&self.name);
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)]
{
eprintln!(
"[warning] 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| c.aliases) {
let src = filenames[0];
let symlinks = &filenames[1..];
for dst in symlinks {
writeln!(&mut f, "ln -s {src} {dst}")?;
}
}
Ok(())
}
}