mecomp_core/
lib.rs

1#![deny(clippy::missing_inline_in_public_items)]
2
3use errors::DirectoryError;
4
5#[cfg(feature = "audio")]
6pub mod audio;
7pub mod config;
8pub mod errors;
9pub mod logger;
10pub mod state;
11#[cfg(any(test, feature = "test_utils"))]
12pub mod test_utils;
13#[cfg(feature = "notifications")]
14pub mod udp;
15
16#[must_use]
17#[inline]
18pub fn format_duration(duration: &std::time::Duration) -> String {
19    let total_seconds = duration.as_secs();
20    let hours = total_seconds / 3600;
21    let minutes = (total_seconds % 3600) / 60;
22    let seconds = duration.as_secs_f32() % 60.;
23
24    format!("{hours:02}:{minutes:02}:{seconds:05.2}")
25}
26
27/// Get the data directory for the application.
28///
29/// Follows the XDG Base Directory Specification for linux, and the equivalents on other platforms.
30/// See the [`directories`](https://docs.rs/directories/latest/directories/) crate for more information.
31///
32/// # Errors
33///
34/// This function will return an error if the data directory could not be found.
35#[inline]
36pub fn get_data_dir() -> Result<std::path::PathBuf, DirectoryError> {
37    let directory = if let Ok(s) = std::env::var("MECOMP_DATA") {
38        std::path::PathBuf::from(s)
39    } else if let Some(proj_dirs) =
40        directories::ProjectDirs::from("com", "anthonymichaeltdm", "mecomp")
41    {
42        proj_dirs.data_local_dir().to_path_buf()
43    } else {
44        return Err(DirectoryError::Data);
45    };
46    Ok(directory)
47}
48
49/// Get the config directory for the application.
50///
51/// Follows the XDG Base Directory Specification for linux, and the equivalents on other platforms.
52/// See the [`directories`](https://docs.rs/directories/latest/directories/) crate for more information.
53///
54/// # Errors
55///
56/// This function will return an error if the config directory could not be found.
57#[inline]
58pub fn get_config_dir() -> Result<std::path::PathBuf, DirectoryError> {
59    let directory = if let Ok(s) = std::env::var("MECOMP_CONFIG") {
60        std::path::PathBuf::from(s)
61    } else if let Some(proj_dirs) =
62        directories::ProjectDirs::from("com", "anthonymichaeltdm", "mecomp")
63    {
64        proj_dirs.config_local_dir().to_path_buf()
65    } else {
66        return Err(DirectoryError::Config);
67    };
68    Ok(directory)
69}
70
71/// Check if a server is already running on localhost on the given port.
72/// If a server is already running, return true, otherwise return false.
73#[must_use]
74#[inline]
75pub fn is_server_running(port: u16) -> bool {
76    std::net::TcpStream::connect(format!("localhost:{port}")).is_ok()
77}
78
79/// A `OnceLock` that returns a default value if it has not been set yet.
80#[derive(Debug, Clone)]
81pub struct OnceLockDefault<T> {
82    value: std::sync::OnceLock<T>,
83    default: T,
84}
85
86impl<T> OnceLockDefault<T> {
87    /// Creates a new `OnceLockDefault` with the given default value.
88    #[inline]
89    pub const fn new(default: T) -> Self {
90        Self {
91            value: std::sync::OnceLock::new(),
92            default,
93        }
94    }
95
96    /// Initializes the contents of the cell to value.
97    ///
98    /// May block if another thread is currently attempting to initialize the cell.
99    /// The cell is guaranteed to contain a value when set returns, though not necessarily the one provided.
100    ///
101    /// # Errors
102    ///
103    /// Returns `Ok(())` if the cell was uninitialized and `Err(value)` if the cell was already initialized.
104    #[inline]
105    pub fn set(&self, value: T) -> Result<(), T> {
106        self.value.set(value)
107    }
108
109    /// Gets the reference to the underlying value, if set. Otherwise returns a reference to the default value.
110    ///
111    /// This method never blocks.
112    #[inline]
113    pub fn get(&self) -> &T {
114        self.value.get().unwrap_or(&self.default)
115    }
116
117    /// Checks if the cell has been initialized.
118    ///
119    /// This method never blocks.
120    #[inline]
121    pub fn is_initialized(&self) -> bool {
122        self.value.get().is_some()
123    }
124}
125
126impl<T> std::ops::Deref for OnceLockDefault<T> {
127    type Target = T;
128
129    #[inline]
130    fn deref(&self) -> &Self::Target {
131        self.get()
132    }
133}
134
135#[cfg(test)]
136mod test {
137    use super::format_duration;
138    use pretty_assertions::assert_eq;
139    use rstest::rstest;
140    use std::time::Duration;
141
142    #[rstest]
143    #[case::zero(Duration::from_secs(0), "00:00:00.00")]
144    #[case::sub_second(Duration::from_millis(100), "00:00:00.10")]
145    #[case::sub_second(Duration::from_millis(101), "00:00:00.10")]
146    #[case::one_second(Duration::from_secs(1), "00:00:01.00")]
147    #[case::one_minute(Duration::from_secs(60), "00:01:00.00")]
148    #[case::one_hour(Duration::from_secs(3600), "01:00:00.00")]
149    #[case::one_hour_one_minute_one_second(Duration::from_secs(3661), "01:01:01.00")]
150    #[case(Duration::from_secs(3600 + 120 + 1), "01:02:01.00")]
151    fn test_format_duration(#[case] duration: Duration, #[case] expected: &str) {
152        let actual = format_duration(&duration);
153        assert_eq!(actual, expected);
154    }
155
156    #[test]
157    fn test_get_data_dir() {
158        let data_dir = super::get_data_dir().unwrap();
159        assert_eq!(
160            data_dir
161                .components()
162                .next_back()
163                .unwrap()
164                .as_os_str()
165                .to_string_lossy(),
166            "mecomp"
167        );
168    }
169
170    #[test]
171    fn test_get_config_dir() {
172        let config_dir = super::get_config_dir().unwrap();
173        assert_eq!(
174            config_dir
175                .components()
176                .next_back()
177                .unwrap()
178                .as_os_str()
179                .to_string_lossy(),
180            "mecomp"
181        );
182    }
183}