From 6e3b3c8013e6d8814dbf70c854e55d062bedbdf4 Mon Sep 17 00:00:00 2001 From: memdmp Date: Mon, 13 Jan 2025 16:54:34 +0100 Subject: chore: initial commit --- src/main.rs | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/xcursor.rs | 101 +++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 src/main.rs create mode 100644 src/xcursor.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fed8090 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,199 @@ +use std::{fs, io::Result, os::unix}; + +use serde::{Deserialize, Serialize}; +use tiny_skia::Pixmap; +use xcursor::{ImageInfo, XCursorEncoder}; + +mod xcursor; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +struct Xy { + x: u32, + y: u32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct BibataPlatformNames { + windows: Option, + xorg: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct BibataFrame { + idx: i32, + id: String, + url: String, + delay: u32, + sha512: String, + svg: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct BibataCursor { + animated: bool, + id: String, + name: String, + aliases: Vec, + hot: Xy, + frames: Vec, + frame_count: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BibataCursorStyle { + kind: String, + cursors: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BibataColourScheme<'a> { + pub base: &'a str, + pub outline: &'a str, + pub watch_background: &'a str, + pub watch_color: Vec<&'a str>, + /** Value between 0 and 1 - Bibata defaults to 0.8 */ + pub watch_opacity: f32, +} + +fn vector_to_pixmap(svg_data: &[u8]) -> Pixmap { + let tree = { + let mut opt = usvg::Options::default(); + // Get file's absolute directory. + // opt.resources_dir = std::fs::canonicalize(&args[1]) + // .ok() + // .and_then(|p| p.parent().map(|p| p.to_path_buf())); + + opt.fontdb_mut().load_system_fonts(); + + usvg::Tree::from_data(svg_data, &opt).unwrap() + }; + + let pixmap_size = tree.size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); + resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut()); + pixmap +} +fn resize_cursvg(svg: &str, new_size: &u16) -> String { + svg.to_string().replace( + "width=\"256\" height=\"256\"", + format!("width=\"{new_size}\" height=\"{new_size}\"").as_str(), + ) +} +fn apply_colours(unpatched_svg: String, colours: &BibataColourScheme) -> String { + if colours.watch_color.len() != 4 { + panic!("Must specify 4 watch colours!"); + }; + unpatched_svg + .replace("#00FF00", colours.base) + .replace("#0000FF", colours.outline) + .replace("#FF0000", colours.watch_background) + .replace("#F05024", colours.watch_color[0]) + .replace("#FCB813", colours.watch_color[1]) + .replace("#7EBA41", colours.watch_color[2]) + .replace("#32A0DA", colours.watch_color[3]) + .replace( + "fill-opacity=\"0.8\"", + format!("fill-opacity=\"{}\"", colours.watch_opacity).as_str(), + ) +} +fn process_xcursor( + cursor: BibataCursor, + desired_colours: BibataColourScheme, + desired_sizes: Vec, + time_scale: u32, +) -> Vec { + let mut image_info_vec: Vec = Vec::new(); + for cursor_frame in cursor.frames { + for desired_size in &desired_sizes { + let svg_data = apply_colours( + resize_cursvg(&cursor_frame.svg, desired_size), + &desired_colours, + ); + let pixmap = vector_to_pixmap(svg_data.as_bytes()); + + let pixels = { + let pixels = pixmap.pixels(); + let mut otherpixels: Vec = Vec::new(); + for elem in pixels.iter() { + otherpixels.push(elem.red()); + otherpixels.push(elem.green()); + otherpixels.push(elem.blue()); + otherpixels.push(elem.alpha()); + } + otherpixels + }; + let image_info = ImageInfo { + r#type: *desired_size as u32, + subtype: *desired_size as u32, + width: pixmap.width().try_into().unwrap(), + height: pixmap.height().try_into().unwrap(), + xhot: (cursor.hot.x * pixmap.width() / 256).try_into().unwrap(), + yhot: (cursor.hot.y * pixmap.height() / 256).try_into().unwrap(), + data: pixels.try_into().unwrap(), + delay: (time_scale / cursor_frame.delay).try_into().unwrap(), + }; + image_info_vec.push(image_info); + } + } + let mut encoder = XCursorEncoder::new(image_info_vec); + encoder.pack() +} + +fn main() -> Result<()> { + let cursor_style: Vec = + serde_json::from_str(include_str!("../.cursors.min")).unwrap(); + + // TODO: proper arg parsing, handling shit cutely, not hardcoding everything, etc + if fs::exists("xcursor-out")? { + fs::remove_dir_all("xcursor-out")?; + } + fs::create_dir("xcursor-out")?; + for style in cursor_style { + fs::create_dir(format!("xcursor-out/{}", style.kind))?; + for cursor in style.cursors { + println!("xcursor-out/{}/{}", style.kind, cursor.name.clone()); + // shove these files in a dir named `cursors` inside the theme dir + // the theme dir should have these 2 files in it: + // + // cursor.theme: + // [Icon Theme] + // Name=Bibata + // Inherits="Bibata" + // + // index.theme: + // [Icon Theme] + // Name=Bibata + // Comment=Generated Bibata Theme + // Inherits="hicolor" + // + // the theme dir should be ~/.local/share/icons/Bibata + // + // to apply on gnome: gsettings set org.gnome.desktop.interface cursor-theme Bibata + fs::write( + format!("xcursor-out/{}/{}", style.kind, cursor.name.clone()), + process_xcursor( + cursor.clone(), + BibataColourScheme { + base: "#ffffff", + outline: "#000000", + watch_background: "#ffffff", + watch_color: vec!["#ff5e9c", "#65c7e0", "#f69a59", "#6789f0"], + watch_opacity: 1.0, + }, + vec![ + 4, 6, 8, 10, 12, 16, 18, 24, 30, 32, 36, 42, 48, 54, 60, 66, 72, 128, + ], + 750, + ), + )?; + for alias in cursor.aliases { + unix::fs::symlink( + cursor.name.clone(), + format!("xcursor-out/{}/{}", style.kind, alias), + )?; + } + } + } + + Ok(()) +} diff --git a/src/xcursor.rs b/src/xcursor.rs new file mode 100644 index 0000000..ff6c3d6 --- /dev/null +++ b/src/xcursor.rs @@ -0,0 +1,101 @@ +#[derive(Debug, Clone)] +pub struct ImageInfo { + pub r#type: u32, + pub subtype: u32, + pub width: u32, + pub height: u32, + pub xhot: u32, + pub yhot: u32, + pub delay: u32, + pub data: Vec, +} + +pub struct XCursorEncoder { + pub images: Vec, +} + +const MAGIC: [u8; 4] = [0x58, 0x63, 0x75, 0x72]; +const IMAGE_HEADER: [u8; 4] = [0x02, 0x00, 0xfd, 0xff]; + +impl XCursorEncoder { + pub fn new(images: Vec) -> Self { + XCursorEncoder { images } + } + + fn image_pos(&self, position: usize) -> usize { + let mut val = 12 * self.images.len() + 16; + for n in 0..position { + val += 36 + ((self.images[n].width as usize) * (self.images[n].height as usize) * 4) + } + val + } + + pub fn pack(&mut self) -> Vec { + fn insert_bytes(data: &mut Vec, new_data: impl IntoIterator) { + for int in new_data { + data.push(int); + } + } + fn insert_int(data: &mut Vec, int: u32) { + data.push((int & 0xff) as u8); + data.push(((int >> 8) & 0xff) as u8); + data.push(((int >> 16) & 0xff) as u8); + data.push(((int >> 24) & 0xff) as u8); + } + + let mut data: Vec = Vec::new(); + + // File Header + { + // MAGIC string ("Xcur") + insert_bytes(&mut data, MAGIC); + // CARD32 bytes in this header + insert_int(&mut data, 16); + // CARD32 file version + insert_int(&mut data, 1); + // CARD32 number of toc entries + insert_int(&mut data, self.images.len().try_into().unwrap()); + } + + // ntoc entries + { + let mut img_idx: usize = 0; + for img in self.images.clone().into_iter() { + // Some header + insert_bytes(&mut data, IMAGE_HEADER); + // CARD32 type-specific label - size for images + insert_int(&mut data, img.r#type); + // CARD32 absolute byte position of table in file + insert_int(&mut data, self.image_pos(img_idx).try_into().unwrap()); + + img_idx += 1; + } + } + + // images + { + for img in self.images.clone().into_iter() { + // Header Size (36) + insert_int(&mut data, 36); + // Image Type + insert_bytes(&mut data, IMAGE_HEADER); + // Subtype, for nominal size + insert_int(&mut data, img.subtype); + // Version + insert_int(&mut data, 1); + // Image dimensions + insert_int(&mut data, img.width); + insert_int(&mut data, img.height); + // Cursor positioning + insert_int(&mut data, img.xhot); + insert_int(&mut data, img.yhot); + // Milliseconds till next frame + insert_int(&mut data, img.delay); + // Raw image data + insert_bytes(&mut data, img.data); + } + } + + data + } +} -- cgit v1.2.3