Skip to main content

sericom_core/path_utils/
macros.rs

1//! This module holds helper macros for dealing with paths
2
3/// Takes a [`&Path`][std::path::Path] and first checks whether it exists or if it is a
4/// directory. If it doesn't exist or is not a directory, it will create
5/// the directory recursively; creating the necessary parent directories.
6///
7/// ## Example
8/// ```
9/// use sericom_core::create_recursive;
10/// use std::path::PathBuf;
11/// fn mkdir() {
12///     let path = PathBuf::from("some/dir");
13///     create_recursive!(&path);
14///     assert!(path.is_dir() && path.exists());
15/// }
16/// ```
17#[macro_export]
18macro_rules! create_recursive {
19    ($path:expr) => {
20        let create_recursive_dir = |p: &std::path::Path| {
21            if !p.exists() || !p.is_dir() {
22                let mut builder = std::fs::DirBuilder::new();
23                builder.recursive(true);
24                builder.create(p).expect("Recursive mode won't panic");
25            }
26        };
27
28        create_recursive_dir($path)
29    };
30}
31
32/// Used to add a `.map_err()` to function calls that return a `Result<T, E>`
33/// to provide better context for the error and print it nicely to stdout.
34///
35/// Takes 2 arguements and optionally a third and fourth:
36/// - The first argument is the expression or function call that would return a `Result<T, E>`
37/// - The second argument is context that better describes the returned error
38/// - The optional third argument is the 'USAGE: sericom ...' that would typically be printed by `clap`
39///   for the respective command
40/// - The optional fourth argument is an additional "help:" message
41///
42/// ## Example
43/// ```
44/// use serial2_tokio::SerialPort;
45/// use crossterm::style::Stylize;
46/// use sericom_core::map_miette;
47/// fn returns_err() -> miette::Result<()> {
48///     let baud: u32 = 9600;
49///     let port = "/dev/fakeport";
50///     let x = map_miette!(
51///         SerialPort::open(port, baud),
52///         format!("Failed to open port '{}'", port),
53///         format!("{} {} [OPTIONS] [PORT] [COMMAND]",
54///             "USAGE:".bold().underlined(),
55///             "sericom".bold()
56///         ),
57///         help = format!(
58///             "To see available ports, try `{}`.",
59///             "sericom list-ports".bold().cyan()
60///         )
61///     )?;
62///     Ok(())
63/// }
64/// let fn_err = returns_err();
65/// assert!(fn_err.is_err());
66/// ```
67#[macro_export]
68macro_rules! map_miette {
69    // Clap-style USAGE: && additional "help" message
70    ($expr:expr, $wrap_msg:expr, $usage:expr, help = $add_help:expr) => {
71        $expr.map_err(|e| {
72            use crossterm::style::Stylize;
73            miette::miette!(
74                help = format!("{}\nFor more information, try `sericom --help`.", $add_help),
75                "{e}"
76            )
77            .wrap_err(format!("{}\n\n{}\n", $wrap_msg, $usage).red())
78        })
79    };
80
81    // Clap-style USAGE: && default "help" message
82    ($expr:expr, $wrap_msg:expr, $usage:expr) => {
83        $expr.map_err(|e| {
84            use crossterm::style::Stylize;
85            miette::miette!(help = "For more information, try `sericom --help`.", "{e}")
86                .wrap_err(format!("{}\n\n{}\n", $wrap_msg, $usage).red())
87        })
88    };
89
90    // Additional "help" message
91    ($expr:expr, $wrap_msg:expr, help = $add_help:expr) => {
92        $expr.map_err(|e| {
93            use crossterm::style::Stylize;
94            miette::miette!(
95                help = format!("{}\nFor more information, try `sericom --help`.", $add_help),
96                "{e}"
97            )
98            .wrap_err(format!("{}", $wrap_msg).red())
99        })
100    };
101
102    // Default "help" message
103    ($expr:expr, $wrap_msg:expr) => {
104        $expr.map_err(|e| {
105            use crossterm::style::Stylize;
106            miette::miette!(help = "For more information, try `sericom --help`.", "{e}")
107                .wrap_err(format!("{}", $wrap_msg).red())
108        })
109    };
110}
111
112/// Creates the default filename if none is specified from the `port` name
113/// and a timestamp.
114///
115/// Can also add a prefix the filename or the target out-dir.
116/// Path will end up looking like this:
117///  - Windows: `com4-09251554.txt`
118///  - Unix: `ttyUSB0-09251554.txt`
119///  - Prefixed: `trace-ttyUSB0-09251554.txt`
120///
121/// # Errors
122/// Errors on Unix systems if [`file_name`] returns `None`
123///
124/// # Examples
125/// ```
126/// use sericom_core::compat_port_path;
127/// use std::path::PathBuf;
128/// use chrono::Utc;
129///
130/// fn get_default_fname() -> miette::Result<()> {
131///     let out_dir = PathBuf::from("/home/dev/test");
132///     let port_name = PathBuf::from("/dev/ttyUSB0");
133///
134///     let fname = compat_port_path!(port_name.clone(), prefix = "test");
135///     assert_eq!(fname, PathBuf::from(format!(
136///         "testing-ttyUSB0-{}.txt",
137///         Utc::now().format("%m%d%H%M")
138///     )));
139///
140///     let fname = compat_port_path!(out_dir, port_name.clone(), prefix = "test");
141///     assert_eq!(fname, PathBuf::from(format!(
142///         "/home/dev/test/testing-ttyUSB0-{}.txt",
143///         Utc::now().format("%m%d%H%M")
144///     )));
145///
146///     let fname = compat_port_path!(out_dir, port_name.clone());
147///     assert_eq!(fname, PathBuf::from(format!(
148///         "/home/dev/test/ttyUSB0-{}.txt",
149///         Utc::now().format("%m%d%H%M")
150///     )));
151///
152///     let fname = compat_port_path!(port_name);
153///     assert_eq!(fname, PathBuf::from(format!(
154///         "ttyUSB0-{}.txt",
155///         Utc::now().format("%m%d%H%M")
156///     )));
157///     Ok(())
158/// }
159///
160///
161/// ```
162///
163/// [`file_name`]: std::path::Path::file_name()
164#[macro_export]
165macro_rules! compat_port_path {
166    ($port:expr, prefix = $prefix:literal) => {{
167        use chrono;
168        use std::path::PathBuf;
169
170        let path_port = $crate::path_utils::get_compat_port_path($port)?;
171        PathBuf::from(format!(
172            "./{}-{}-{}.txt",
173            $prefix,
174            path_port.display(),
175            chrono::Utc::now().format("%m%d%H%M"),
176        ))
177    }};
178
179    ($out_dir:expr, $port:expr, prefix = $prefix:expr) => {{
180        use chrono;
181
182        let path_port = $crate::path_utils::get_compat_port_path($port)?;
183        $out_dir.join(format!(
184            "./{}-{}-{}.txt",
185            $prefix,
186            path_port.display(),
187            chrono::Utc::now().format("%m%d%H%M"),
188        ))
189    }};
190
191    ($out_dir:expr, $port:expr) => {{
192        use chrono;
193
194        let path_port = $crate::path_utils::get_compat_port_path($port)?;
195        $out_dir.join(format!(
196            "./{}-{}.txt",
197            path_port.display(),
198            chrono::Utc::now().format("%m%d%H%M"),
199        ))
200    }};
201
202    ($port:expr) => {{
203        use chrono;
204
205        let path_port = $crate::path_utils::get_compat_port_path($port)?;
206        PathBuf::from(format!(
207            "./{}-{}.txt",
208            path_port.display(),
209            chrono::Utc::now().format("%m%d%H%M"),
210        ))
211    }};
212}
213
214#[doc(hidden)]
215pub fn get_compat_port_path<S>(port: S) -> miette::Result<std::path::PathBuf>
216where
217    S: Into<std::path::PathBuf>,
218{
219    #[cfg(windows)]
220    {
221        Ok(port.into())
222    }
223    #[cfg(unix)]
224    {
225        use miette::{self, WrapErr};
226        use std::path::PathBuf;
227
228        let p: PathBuf = port.into();
229        Ok(PathBuf::from(p.file_name().ok_or(std::io::ErrorKind::InvalidFilename)
230            .map_err(|e| miette::miette!(
231                help = format!("The name of the tracing file is tied to the port being opened, make sure you are using a valid port."),
232                "{e}: '{}'\n",
233                p.display()
234            )).wrap_err_with(|| format!("Could not create file: '{}' for tracing output.\n", p.display()))?))
235    }
236}
237
238/// Macro to join a path to the user's home directory and
239/// check whether it exists.
240///
241/// Used for building the $XDG base directories.
242///
243/// Returns `None` if the joined path doesn't exist.
244macro_rules! push_n_check {
245    ($home:expr, $push:literal) => {
246        $home.push($push);
247        if !$home.exists() {
248            return None;
249        }
250    };
251}
252
253/// Macro to expand a path like shell expansion.
254///
255/// - On unix, this handles the $XDG base directories and `~`
256/// - On Windows, this handles %USERPROFILE%, %APPDATA%, etc.
257///
258/// Returns `None` if unable to retrieve the user's [`home_dir`]
259///
260/// [`home_dir`]: std::env::home_dir()
261macro_rules! expand_path {
262    ($self:ident, $expand:literal, to = $expand_to:literal) => {{
263        use std::{env, path::PathBuf};
264
265        if $self.starts_with($expand) {
266            let mut home = env::home_dir()?;
267            let expanded: PathBuf = $self.components().skip(1).collect();
268            push_n_check!(home, $expand_to);
269            $self = home.join(expanded);
270        }
271    }};
272
273    ($self:ident, $expand:literal) => {{
274        use std::{env, path::PathBuf};
275
276        if $self.starts_with($expand) {
277            let home = env::home_dir()?;
278            let expanded: PathBuf = $self.components().skip(1).collect();
279            $self = home.join(expanded);
280        }
281    }};
282}
283
284/// For use with Windows Env variables for path expansions
285#[cfg(windows)]
286macro_rules! expand_env_path {
287    ($self:ident, $expand:literal, env_var = $env_var:literal) => {{
288        use std::{env, path::PathBuf};
289
290        if $self.starts_with($expand) {
291            if let Ok(base_path) = env::var($env_var) {
292                let expanded: PathBuf = $self.components().skip(1).collect();
293                $self = PathBuf::from(base_path).join(expanded)
294            }
295        }
296    }};
297}
298
299/// Trait for expanding paths in a shell-like way.
300pub trait ExpandPaths {
301    /// Expands path in a shell-like way
302    ///
303    /// Takes ownership of the implementor and returns `Some(PathBuf)` if
304    /// self can successfully expand the path; otherwise, returns `None`.
305    fn get_expanded_path(self) -> Option<std::path::PathBuf>;
306}
307
308impl ExpandPaths for std::path::PathBuf {
309    /// Expands path in a shell-like way
310    ///
311    /// Returns `None` if fails to retrieve the user's home dir.
312    #[cfg(unix)]
313    fn get_expanded_path(mut self) -> Option<Self> {
314        expand_path!(self, "~");
315        expand_path!(self, "$HOME");
316        expand_path!(self, "$XDG_CACHE_HOME", to = ".cache");
317        expand_path!(self, "$XDG_CONFIG_HOME", to = ".config");
318        expand_path!(self, "$XDG_DATA_HOME", to = ".local/share");
319        expand_path!(self, "$XDG_DESKTOP_DIR", to = "Desktop");
320        expand_path!(self, "$XDG_DOCUMENTS_DIR", to = "Documents");
321        expand_path!(self, "$XDG_DOWNLOAD_DIR", to = "Downloads");
322        expand_path!(self, "$XDG_MUSIC_DIR", to = "Music");
323        expand_path!(self, "$XDG_PICTURES_DIR", to = "Pictures");
324        expand_path!(self, "$XDG_PUBLICSHARE_DIR", to = "Public");
325        expand_path!(self, "$XDG_STATE_HOME", to = ".local/state");
326        expand_path!(self, "$XDG_TEMPLATES_DIR", to = "Templates");
327        Some(self)
328    }
329    #[cfg(windows)]
330    fn get_expanded_path(mut self) -> Option<Self> {
331        // Basic home directory expansion
332        expand_path!(self, "~");
333        expand_env_path!(self, "%USERPROFILE%", env_var = "USERPROFILE");
334        expand_env_path!(self, "%APPDATA%", env_var = "APPDATA");
335        expand_env_path!(self, "%LOCALAPPDATA%", env_var = "LOCALAPPDATA");
336        expand_env_path!(self, "%TEMP%", env_var = "TEMP");
337        expand_env_path!(self, "%TMP%", env_var = "TMP");
338        expand_path!(self, "%DESKTOP%", to = "Desktop");
339        expand_path!(self, "%DOCUMENTS%", to = "Documents");
340        expand_path!(self, "%DOWNLOADS%", to = "Downloads");
341        expand_path!(self, "%MUSIC%", to = "Music");
342        expand_path!(self, "%PICTURES%", to = "Pictures");
343        expand_path!(self, "%VIDEOS%", to = "Videos");
344        expand_path!(self, "%PUBLIC%", to = "Public");
345        Some(self)
346    }
347}