Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
trayicon = "0.2.0"
winreg = "0.55.0"
flate2 = "1.0"

[dependencies.windows]
version = "0.61"
Expand All @@ -19,8 +20,16 @@ features = [
"Win32_System_SystemInformation"
]

[profile.release]
opt-level = "z" # Optimize for size
strip = true # Strip debug symbols
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce code generation units
panic = "abort" # Reduce panic code size

[build-dependencies]
winres = "0.1"
flate2 = "1.0"

[package.metadata.winres]
OriginalFilename = "rust_cat.exe"
Expand Down
Binary file added assets/appIcon-orginal.ico
Binary file not shown.
Binary file modified assets/appIcon.ico
Binary file not shown.
172 changes: 134 additions & 38 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{io, path::Path};
use std::{io, path::Path, collections::HashMap};
use winres::WindowsResource;
use flate2::{write::GzEncoder, Compression};

fn main() -> io::Result<()> {
// Get Git commit hash
Expand Down Expand Up @@ -36,45 +37,140 @@ fn main() -> io::Result<()> {

fn generate_icon_resources() -> io::Result<()> {
let out_dir = std::env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("icons.rs");
let themes = ["light", "dark"];
let names = [("cat", 5), ("parrot", 10usize)];
let mut code = vec![];
for theme in themes.iter() {
for (name, count) in names.iter() {
code.push(generate_icon_resources_array(theme, name, *count));
let dest_path = Path::new(&out_dir).join("icon_data.rs");

// Define icon configurations that match the icon manager structure
let icon_configs = [
("cat", [("light", 5), ("dark", 5)]),
("parrot", [("light", 10), ("dark", 10)]),
];

// Concatenate ALL icons into one big chunk for maximum compression
let (compressed_data, icon_metadata) = generate_all_icons_compressed(&icon_configs)?;

// Generate code with single compressed chunk
let code = generate_single_chunk_module(&compressed_data, &icon_metadata);

std::fs::write(&dest_path, code.as_bytes())
}

// Icon metadata for the single compressed chunk
#[derive(Debug)]
struct IconGroupMetadata {
icon_name: String,
theme: String,
offset: usize,
sizes: Vec<usize>,
}

fn generate_all_icons_compressed(icon_configs: &[(&str, [(&str, usize); 2])]) -> io::Result<(String, Vec<IconGroupMetadata>)> {
let mut all_icons_data = Vec::new();
let mut metadata = Vec::new();

// Collect all icon data
for (icon_name, themes) in icon_configs.iter() {
for (theme, count) in themes.iter() {
let base = std::fs::canonicalize(Path::new("assets").join(icon_name))?;
let icon_file_names = (0..*count)
.map(|i| format!("{}_{}_{}", theme, icon_name, i))
.collect::<Vec<_>>();

let mut group_sizes = Vec::new();
let group_offset = all_icons_data.len();

// Read all icons for this group
for name in &icon_file_names {
let icon_path = base.join(format!("{}.ico", name));
let icon_data = std::fs::read(&icon_path)?;
group_sizes.push(icon_data.len());
all_icons_data.extend_from_slice(&icon_data);
}

metadata.push(IconGroupMetadata {
icon_name: icon_name.to_string(),
theme: theme.to_string(),
offset: group_offset,
sizes: group_sizes,
});
}
}
std::fs::write(&dest_path, code.join("\n").as_bytes())

// Compress all icons together
let mut gz_encoder = GzEncoder::new(Vec::new(), Compression::best());
std::io::copy(&mut &all_icons_data[..], &mut gz_encoder)?;
let compressed = gz_encoder.finish()?;

// Write compressed data to OUT_DIR
let out_dir = std::env::var_os("OUT_DIR").unwrap();
let compressed_path = Path::new(&out_dir).join("all_icons.gz");
std::fs::write(&compressed_path, &compressed)?;

println!("cargo:info=All icons compressed: {} bytes -> {} bytes ({:.1}% reduction)",
all_icons_data.len(), compressed.len(),
100.0 * (1.0 - compressed.len() as f64 / all_icons_data.len() as f64));

Ok((compressed_path.display().to_string(), metadata))
}

fn generate_icon_resources_array(theme: &str, name: &str, cnt: usize) -> String {
let base = std::fs::canonicalize(Path::new("assets").join(name)).unwrap();
let names = (0..cnt)
.map(|i| format!("{}_{}_{}", theme, name, i))
.collect::<Vec<_>>();
let res = names
.iter()
.map(|name| {
format!(
r#"pub const {name}: &[u8] = include_bytes!(r"{fname}.ico");"#,
fname = base.join(name).display(),
name = name.to_uppercase(),
)
})
.collect::<Vec<_>>()
.join("\n");

format!(
r#"
{res}
pub const {theme}_{name}: &[&[u8]] = &[
{names}
];
"#,
res = res,
theme = theme.to_uppercase(),
name = name.to_uppercase(),
names = names.join(",").to_uppercase(),
)
fn generate_single_chunk_module(compressed_path: &str, metadata: &[IconGroupMetadata]) -> String {
let mut code = String::new();

code.push_str("// All icons compressed into a single chunk for maximum compression\n");
code.push_str(&format!("pub const ALL_ICONS_COMPRESSED: &[u8] = include_bytes!(r\"{}\");\n\n", compressed_path));

code.push_str("use std::collections::HashMap;\n\n");

// IconGroupInfo is now defined in main.rs

// Generate size arrays for each group
for meta in metadata {
let sizes_array = meta.sizes
.iter()
.map(|size| size.to_string())
.collect::<Vec<_>>()
.join(", ");

code.push_str(&format!(
"const {}_{}_{}_SIZES: &[u32] = &[{}];\n",
meta.theme.to_uppercase(),
meta.icon_name.to_uppercase(),
"INDIVIDUAL",
sizes_array
));
}

code.push('\n');

// Generate function to get icon metadata
code.push_str("pub fn get_icon_metadata() -> IconData {\n");
code.push_str(" let mut icons = HashMap::new();\n\n");

// Group metadata by icon name
let mut grouped_metadata: HashMap<String, Vec<&IconGroupMetadata>> = HashMap::new();
for meta in metadata {
grouped_metadata.entry(meta.icon_name.clone()).or_default().push(meta);
}

for (icon_name, themes) in grouped_metadata {
code.push_str(&format!(" // {} icons\n", icon_name));
code.push_str(&format!(" let mut {} = HashMap::new();\n", icon_name));

for meta in themes {
code.push_str(&format!(
" {}.insert(\"{}\", IconGroupInfo {{ offset: {}, sizes: {}_{}_INDIVIDUAL_SIZES }});\n",
icon_name,
meta.theme,
meta.offset,
meta.theme.to_uppercase(),
meta.icon_name.to_uppercase()
));
}

code.push_str(&format!(" icons.insert(\"{}\", {});\n\n", icon_name, icon_name));
}

code.push_str(" icons\n");
code.push_str("}\n");

code
}
80 changes: 53 additions & 27 deletions src/icon_manager.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
use trayicon::Icon;
use std::collections::HashMap;
use flate2::read::GzDecoder;
use std::io::Read;

#[allow(dead_code)]
mod icon_data {
pub struct IconGroupInfo {
pub offset: usize,
pub sizes: &'static [u32],
}
pub type IconData = HashMap<&'static str, HashMap<&'static str, IconGroupInfo>>;
include!(concat!(env!("OUT_DIR"), "/icon_data.rs"));
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Theme {
Expand Down Expand Up @@ -44,39 +56,53 @@ impl IconManager {
pub fn load_icons() -> Result<Self, Box<dyn std::error::Error>> {
let mut manager = Self::new();

// Define icon configurations: (base_name, supports_themes, [(theme, icon_data)])
// Adding new icons is as simple as adding a new entry here!
let icon_configs = [
("cat", true, vec![
(Theme::Dark, crate::icon_data::DARK_CAT),
(Theme::Light, crate::icon_data::LIGHT_CAT),
]),
("parrot", true, vec![
(Theme::Dark, crate::icon_data::DARK_PARROT),
(Theme::Light, crate::icon_data::LIGHT_PARROT),
]),
// Example: to add a new "dog" icon, just add:
// ("dog", true, vec![
// (Theme::Dark, crate::icon_data::DARK_DOG),
// (Theme::Light, crate::icon_data::LIGHT_DOG),
// ]),
// The menu system will automatically detect it and add it to the menu!
];
// Decompress the single big chunk containing all icons
let mut decoder = GzDecoder::new(icon_data::ALL_ICONS_COMPRESSED);
let mut all_decompressed = Vec::new();
decoder.read_to_end(&mut all_decompressed)
.map_err(|e| format!("Failed to decompress all icons: {}", e))?;

// Leak the decompressed data to keep it alive for the lifetime of the program
let all_decompressed = Box::leak(all_decompressed.into_boxed_slice());

for (base_name, supports_themes, theme_data) in icon_configs.iter() {
manager.theme_support.insert(base_name.to_string(), *supports_themes);
// Get icon metadata from build script generated module
let icon_metadata_map = icon_data::get_icon_metadata();

for (icon_name, theme_data) in icon_metadata_map {
// All current icons support themes
manager.theme_support.insert(icon_name.to_string(), true);
let mut themes_map = HashMap::new();

for (theme, data) in theme_data.iter() {
let icons: Result<Vec<Icon>, _> = data
.iter()
.map(|icon_bytes| Icon::from_buffer(icon_bytes, None, None))
.collect();
for (theme_str, group_info) in theme_data {
let theme = match theme_str {
"dark" => Theme::Dark,
"light" => Theme::Light,
_ => continue, // Skip unknown themes
};

// Extract this group's data from the big decompressed chunk
let mut icons = Vec::new();
let mut current_offset = group_info.offset;

for &size in group_info.sizes {
let size = size as usize;
if current_offset + size > all_decompressed.len() {
return Err(format!("Invalid icon offset/size data for {} {}", icon_name, theme_str).into());
}

let icon_data = &all_decompressed[current_offset..current_offset + size];

let icon = Icon::from_buffer(icon_data, None, None)
.map_err(|e| format!("Failed to create icon from buffer for {} {}: {}", icon_name, theme_str, e))?;

icons.push(icon);
current_offset += size;
}

themes_map.insert(*theme, icons?);
themes_map.insert(theme, icons);
}

manager.icon_sets.insert(base_name.to_string(), themes_map);
manager.icon_sets.insert(icon_name.to_string(), themes_map);
}

Ok(manager)
Expand Down
Loading
Loading