Skip to main content

bevy_etcetera/
lib.rs

1//! A very small bevy wrapper over [`etcetera`](https://docs.rs/etcetera/latest/etcetera/). It allows you to
2//! access common directories across MacOS, Windows, and Linux.
3//!
4//! # Basic usage
5//!
6//! ```
7//! use bevy::prelude::*;
8//! use bevy_etcetera::Directories;
9//!
10//! fn my_setup_system(mut cmd: Commands) -> Result<(), BevyError> {
11//!     let directories = Directories::new("com", "doomy", "Cool Bevy Game")?;
12//!     cmd.insert_resource(directories);
13//!     Ok(())
14//! }
15//!
16//! fn my_system(directories: Res<Directories>) {
17//!     // Path dependent on OS
18//!     let path = directories
19//!         .data_dir()
20//!         .join("some_file")
21//!         .with_extension("item.ron");
22//! }
23//! ```
24
25use std::path::{Path, PathBuf};
26
27use bevy::prelude::*;
28use etcetera::{AppStrategy, AppStrategyArgs, app_strategy::choose_native_strategy};
29
30#[cfg(target_os = "windows")]
31type Strategy = etcetera::app_strategy::Windows;
32
33#[cfg(target_os = "linux")]
34type Strategy = etcetera::app_strategy::Xdg;
35
36#[cfg(any(target_os = "macos", target_os = "ios"))]
37type Strategy = etcetera::app_strategy::Apple;
38
39#[derive(Resource)]
40pub struct Directories(Strategy);
41
42impl Directories {
43    /// Creates a new [`Directories`] resource
44    ///
45    /// # Parameters
46    ///
47    /// * `top_level_domain` - The top level domain of the application, e.g.
48    ///   `com`, `org`, or `io.github`.
49    /// * `author` - The name of the author of the application.
50    /// * `app_name` - The application’s name. This should be capitalised if
51    ///   appropriate.
52    ///
53    /// # Returns
54    ///
55    /// Returns a `Result` containing the `Directories` struct, or a `BevyError`
56    /// (from [`etcetera::HomeDirError`] if the home directory cannot be found.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use bevy::prelude::*;
62    /// use bevy_etcetera::Directories;
63    ///
64    /// let mut world = World::new();
65    /// if let Ok(directories) = Directories::from_string("com.doomy.Cool Bevy Game") {
66    ///     world.insert_resource(directories);
67    /// }
68    /// ```
69    pub fn new(
70        top_level_domain: impl Into<String>,
71        author: impl Into<String>,
72        app_name: impl Into<String>,
73    ) -> Result<Self, BevyError> {
74        choose_native_strategy(AppStrategyArgs {
75            top_level_domain: top_level_domain.into(),
76            author: author.into(),
77            app_name: app_name.into(),
78        })
79        .map(Self)
80        .map_err(BevyError::from)
81    }
82
83    /// Creates a new [`Directories`] resource given a valid string. Valid
84    /// identifiers should contain the top level domain, author, and then app
85    /// name, separated by periods.
86    ///
87    /// # Examples
88    ///
89    /// "com.example.My App"
90    /// "My.App.Name"
91    ///
92    /// # Parameters
93    ///
94    /// * `value` - the string identifier to parse
95    ///
96    /// # Returns
97    ///
98    /// Returns [`Directories`] if successful, or an error if an invalid string,
99    /// or the home directory was not found
100    pub fn from_string<S>(value: S) -> Result<Self, BevyError>
101    where
102        S: AsRef<str>,
103    {
104        let values: Vec<&str> = value.as_ref().splitn(3, '.').collect();
105        match values.len() == 3 {
106            false => Err("Invalid string provided.".into()),
107            true => Ok(Self::new(values[0], values[1], values[2])?),
108        }
109    }
110
111    /// Gets the home directory of the current user
112    ///
113    /// * MacOS - `~/`
114    /// * Linux - `~/`
115    /// * Windows - `%USERPROFILE%`
116    pub fn home_dir(&self) -> &Path {
117        self.0.home_dir()
118    }
119
120    /// Gets the configuration directory for your application.
121    ///
122    /// * MacOS - `~/Library/Preferences/`
123    /// * Linux - `~/.config/`
124    /// * Windows - `%APPDATA%`
125    pub fn config_dir(&self) -> PathBuf {
126        self.0.config_dir()
127    }
128
129    /// Gets the data directory for your application.
130    ///
131    /// * MacOS - `~/Library/Application Support/`
132    /// * Linux - `~/.local/share/`
133    /// * Windows - `%APPDATA%`
134    pub fn data_dir(&self) -> PathBuf {
135        self.0.data_dir()
136    }
137
138    /// Gets the cache directory for your application.
139    ///
140    /// * MacOS - `~/Library/Caches/`
141    /// * Linux - `~/.cache/`
142    /// * Windows - `%LOCALAPPDATA%`
143    pub fn cache_dir(&self) -> PathBuf {
144        self.0.cache_dir()
145    }
146
147    /// Gets the state directory for your application. State directory may not
148    /// to exist for all conventions.
149    ///
150    /// * Linux - `~/.local/state`
151    pub fn state_dir(&self) -> Option<PathBuf> {
152        self.0.state_dir()
153    }
154
155    /// Gets the runtime directory for your application. Runtime directory may
156    /// not to exist for all conventions.
157    pub fn runtime_dir(&self) -> Option<PathBuf> {
158        self.0.runtime_dir()
159    }
160
161    /// Gets the underlying [`Strategy`] used to obtain directories
162    pub fn strategy(&self) -> &Strategy {
163        &self.0
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn create_valid_directories_from_string_literals() {
173        [
174            "a.b.c",
175            "1.2.3",
176            "Testing One.Mycool Bundle.App Name",
177            "APP$1.90))3,.(DJHG",
178        ]
179        .iter()
180        .for_each(|s| {
181            matches!(Directories::from_string(s), Ok(_));
182        })
183    }
184
185    #[test]
186    fn create_invalid_directories_from_string_literals() {
187        [
188            "a.b.c.d",
189            "1.2",
190            "Testing One..Mycool Bundle.App Name",
191            "ABCDEFG",
192        ]
193        .iter()
194        .for_each(|s| {
195            matches!(Directories::from_string(s), Err(_));
196        })
197    }
198}