1use std::collections::{BTreeMap, HashMap};
4use std::env;
5use std::fs;
6use std::fs::File;
7use std::io::BufWriter;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11use gvdb::gresource::{BundleBuilder, FileData, PreprocessOptions};
12use serde::Deserialize;
13use serde::Serialize;
14use walkdir::WalkDir;
15
16struct IconData {
18 path: PathBuf,
20 is_shipped: bool,
22}
23
24#[derive(Serialize, Deserialize)]
25struct Config {}
26
27pub mod constants {
29 pub const SHIPPED_ICONS_PATH: &str =
30 include_str!(concat!(env!("OUT_DIR"), "/shipped_icons.txt"));
31}
32
33const GENERAL_PREFIX: &str = "/org/relm4/icons";
34
35pub fn path_to_icon_alias(path: impl AsRef<Path>) -> Option<String> {
38 match path.as_ref().to_str() {
39 Some(path) => {
40 if path.ends_with(".svg") {
41 println!("{path}");
42 Some(path.trim_end_matches(".svg").to_owned())
43 } else {
44 println!("Found non-icon file `{path}`, ignoring");
45 None
46 }
47 }
48 None => panic!(
49 "Failed to convert file path `{:?}` to string",
50 path.as_ref()
51 ),
52 }
53}
54
55pub fn bundle_icons<P, I, S>(
60 out_file_name: &str,
61 app_id: Option<&str>,
62 base_resource_path: Option<&str>,
63 icons_folder: Option<P>,
64 icon_names: I,
65) where
66 P: AsRef<Path>,
67 I: IntoIterator<Item = S>,
68 S: AsRef<str>,
69{
70 let out_dir = env::var("OUT_DIR").unwrap();
71 let out_dir = Path::new(&out_dir);
72 let mut icons: HashMap<String, IconData> = HashMap::new();
73
74 if let Some(folder) = &icons_folder {
76 println!("cargo:rerun-if-changed={}", folder.as_ref().display());
77
78 let read_dir = WalkDir::new(folder);
79 for entry in read_dir {
80 let entry = entry
81 .expect("Couldn't open icon path specified in config (relative to the manifest)");
82 if let Some(icon) = path_to_icon_alias(entry.path()) {
83 if icons
84 .insert(
85 icon.replace('/', "-").replace('\\', "-").clone(),
86 IconData {
87 path: entry.path().to_path_buf(),
88 is_shipped: false,
89 },
90 )
91 .is_some()
92 {
93 panic!("Icon with name `{icon}` exists twice");
94 }
95 }
96 }
97 }
98
99 let shipped_icons_folder = constants::SHIPPED_ICONS_PATH;
100
101 let dirs = fs::read_dir(shipped_icons_folder)
102 .expect("Couldn't open folder of shipped icons")
103 .map(|entry| {
104 entry
105 .expect("Couldn't open directories in shipped icon folder")
106 .path()
107 })
108 .collect::<Vec<_>>();
109
110 for icon in icon_names {
111 let icon = icon.as_ref();
112 let icon_path = dirs
113 .iter()
114 .find_map(|dir| {
115 let icon_file_name = format!("{icon}-symbolic.svg");
116 let icon_path = dir.join(icon_file_name);
117 icon_path.exists().then_some(icon_path)
118 })
119 .unwrap_or_else(|| panic!("Icon with name `{icon}` does not exist"));
120
121 if icons
122 .insert(
123 icon.to_string(),
124 IconData {
125 path: icon_path,
126 is_shipped: true,
127 },
128 )
129 .is_some()
130 {
131 panic!("Icon with name `{icon}` exists twice");
132 }
133 }
134
135 let prefix = if let Some(base_resource_path) = &base_resource_path {
136 format!("{base_resource_path}/icons")
137 } else if let Some(app_id) = app_id {
138 format!("/{}/icons", app_id.replace('.', "/"))
139 } else {
140 GENERAL_PREFIX.into()
141 };
142 let gresource_file_name = format!("{out_file_name}.gresource");
143
144 {
146 let resources = icons
147 .iter()
148 .map(|(icon, IconData { path, is_shipped })| {
149 FileData::from_file(
150 if *is_shipped {
151 format!("{prefix}/scalable/actions/{icon}-symbolic.svg")
152 } else {
153 format!("{prefix}/scalable/actions/{icon}.svg")
154 },
155 path,
156 true,
157 &PreprocessOptions::xml_stripblanks(),
158 )
159 .unwrap()
160 })
161 .collect();
162
163 let data = BundleBuilder::from_file_data(resources)
164 .build()
165 .expect("Failed to build resource bundle");
166
167 fs::write(out_dir.join(&gresource_file_name), data).unwrap();
168 }
169
170 {
172 let mut out_file = BufWriter::new(File::create(out_dir.join(out_file_name)).unwrap());
173
174 writeln!(out_file, "#[rustfmt::skip]").unwrap();
175 writeln!(
176 out_file,
177 "pub mod shipped {{\n\
178 //! module contains shipped icons\n"
179 )
180 .unwrap();
181 for (icon, IconData { path, is_shipped }) in &icons {
182 if *is_shipped {
183 let const_name = icon.to_uppercase().replace('-', "_");
184 let path = path.display();
185 writeln!(
186 out_file,
187 "/// Icon name of the icon `{icon}`, found at `{path}`\n\
188 pub const {const_name}: &str = \"{icon}\";"
189 )
190 .unwrap();
191 }
192 }
193 writeln!(out_file, "}}\n").unwrap();
194
195 writeln!(
196 out_file,
197 "pub mod custom {{\n\
198 //! module contains user's custom icons\n"
199 )
200 .unwrap();
201 let mut modules = BTreeMap::<Vec<&str>, Vec<(String, String)>>::new();
202 for (icon, IconData { path, is_shipped }) in &icons {
203 if !*is_shipped {
204 let mut path_vec = path
205 .strip_prefix(icons_folder.as_ref().unwrap())
206 .unwrap()
207 .to_str()
208 .unwrap()
209 .split(&['/', '\\'])
210 .collect::<Vec<_>>();
211
212 let file_name = path_vec.pop().unwrap().trim_end_matches(".svg");
213 let dir_components = path_vec;
214
215 let const_name = file_name.to_uppercase().replace('-', "_");
216 modules
217 .entry(dir_components)
218 .or_default()
219 .push((const_name, icon.to_string()));
220 }
221 }
222 for (module_path, constants) in &modules {
223 if module_path.is_empty() {
224 for (const_name, const_value) in constants {
225 writeln!(
226 out_file,
227 "pub const {const_name}: &str = \"{const_value}\";"
228 )
229 .unwrap();
230 }
231 } else {
232 for part in module_path.iter() {
233 writeln!(out_file, "pub mod {} {{", part.replace('-', "_")).unwrap();
234 }
235 for (const_name, const_value) in constants {
236 writeln!(
237 out_file,
238 "pub const {const_name}: &str = \"{const_value}\";"
239 )
240 .unwrap();
241 }
242 for _ in 0..module_path.len() {
243 writeln!(out_file, "}}").unwrap();
244 }
245 }
246 }
247 writeln!(out_file, "}}").unwrap();
248 write!(
249 out_file,
250 "/// `GResource` file contents\n\
251 pub const GRESOURCE_BYTES: &[u8] = include_bytes!(\"{gresource_file_name}\");\n\
252 /// Resource prefix used in generated `.gresource` file\n\
253 pub const RESOURCE_PREFIX: &str = \"{prefix}\";"
254 )
255 .unwrap();
256 }
257}