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#[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#[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#[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#[derive(Debug, Clone)]
81pub struct OnceLockDefault<T> {
82 value: std::sync::OnceLock<T>,
83 default: T,
84}
85
86impl<T> OnceLockDefault<T> {
87 #[inline]
89 pub const fn new(default: T) -> Self {
90 Self {
91 value: std::sync::OnceLock::new(),
92 default,
93 }
94 }
95
96 #[inline]
105 pub fn set(&self, value: T) -> Result<(), T> {
106 self.value.set(value)
107 }
108
109 #[inline]
113 pub fn get(&self) -> &T {
114 self.value.get().unwrap_or(&self.default)
115 }
116
117 #[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}