tauri_bundler/bundle/
category.rs

1// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
2// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6use std::{fmt, str::FromStr};
7
8const CONFIDENCE_THRESHOLD: f64 = 0.8;
9
10const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category.";
11
12// TODO: RIght now, these categories correspond to LSApplicationCategoryType
13// values for OS X.  There are also some additional GNOME registered categories
14// that don't fit these; we should add those here too.
15/// The possible app categories.
16/// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.
17#[allow(missing_docs)]
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19#[non_exhaustive]
20pub enum AppCategory {
21  Business,
22  DeveloperTool,
23  Education,
24  Entertainment,
25  Finance,
26  Game,
27  ActionGame,
28  AdventureGame,
29  ArcadeGame,
30  BoardGame,
31  CardGame,
32  CasinoGame,
33  DiceGame,
34  EducationalGame,
35  FamilyGame,
36  KidsGame,
37  MusicGame,
38  PuzzleGame,
39  RacingGame,
40  RolePlayingGame,
41  SimulationGame,
42  SportsGame,
43  StrategyGame,
44  TriviaGame,
45  WordGame,
46  GraphicsAndDesign,
47  HealthcareAndFitness,
48  Lifestyle,
49  Medical,
50  Music,
51  News,
52  Photography,
53  Productivity,
54  Reference,
55  SocialNetworking,
56  Sports,
57  Travel,
58  Utility,
59  Video,
60  Weather,
61}
62
63impl FromStr for AppCategory {
64  type Err = Option<&'static str>;
65
66  /// Given a string, returns the `AppCategory` it refers to, or the closest
67  /// string that the user might have intended (if any).
68  fn from_str(input: &str) -> Result<AppCategory, Self::Err> {
69    // Canonicalize input:
70    let mut input = input.to_ascii_lowercase();
71    if input.starts_with(MACOS_APP_CATEGORY_PREFIX) {
72      input = input
73        .split_at(MACOS_APP_CATEGORY_PREFIX.len())
74        .1
75        .to_string();
76    }
77    input = input.replace(' ', "");
78    input = input.replace('-', "");
79
80    // Find best match:
81    let mut best_confidence = 0.0;
82    let mut best_category: Option<AppCategory> = None;
83    for &(string, category) in CATEGORY_STRINGS.iter() {
84      if input == string {
85        return Ok(category);
86      }
87      let confidence = strsim::jaro_winkler(&input, string);
88      if confidence >= CONFIDENCE_THRESHOLD && confidence > best_confidence {
89        best_confidence = confidence;
90        best_category = Some(category);
91      }
92    }
93    Err(best_category.map(AppCategory::canonical))
94  }
95}
96
97impl AppCategory {
98  /// Map an AppCategory to the string we recommend to use in Cargo.toml if
99  /// the users misspells the category name.
100  fn canonical(self) -> &'static str {
101    match self {
102      AppCategory::Business => "Business",
103      AppCategory::DeveloperTool => "Developer Tool",
104      AppCategory::Education => "Education",
105      AppCategory::Entertainment => "Entertainment",
106      AppCategory::Finance => "Finance",
107      AppCategory::Game => "Game",
108      AppCategory::ActionGame => "Action Game",
109      AppCategory::AdventureGame => "Adventure Game",
110      AppCategory::ArcadeGame => "Arcade Game",
111      AppCategory::BoardGame => "Board Game",
112      AppCategory::CardGame => "Card Game",
113      AppCategory::CasinoGame => "Casino Game",
114      AppCategory::DiceGame => "Dice Game",
115      AppCategory::EducationalGame => "Educational Game",
116      AppCategory::FamilyGame => "Family Game",
117      AppCategory::KidsGame => "Kids Game",
118      AppCategory::MusicGame => "Music Game",
119      AppCategory::PuzzleGame => "Puzzle Game",
120      AppCategory::RacingGame => "Racing Game",
121      AppCategory::RolePlayingGame => "Role-Playing Game",
122      AppCategory::SimulationGame => "Simulation Game",
123      AppCategory::SportsGame => "Sports Game",
124      AppCategory::StrategyGame => "Strategy Game",
125      AppCategory::TriviaGame => "Trivia Game",
126      AppCategory::WordGame => "Word Game",
127      AppCategory::GraphicsAndDesign => "Graphics and Design",
128      AppCategory::HealthcareAndFitness => "Healthcare and Fitness",
129      AppCategory::Lifestyle => "Lifestyle",
130      AppCategory::Medical => "Medical",
131      AppCategory::Music => "Music",
132      AppCategory::News => "News",
133      AppCategory::Photography => "Photography",
134      AppCategory::Productivity => "Productivity",
135      AppCategory::Reference => "Reference",
136      AppCategory::SocialNetworking => "Social Networking",
137      AppCategory::Sports => "Sports",
138      AppCategory::Travel => "Travel",
139      AppCategory::Utility => "Utility",
140      AppCategory::Video => "Video",
141      AppCategory::Weather => "Weather",
142    }
143  }
144
145  /// Map an AppCategory to the closest set of Freedesktop registered
146  /// categories that matches that category.
147  ///
148  /// Cf <https://specifications.freedesktop.org/menu-spec/latest/>
149  pub fn freedesktop_categories(self) -> &'static str {
150    match &self {
151      AppCategory::Business => "Office;",
152      AppCategory::DeveloperTool => "Development;",
153      AppCategory::Education => "Education;",
154      AppCategory::Entertainment => "Network;",
155      AppCategory::Finance => "Office;Finance;",
156      AppCategory::Game => "Game;",
157      AppCategory::ActionGame => "Game;ActionGame;",
158      AppCategory::AdventureGame => "Game;AdventureGame;",
159      AppCategory::ArcadeGame => "Game;ArcadeGame;",
160      AppCategory::BoardGame => "Game;BoardGame;",
161      AppCategory::CardGame => "Game;CardGame;",
162      AppCategory::CasinoGame => "Game;",
163      AppCategory::DiceGame => "Game;",
164      AppCategory::EducationalGame => "Game;Education;",
165      AppCategory::FamilyGame => "Game;",
166      AppCategory::KidsGame => "Game;KidsGame;",
167      AppCategory::MusicGame => "Game;",
168      AppCategory::PuzzleGame => "Game;LogicGame;",
169      AppCategory::RacingGame => "Game;",
170      AppCategory::RolePlayingGame => "Game;RolePlaying;",
171      AppCategory::SimulationGame => "Game;Simulation;",
172      AppCategory::SportsGame => "Game;SportsGame;",
173      AppCategory::StrategyGame => "Game;StrategyGame;",
174      AppCategory::TriviaGame => "Game;",
175      AppCategory::WordGame => "Game;",
176      AppCategory::GraphicsAndDesign => "Graphics;",
177      AppCategory::HealthcareAndFitness => "Science;",
178      AppCategory::Lifestyle => "Education;",
179      AppCategory::Medical => "Science;MedicalSoftware;",
180      AppCategory::Music => "AudioVideo;Audio;Music;",
181      AppCategory::News => "Network;News;",
182      AppCategory::Photography => "Graphics;Photography;",
183      AppCategory::Productivity => "Office;",
184      AppCategory::Reference => "Education;",
185      AppCategory::SocialNetworking => "Network;",
186      AppCategory::Sports => "Education;Sports;",
187      AppCategory::Travel => "Education;",
188      AppCategory::Utility => "Utility;",
189      AppCategory::Video => "AudioVideo;Video;",
190      AppCategory::Weather => "Science;",
191    }
192  }
193
194  /// Map an AppCategory to the closest LSApplicationCategoryType value that
195  /// matches that category.
196  pub fn macos_application_category_type(self) -> &'static str {
197    match &self {
198      AppCategory::Business => "public.app-category.business",
199      AppCategory::DeveloperTool => "public.app-category.developer-tools",
200      AppCategory::Education => "public.app-category.education",
201      AppCategory::Entertainment => "public.app-category.entertainment",
202      AppCategory::Finance => "public.app-category.finance",
203      AppCategory::Game => "public.app-category.games",
204      AppCategory::ActionGame => "public.app-category.action-games",
205      AppCategory::AdventureGame => "public.app-category.adventure-games",
206      AppCategory::ArcadeGame => "public.app-category.arcade-games",
207      AppCategory::BoardGame => "public.app-category.board-games",
208      AppCategory::CardGame => "public.app-category.card-games",
209      AppCategory::CasinoGame => "public.app-category.casino-games",
210      AppCategory::DiceGame => "public.app-category.dice-games",
211      AppCategory::EducationalGame => "public.app-category.educational-games",
212      AppCategory::FamilyGame => "public.app-category.family-games",
213      AppCategory::KidsGame => "public.app-category.kids-games",
214      AppCategory::MusicGame => "public.app-category.music-games",
215      AppCategory::PuzzleGame => "public.app-category.puzzle-games",
216      AppCategory::RacingGame => "public.app-category.racing-games",
217      AppCategory::RolePlayingGame => "public.app-category.role-playing-games",
218      AppCategory::SimulationGame => "public.app-category.simulation-games",
219      AppCategory::SportsGame => "public.app-category.sports-games",
220      AppCategory::StrategyGame => "public.app-category.strategy-games",
221      AppCategory::TriviaGame => "public.app-category.trivia-games",
222      AppCategory::WordGame => "public.app-category.word-games",
223      AppCategory::GraphicsAndDesign => "public.app-category.graphics-design",
224      AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness",
225      AppCategory::Lifestyle => "public.app-category.lifestyle",
226      AppCategory::Medical => "public.app-category.medical",
227      AppCategory::Music => "public.app-category.music",
228      AppCategory::News => "public.app-category.news",
229      AppCategory::Photography => "public.app-category.photography",
230      AppCategory::Productivity => "public.app-category.productivity",
231      AppCategory::Reference => "public.app-category.reference",
232      AppCategory::SocialNetworking => "public.app-category.social-networking",
233      AppCategory::Sports => "public.app-category.sports",
234      AppCategory::Travel => "public.app-category.travel",
235      AppCategory::Utility => "public.app-category.utilities",
236      AppCategory::Video => "public.app-category.video",
237      AppCategory::Weather => "public.app-category.weather",
238    }
239  }
240}
241
242impl<'d> serde::Deserialize<'d> for AppCategory {
243  fn deserialize<D: serde::Deserializer<'d>>(deserializer: D) -> Result<AppCategory, D::Error> {
244    deserializer.deserialize_str(AppCategoryVisitor { did_you_mean: None })
245  }
246}
247
248struct AppCategoryVisitor {
249  did_you_mean: Option<&'static str>,
250}
251
252impl serde::de::Visitor<'_> for AppCategoryVisitor {
253  type Value = AppCategory;
254
255  fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256    match self.did_you_mean {
257      Some(string) => write!(
258        formatter,
259        "a valid app category string (did you mean \"{string}\"?)"
260      ),
261      None => write!(formatter, "a valid app category string"),
262    }
263  }
264
265  fn visit_str<E: serde::de::Error>(mut self, value: &str) -> Result<AppCategory, E> {
266    match AppCategory::from_str(value) {
267      Ok(category) => Ok(category),
268      Err(did_you_mean) => {
269        self.did_you_mean = did_you_mean;
270        let unexp = serde::de::Unexpected::Str(value);
271        Err(serde::de::Error::invalid_value(unexp, &self))
272      }
273    }
274  }
275}
276
277const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[
278  ("actiongame", AppCategory::ActionGame),
279  ("actiongames", AppCategory::ActionGame),
280  ("adventuregame", AppCategory::AdventureGame),
281  ("adventuregames", AppCategory::AdventureGame),
282  ("arcadegame", AppCategory::ArcadeGame),
283  ("arcadegames", AppCategory::ArcadeGame),
284  ("boardgame", AppCategory::BoardGame),
285  ("boardgames", AppCategory::BoardGame),
286  ("business", AppCategory::Business),
287  ("cardgame", AppCategory::CardGame),
288  ("cardgames", AppCategory::CardGame),
289  ("casinogame", AppCategory::CasinoGame),
290  ("casinogames", AppCategory::CasinoGame),
291  ("developer", AppCategory::DeveloperTool),
292  ("developertool", AppCategory::DeveloperTool),
293  ("developertools", AppCategory::DeveloperTool),
294  ("development", AppCategory::DeveloperTool),
295  ("dicegame", AppCategory::DiceGame),
296  ("dicegames", AppCategory::DiceGame),
297  ("education", AppCategory::Education),
298  ("educationalgame", AppCategory::EducationalGame),
299  ("educationalgames", AppCategory::EducationalGame),
300  ("entertainment", AppCategory::Entertainment),
301  ("familygame", AppCategory::FamilyGame),
302  ("familygames", AppCategory::FamilyGame),
303  ("finance", AppCategory::Finance),
304  ("fitness", AppCategory::HealthcareAndFitness),
305  ("game", AppCategory::Game),
306  ("games", AppCategory::Game),
307  ("graphicdesign", AppCategory::GraphicsAndDesign),
308  ("graphicsanddesign", AppCategory::GraphicsAndDesign),
309  ("graphicsdesign", AppCategory::GraphicsAndDesign),
310  ("healthcareandfitness", AppCategory::HealthcareAndFitness),
311  ("healthcarefitness", AppCategory::HealthcareAndFitness),
312  ("kidsgame", AppCategory::KidsGame),
313  ("kidsgames", AppCategory::KidsGame),
314  ("lifestyle", AppCategory::Lifestyle),
315  ("logicgame", AppCategory::PuzzleGame),
316  ("medical", AppCategory::Medical),
317  ("medicalsoftware", AppCategory::Medical),
318  ("music", AppCategory::Music),
319  ("musicgame", AppCategory::MusicGame),
320  ("musicgames", AppCategory::MusicGame),
321  ("news", AppCategory::News),
322  ("photography", AppCategory::Photography),
323  ("productivity", AppCategory::Productivity),
324  ("puzzlegame", AppCategory::PuzzleGame),
325  ("puzzlegames", AppCategory::PuzzleGame),
326  ("racinggame", AppCategory::RacingGame),
327  ("racinggames", AppCategory::RacingGame),
328  ("reference", AppCategory::Reference),
329  ("roleplaying", AppCategory::RolePlayingGame),
330  ("roleplayinggame", AppCategory::RolePlayingGame),
331  ("roleplayinggames", AppCategory::RolePlayingGame),
332  ("rpg", AppCategory::RolePlayingGame),
333  ("simulationgame", AppCategory::SimulationGame),
334  ("simulationgames", AppCategory::SimulationGame),
335  ("socialnetwork", AppCategory::SocialNetworking),
336  ("socialnetworking", AppCategory::SocialNetworking),
337  ("sports", AppCategory::Sports),
338  ("sportsgame", AppCategory::SportsGame),
339  ("sportsgames", AppCategory::SportsGame),
340  ("strategygame", AppCategory::StrategyGame),
341  ("strategygames", AppCategory::StrategyGame),
342  ("travel", AppCategory::Travel),
343  ("triviagame", AppCategory::TriviaGame),
344  ("triviagames", AppCategory::TriviaGame),
345  ("utilities", AppCategory::Utility),
346  ("utility", AppCategory::Utility),
347  ("video", AppCategory::Video),
348  ("weather", AppCategory::Weather),
349  ("wordgame", AppCategory::WordGame),
350  ("wordgames", AppCategory::WordGame),
351];
352
353#[cfg(test)]
354mod tests {
355  use super::AppCategory;
356  use std::str::FromStr;
357
358  #[test]
359  fn category_from_string_ok() {
360    // Canonical name of category works:
361    assert_eq!(
362      AppCategory::from_str("Education"),
363      Ok(AppCategory::Education)
364    );
365    assert_eq!(
366      AppCategory::from_str("Developer Tool"),
367      Ok(AppCategory::DeveloperTool)
368    );
369    // Lowercase, spaces, and hyphens are fine:
370    assert_eq!(
371      AppCategory::from_str(" puzzle  game "),
372      Ok(AppCategory::PuzzleGame)
373    );
374    assert_eq!(
375      AppCategory::from_str("Role-playing game"),
376      Ok(AppCategory::RolePlayingGame)
377    );
378    // Using macOS LSApplicationCategoryType value is fine:
379    assert_eq!(
380      AppCategory::from_str("public.app-category.developer-tools"),
381      Ok(AppCategory::DeveloperTool)
382    );
383    assert_eq!(
384      AppCategory::from_str("public.app-category.role-playing-games"),
385      Ok(AppCategory::RolePlayingGame)
386    );
387    // Using GNOME category name is fine:
388    assert_eq!(
389      AppCategory::from_str("Development"),
390      Ok(AppCategory::DeveloperTool)
391    );
392    assert_eq!(
393      AppCategory::from_str("LogicGame"),
394      Ok(AppCategory::PuzzleGame)
395    );
396    // Using common abbreviations is fine:
397    assert_eq!(
398      AppCategory::from_str("RPG"),
399      Ok(AppCategory::RolePlayingGame)
400    );
401  }
402
403  #[test]
404  fn category_from_string_did_you_mean() {
405    assert_eq!(AppCategory::from_str("gaming"), Err(Some("Game")));
406    assert_eq!(AppCategory::from_str("photos"), Err(Some("Photography")));
407    assert_eq!(
408      AppCategory::from_str("strategery"),
409      Err(Some("Strategy Game"))
410    );
411  }
412
413  #[test]
414  fn category_from_string_totally_wrong() {
415    assert_eq!(AppCategory::from_str("fhqwhgads"), Err(None));
416    assert_eq!(AppCategory::from_str("WHARRGARBL"), Err(None));
417  }
418
419  #[test]
420  fn ls_application_category_type_round_trip() {
421    let values = &[
422      "public.app-category.business",
423      "public.app-category.developer-tools",
424      "public.app-category.education",
425      "public.app-category.entertainment",
426      "public.app-category.finance",
427      "public.app-category.games",
428      "public.app-category.action-games",
429      "public.app-category.adventure-games",
430      "public.app-category.arcade-games",
431      "public.app-category.board-games",
432      "public.app-category.card-games",
433      "public.app-category.casino-games",
434      "public.app-category.dice-games",
435      "public.app-category.educational-games",
436      "public.app-category.family-games",
437      "public.app-category.kids-games",
438      "public.app-category.music-games",
439      "public.app-category.puzzle-games",
440      "public.app-category.racing-games",
441      "public.app-category.role-playing-games",
442      "public.app-category.simulation-games",
443      "public.app-category.sports-games",
444      "public.app-category.strategy-games",
445      "public.app-category.trivia-games",
446      "public.app-category.word-games",
447      "public.app-category.graphics-design",
448      "public.app-category.healthcare-fitness",
449      "public.app-category.lifestyle",
450      "public.app-category.medical",
451      "public.app-category.music",
452      "public.app-category.news",
453      "public.app-category.photography",
454      "public.app-category.productivity",
455      "public.app-category.reference",
456      "public.app-category.social-networking",
457      "public.app-category.sports",
458      "public.app-category.travel",
459      "public.app-category.utilities",
460      "public.app-category.video",
461      "public.app-category.weather",
462    ];
463    // Test that if the user uses an LSApplicationCategoryType string as
464    // the category string, they will get back that same string for the
465    // macOS app bundle LSApplicationCategoryType.
466    for &value in values.iter() {
467      let category = AppCategory::from_str(value).expect(value);
468      assert_eq!(category.macos_application_category_type(), value);
469    }
470  }
471}