1use std::collections::HashMap;
2use std::fs::{self, create_dir_all, File};
3use std::io::{Result, Write};
4use std::path::PathBuf;
5use std::str;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use dirs::home_dir;
9
10#[cfg(unix)]
11use dirs::font_dir;
12
13#[cfg(target_os = "windows")]
15fn windows_user_folder_fonts() -> Option<PathBuf> {
16 return std::env::var_os("userprofile").and_then(|u| {
17 if u.is_empty() {
18 None
19 } else {
20 let f = PathBuf::from(u).join("AppData\\Local\\Microsoft\\Windows\\Fonts");
21 if !f.is_dir() {
22 None
23 } else {
24 Some(f)
25 }
26 }
27 });
28}
29#[cfg(target_os = "windows")]
30use self::windows_user_folder_fonts as font_dir;
31
32use font_kit::handle::Handle;
33use font_kit::source::SystemSource;
34
35use chrono::offset::Utc;
36use chrono::{DateTime, NaiveDate};
37
38use curl::easy::Easy;
39
40use serde::{Deserialize, Serialize};
41use serde_json;
42use toml;
43
44#[derive(Serialize, Deserialize, Clone)]
45pub struct FontsList {
46 pub kind: String,
47 pub items: Vec<RepoFont>,
48}
49
50#[derive(Serialize, Deserialize)]
51pub struct Repository {
52 pub name: String,
53 pub url: String,
54 pub key: Option<String>,
55}
56
57#[derive(Serialize, Deserialize)]
58pub struct Repositories {
59 pub repo: Vec<Repository>,
60}
61
62#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
63pub struct RepoFont {
64 kind: Option<String>,
65 family: Option<String>,
66 variants: Vec<String>,
67 subsets: Option<Vec<String>>,
68 version: Option<String>,
69 lastModified: Option<String>,
70 files: HashMap<String, String>,
71 commentary: Option<String>,
72 creator: Option<String>,
73}
74
75#[derive(Clone, Debug, PartialEq)]
76pub struct LocalFont {
77 family: Option<String>,
78 variants: Option<Vec<String>>,
79 files: Option<HashMap<String, PathBuf>>,
80 lastModified: Option<SystemTime>,
81 installed: Option<bool>
82}
83
84#[derive(Eq, PartialEq, Hash, Debug, Clone)]
85pub enum Location {
86 User,
87 System,
88 Memory,
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct Font {
93 family: String,
94 repo_font: HashMap<String, RepoFont>,
95 local_font: HashMap<Location, LocalFont>,
96}
97
98fn download(url: &str) -> Vec<u8> {
99 let mut handle = Easy::new();
100 let mut file: Vec<u8> = Vec::new();
101
102 handle.url(url).unwrap();
103 let _location = handle.follow_location(true);
104
105 {
106 let mut transfer = handle.transfer();
107 transfer
108 .write_function(|data| {
109 file.extend_from_slice(data);
110 Ok(data.len())
111 })
112 .unwrap();
113 transfer.perform().unwrap();
114 }
115 file
116}
117
118fn download_file(output_file: &PathBuf, url: &str) -> Result<()> {
119 create_dir_all(output_file.parent().unwrap())?;
120 println!(
121 "Downloading to {} from {}...",
122 output_file.as_os_str().to_str().unwrap(),
123 url
124 );
125 let mut file = File::create(output_file)?;
126 file.write_all(download(url).as_slice())?;
127 Ok(())
128}
129
130pub fn get_default_repos() -> Vec<Repository> {
131 vec![
132 #[cfg(feature = "google_repo")]
133 Repository {
134 name: "Google Fonts".to_string(),
135 url: "https://www.googleapis.com/webfonts/v1/webfonts?key={API_KEY}".to_string(),
136 key: {
137 const PASSWORD: &'static str = env!("GOOGLE_FONTS_KEY");
138 Some(PASSWORD.to_string())
139 },
140 },
141 Repository {
142 name: "Open Font Repository".to_string(),
143 url: "https://raw.githubusercontent.com/GustavoPeredo/open-font-repository/main/fonts.json".to_string(),
144 key: None,
145 }
146 ]
147}
148
149pub fn generate_repos_from_str(repos_as_str: &str) -> Result<Vec<Repository>> {
150 let repositories: Repositories = match toml::from_str(&repos_as_str) {
151 Ok(r) => r,
152 Err(e) => {
153 eprintln!("error: {:#}", e);
154 println!("Skipping reading from local repositories");
155 Repositories {
156 repo: Vec::<Repository>::new(),
157 }
158 }
159 };
160 Ok(repositories.repo)
161}
162
163pub fn generate_repos_from_file(repos_path: &PathBuf) -> Result<Vec<Repository>> {
164 Ok(generate_repos_from_str(&fs::read_to_string(repos_path)?)?)
165}
166
167pub fn generate_repo_font_list_from_str(font_list_as_str: &str) -> Result<Vec<RepoFont>> {
168 Ok(serde_json::from_str::<FontsList>(font_list_as_str)?.items)
169}
170
171pub fn generate_repo_font_list_from_file(repo_path: &PathBuf) -> Result<Vec<RepoFont>> {
172 Ok(generate_repo_font_list_from_str(&fs::read_to_string(
173 repo_path,
174 )?)?)
175}
176
177pub fn generate_repo_font_list_from_url(
178 repo_url: &str,
179 key: Option<String>,
180) -> Result<Vec<RepoFont>> {
181 let repo_url = match key {
182 Some(key) => repo_url.replace("{API_KEY}", &key),
183 None => repo_url.to_string(),
184 };
185 Ok(generate_repo_font_list_from_str(
186 match str::from_utf8(download(&repo_url).as_slice()) {
187 Ok(s) => s,
188 Err(_) => "",
189 },
190 )?)
191}
192
193pub fn init() -> Result<HashMap<String, Font>> {
194 let local_fonts = generate_local_fonts(None)?;
195 let default_repos = get_default_repos();
196 let repo_fonts: HashMap<String, Vec<RepoFont>> = default_repos
197 .iter()
198 .map(|repo| {
199 (
200 repo.name.clone(),
201 generate_repo_font_list_from_url(&repo.url, repo.key.clone()).unwrap(),
202 )
203 })
204 .collect::<HashMap<String, Vec<RepoFont>>>();
205 Ok(generate_fonts_list(repo_fonts, local_fonts))
206}
207
208pub fn generate_local_fonts(location: Option<Location>) -> Result<Vec<LocalFont>> {
209 let fonts = SystemSource::new().all_families().unwrap();
210
211 let results = fonts.iter().map(|font_family| {
212 LocalFont {
213 family: Some(font_family.to_string()),
214 variants: None,
215 files: None,
216 lastModified: None,
217 installed: None
218 }
219 }).collect::<Vec<LocalFont>>();
220 Ok(results)
221}
222
223pub fn generate_fonts_list(
224 repos_font_lists: HashMap<String, Vec<RepoFont>>,
225 local_fonts: Vec<LocalFont>,
226) -> HashMap<String, Font> {
227 let mut result: HashMap<String, Font> = HashMap::new();
228
229 for (repo_name, repo_fonts) in repos_font_lists.iter() {
230 for repo_font in repo_fonts {
231 let current_font = result.entry(repo_font.family.clone().unwrap()).or_insert(
232 Font {
233 family: repo_font.family.clone().unwrap(),
234 repo_font: HashMap::new(),
235 local_font: HashMap::new(),
236 }
237 );
238 current_font
239 .repo_font
240 .insert(repo_name.to_string(), repo_font.clone());
241 }
242 }
243
244 for local_font in local_fonts {
245 let local_font_format = Font {
246 family: local_font.family.clone().unwrap(),
247 repo_font: HashMap::new(),
248 local_font: HashMap::from([
249 (Location::System, local_font.clone()),
250 (Location::User, local_font.clone()),
251 (Location::Memory, local_font.clone())
252 ]),
253 };
254 let current_font = result.entry(local_font.family.clone().unwrap()).or_insert(
255 local_font_format
256 );
257 for location in [Location::User, Location::System, Location::Memory].iter() {
258 current_font.local_font.insert(
259 location.clone(), local_font.clone(),
260 );
261 }
262 }
263 result
264}
265
266pub fn generate_local_font_from_handles(handles: &[Handle]) -> (Location, LocalFont) {
267 let mut family_name = "".to_string();
268 let mut variants: Vec<String> = Vec::new();
269 let mut files: HashMap<String, PathBuf> = HashMap::new();
270 let mut lastModified = None;
271
272 let mut location = Location::Memory;
273
274 for handle in handles.iter() {
275 match handle.load() {
276 Ok(font_info) => {
277 family_name = font_info.family_name();
278
279 let variant = match font_info.postscript_name() {
280 Some(postscript_name) => {
281 let mut var = postscript_name.replace(&family_name, "")
282 .replace(&family_name.replace(" ", ""), "")
283 .replace("-", " ");
284 if var.len() == 0 {
285 var = "Regular".to_string();
286 }
287 while variants.contains(&var) {
288 var = var + "-";
289 }
290 var
291 },
292 None => "Regular".to_string()
293 };
294
295 variants.push(variant.clone());
296
297 match handle {
298 Handle::Path {ref path, font_index: _} => {
299 lastModified = Some(
300 match fs::metadata(&path) {
301 Ok(metadata) => {
302 match metadata.modified() {
303 Ok(time) => time,
304 Err(_) => SystemTime::now()
305 }
306 },
307 Err(_) => SystemTime::now()
308 }
309 );
310 location = if path.starts_with(home_dir().unwrap()) {
311 Location::User
312 } else {
313 Location::System
314 };
315
316 files.insert(
317 variant,
318 path.to_path_buf()
319 );
320 },
321 Memory => {
322 lastModified = Some(SystemTime::now());
323 location = Location::Memory;
324 }
325 }
326 },
327 Err(_) => {}
328 }
329 }
330 (
331 location,
332 LocalFont {
333 family: Some(family_name),
334 variants: {
335 if !variants.is_empty() {
336 Some(variants)
337 } else {
338 None
339 }
340 },
341 files: {
342 if !files.is_empty() {
343 Some(files)
344 } else {
345 None
346 }
347 },
348 lastModified: lastModified,
349 installed: Some(true)
350 }
351 )
352}
353
354 macro_rules! create_fn {
355 (
356 $func_name:ident,
357 $variable:ident,
358 $default_return:expr,
359 $return_type:ty
360 ) => {
361 fn $func_name(&mut self, location: &Location) -> $return_type {
362 match self.local_font.get(location) {
363 Some(font) => {
364 match &font.$variable {
365 Some(value) => value.clone(),
366 None => {
367 match SystemSource::new().select_family_by_name(&self.family) {
368 Ok(family_handle) => {
369 let new_local_font = generate_local_font_from_handles(
370 family_handle.fonts()
371 );
372 self.local_font.insert(
373 new_local_font.0.clone(), new_local_font.1
374 );
375 if &new_local_font.0 == location {
376 return self.$func_name(location);
377 }
378 $default_return
379 },
380 Err(_) => $default_return
381 }
382 }
383 }
384 },
385 None => $default_return
386 }
387 }
388 }
389 }
390
391impl Font {
392 create_fn!(is_font_x_installed, installed, false, bool);
393 create_fn!(get_local_x_variants, variants, Vec::new(), Vec<String>);
394 create_fn!(get_local_x_files, files, HashMap::new(), HashMap<String, PathBuf>);
395 create_fn!(get_local_x_last_modified, lastModified, SystemTime::now(), SystemTime);
396 create_fn!(get_local_x_font_family, family, "".to_string(), String);
397
398 pub fn is_font_system_installed(&mut self) -> bool {
399 self.is_font_x_installed(&Location::System)
400 }
401 pub fn is_font_user_installed(&mut self) -> bool {
402 self.is_font_x_installed(&Location::User)
403 }
404 pub fn is_font_memory_installed(&mut self) -> bool {
405 self.is_font_x_installed(&Location::Memory)
406 }
407
408 pub fn is_font_installed(&mut self) -> bool {
409 self.is_font_system_installed() ||
410 self.is_font_user_installed() ||
411 self.is_font_memory_installed()
412 }
413
414 pub fn get_local_system_variants(&mut self) -> Vec<String> {
415 self.get_local_x_variants(&Location::System)
416 }
417 pub fn get_local_user_variants(&mut self) -> Vec<String> {
418 self.get_local_x_variants(&Location::User)
419 }
420 pub fn get_local_memory_variants(&mut self) -> Vec<String> {
421 self.get_local_x_variants(&Location::Memory)
422 }
423
424 pub fn get_local_system_files(&mut self) -> HashMap<String, PathBuf> {
425 self.get_local_x_files(&Location::System)
426 }
427 pub fn get_local_user_files(&mut self) -> HashMap<String, PathBuf> {
428 self.get_local_x_files(&Location::User)
429 }
430 pub fn get_local_memory_files(&mut self) -> HashMap<String, PathBuf> {
431 self.get_local_x_files(&Location::Memory)
432 }
433
434 pub fn get_local_system_last_modified(&mut self) -> DateTime<Utc> {
435 DateTime::<Utc>::from(
436 self.get_local_x_last_modified(&Location::System)
437 )
438 }
439 pub fn get_local_user_last_modified(&mut self) -> DateTime<Utc> {
440 DateTime::<Utc>::from(
441 self.get_local_x_last_modified(&Location::User)
442 )
443 }
444 pub fn get_local_memory_last_modified(&mut self) -> DateTime<Utc> {
445 DateTime::<Utc>::from(
446 self.get_local_x_last_modified(&Location::Memory)
447 )
448 }
449
450 pub fn get_local_system_font_family(&mut self) -> String {
451 self.get_local_x_font_family(&Location::System).to_string()
452 }
453 pub fn get_local_user_font_family(&mut self) -> String {
454 self.get_local_x_font_family(&Location::User).to_string()
455 }
456 pub fn get_local_memory_font_family(&mut self) -> String {
457 self.get_local_x_font_family(&Location::Memory).to_string()
458 }
459
460 pub fn is_font_in_repo(&self, repo: &str) -> bool {
461 match &self.repo_font.get(repo) {
462 Some(_repo_font) => true,
463 None => false,
464 }
465 }
466
467 pub fn get_repos_availability(&self) -> Option<Vec<String>> {
468 if self.repo_font.len() > 0 {
469 Some(self.repo_font.keys().cloned().collect())
470 } else {
471 None
472 }
473 }
474
475 pub fn get_repo_variants(&self, repo: &str) -> Option<Vec<String>> {
476 match &self.repo_font.get(repo) {
477 Some(repo_font) => Some(repo_font.variants.clone()),
478 None => None,
479 }
480 }
481
482 pub fn get_repo_files(&self, repo: &str) -> Option<HashMap<String, String>> {
483 match &self.repo_font.get(repo) {
484 Some(repo_font) => Some(repo_font.files.clone()),
485 None => None,
486 }
487 }
488
489 pub fn get_repo_last_modified(&self, repo: &str) -> Option<DateTime<Utc>> {
490 match &self.repo_font.get(repo) {
491 Some(repo_font) => match &repo_font.lastModified {
492 Some(date) => {
493 let naive_date = NaiveDate::parse_from_str(&date, "%Y-%m-%d");
494 match naive_date {
495 Ok(naive_date) => {
496 Some(DateTime::from_utc(naive_date.and_hms(0, 0, 0), Utc))
497 }
498 Err(_) => {
499 eprintln!("error: date not in %Y-%m-%d");
500 None
501 }
502 }
503 }
504 None => None,
505 },
506 None => None,
507 }
508 }
509
510 pub fn get_repo_family(&self, repo: &str) -> Option<String> {
511 match &self.repo_font.get(repo) {
512 Some(repo_font) => Some(repo_font.family.clone().unwrap()),
513 None => None,
514 }
515 }
516
517 pub fn get_repo_subsets(&self, repo: &str) -> Option<Vec<String>> {
518 match &self.repo_font.get(repo) {
519 Some(repo_font) => match &repo_font.subsets {
520 Some(i) => Some(i.clone()),
521 None => None,
522 },
523 None => None,
524 }
525 }
526
527 pub fn get_repo_version(&self, repo: &str) -> Option<String> {
528 match &self.repo_font.get(repo) {
529 Some(repo_font) => match &repo_font.version {
530 Some(i) => Some(i.clone()),
531 None => None,
532 },
533 None => None,
534 }
535 }
536
537 pub fn get_repo_commentary(&self, repo: &str) -> Option<String> {
538 match &self.repo_font.get(repo) {
539 Some(repo_font) => match &repo_font.commentary {
540 Some(i) => Some(i.clone()),
541 None => None,
542 },
543 None => None,
544 }
545 }
546
547 pub fn get_repo_creator(&self, repo: &str) -> Option<String> {
548 match &self.repo_font.get(repo) {
549 Some(repo_font) => match &repo_font.creator {
550 Some(i) => Some(i.clone()),
551 None => None,
552 },
553 None => None,
554 }
555 }
556
557 pub fn get_all_repos_with_update_user(&mut self) -> Option<Vec<String>> {
558 let mut result: Vec<String> = Vec::new();
559 let local_last_modified = &self.get_local_user_last_modified();
560 match &self.get_repos_availability() {
561 Some(repos) => {
562 for repo in repos.iter() {
563 match &self.get_repo_last_modified(repo) {
564 Some(repo_last_modified) => {
565 if repo_last_modified > local_last_modified {
566 result.push(repo.to_string());
567 }
568 }
569 None => {}
570 }
571 }
572 }
573 None => {}
574 }
575 if result.len() > 0 {
576 Some(result)
577 } else {
578 None
579 }
580 }
581 pub fn get_all_repos_with_update_system(&mut self) -> Option<Vec<String>> {
582 let mut result: Vec<String> = Vec::new();
583 let local_last_modified = &self.get_local_system_last_modified();
584 match &self.get_repos_availability() {
585 Some(repos) => {
586 for repo in repos.iter() {
587 match &self.get_repo_last_modified(repo) {
588 Some(repo_last_modified) => {
589 if repo_last_modified > local_last_modified {
590 result.push(repo.to_string());
591 }
592 }
593 None => {}
594 }
595 }
596 }
597 None => {}
598 }
599 if result.len() > 0 {
600 Some(result)
601 } else {
602 None
603 }
604 }
605
606 pub fn is_update_available_user(&mut self) -> bool {
607 match self.get_all_repos_with_update_user() {
608 Some(_repos) => true,
609 None => false,
610 }
611 }
612 pub fn is_update_available_system(&mut self) -> bool {
613 match self.get_all_repos_with_update_system() {
614 Some(_repos) => true,
615 None => false,
616 }
617 }
618
619 pub fn get_first_available_repo(&self) -> Option<String> {
620 let repos = &self.get_repos_availability();
621 match repos {
622 Some(repos) => Some(repos.first().unwrap().to_string()),
623 None => None,
624 }
625 }
626
627 pub fn uninstall_from_user(&mut self, output: bool) -> Result<()> {
628 for (_name, file) in self.get_local_user_files(){
629 if output {
630 println!("Removing {}...", &file.display());
631 }
632 fs::remove_file(&file)?;
633 }
634 self.local_font.insert(
635 Location::User,
636 LocalFont {
637 family: None,
638 variants: None,
639 files: None,
640 lastModified: None,
641 installed: Some(false)
642 }
643 );
644 Ok(())
645 }
646
647 pub fn uninstall_from_system(&mut self, output: bool) -> Result<()> {
648 for (_name, file) in self.get_local_system_files() {
649 if output {
650 println!("Removing {}...", &file.display());
651 }
652 fs::remove_file(&file)?;
653 }
654 self.local_font.insert(
655 Location::System,
656 LocalFont {
657 family: None,
658 variants: None,
659 files: None,
660 lastModified: None,
661 installed: Some(false)
662 }
663 );
664 Ok(())
665 }
666
667 pub fn download(
668 &self,
669 repo: Option<&str>,
670 download_path: &PathBuf,
671 output: bool,
672 ) -> Result<()> {
673 let repos = self.get_first_available_repo();
674 let repo = match repo {
675 Some(repo) => repo,
676 None => match &repos {
677 Some(repo) => repo,
678 None => "",
679 },
680 };
681 match self.get_repo_files(repo) {
682 Some(files) => {
683 for (variant, file) in files {
684 let extension: &str = file.split(".").collect::<Vec<&str>>().last().unwrap();
685
686 if output {
687 println!(
688 "Downloading {} from {}",
689 &format!(
690 "{}-{}.{}",
691 &self.get_repo_family(repo).unwrap(),
692 &variant,
693 &extension
694 ),
695 &file
696 );
697 }
698 download_file(
699 &download_path.join(&format!(
700 "{}-{}.{}",
701 &self.get_repo_family(repo).unwrap(),
702 &variant,
703 &extension
704 )),
705 &file,
706 )?;
707 }
708 }
709 None => {}
710 }
711 Ok(())
712 }
713
714 pub fn output_paths(
715 &self,
716 repo: Option<&str>,
717 path: &PathBuf
718 ) -> Vec<PathBuf> {
719 let repos = self.get_first_available_repo();
720 let repo = match repo {
721 Some(repo) => repo,
722 None => match &repos {
723 Some(repo) => repo,
724 None => "",
725 },
726 };
727
728 let mut results: Vec<PathBuf> = Vec::new();
729
730 match self.get_repo_files(repo) {
731 Some(files) => {
732 for (variant, file) in files {
733 let extension: &str = file.split(".").collect::<Vec<&str>>().last().unwrap();
734 results.push(
735 path.join(&format!(
736 "{}-{}.{}",
737 &self.get_repo_family(repo).unwrap(),
738 &variant,
739 &extension
740 ))
741 );
742 }
743 }
744 None => {}
745 }
746
747 results
748 }
749
750 pub fn install_to_user(&mut self, repo: Option<&str>, output: bool) -> Result<()> {
751 let install_dir = font_dir().unwrap();
752
753 self.download(repo.clone(), &install_dir, output)?;
754
755 let new_local_font = generate_local_font_from_handles(
756 &self.output_paths(repo, &install_dir).iter().map(
757 |path| {
758 Handle::from_path(path.to_path_buf(), 0)
759 }).collect::<Vec<Handle>>()
760 );
761 self.local_font.insert(
762 new_local_font.0.clone(), new_local_font.1
763 );
764
765 Ok(())
766 }
767}