dev_utils/
lib.rs

1//! A collection of utility functions for common development tasks, including logging, terminal manipulation, file handling, and more.
2//!
3//! ```toml
4//! [dependencies]
5//! dev_utils = "0.*"  # Add the latest version of this crate
6//! ```
7//!
8//! # Usage
9//!
10//! ```rust
11//! use dev_utils::app_dt;
12//!
13//! app_dt!(file!());  // Print package name and version from Cargo.toml
14//! app_dt!(file!(),  // Print package name, version, license, and keywords
15//!     "package" => ["license", "keywords"]  // selected sections and keys
16//! );
17//! ```
18#![allow(unused)]
19
20pub mod base_change;
21pub mod datetime;
22pub mod dlog;
23pub mod file;
24pub mod format;
25
26use std::fmt::Display;
27use std::io::{self, Write};
28use std::str::FromStr;
29
30// todo: Add the key logger module as an optional feature!
31// // if the feature 'key_logger' is enabled, include the key_logger module
32// // #[cfg(feature = "key_logger")]
33// // pub mod key_logger;
34// // if the feature 'dev_macros' is enabled, include the dev_macros module
35// #[cfg(feature = "dev_macros")]
36// pub mod dev_macros;
37
38// todo: FOR dev_utils: dev_macros/some(mod)
39// todo:     - some custom proc-macro to gen:
40// todo:         - new() fn should have default values for all fields
41// todo:         - new(*args) fn should have custom values for all fields
42
43/// Reads input from the console, optionally displaying a prompt message.
44///
45/// This function can:
46/// - Display a custom prompt message
47/// - Read input until the user presses Enter
48/// - Parse the input into any type that implements FromStr
49/// - Act as a pause mechanism when no prompt is provided
50///
51/// # Type Parameters
52///
53/// - `T`: The type to parse the input into. Must implement FromStr and Default.
54///
55/// # Arguments
56///
57/// - `prompt`: An optional prompt message to display before reading input.
58///
59/// # Returns
60///
61/// - `T`: The parsed input value
62/// - `String`: The raw input string if parsing fails or no parsing is needed
63///
64/// # Examples
65///
66/// ```
67/// let number: i32 = read_input(Some("Enter a number: ")).unwrap();
68/// let name: String = read_input(Some("Enter your name: ")).unwrap();
69/// read_input::<String>(None); // Acts as a pause
70/// ```
71pub fn read_input<T>(prompt: Option<&str>) -> Result<T, String>
72where
73    T: FromStr + Default,
74    <T as FromStr>::Err: Display,
75{
76    if let Some(msg) = prompt {
77        print!("{}", msg);
78        io::stdout().flush().unwrap();
79    }
80
81    let mut input = String::new();
82    io::stdin()
83        .read_line(&mut input)
84        .expect("Failed to read line");
85
86    let trimmed = input.trim();
87
88    if trimmed.is_empty() {
89        return Ok(T::default());
90    }
91
92    trimmed.parse().map_err(|e| format!("Parse error: {}", e))
93}
94
95/// Delays the program execution for the specified number of milliseconds.
96pub fn __delay_ms(ms: u64) {
97    std::thread::sleep(std::time::Duration::from_millis(ms));
98}
99
100/// Module containing helper functions for the print_app_data macro
101pub mod helpers {
102    use std::collections::HashMap;
103    use std::env;
104    use std::fs;
105    use std::io;
106    use std::path::PathBuf;
107
108    use crate::format::{Color, Style, Stylize};
109
110    /// Finds the Cargo.toml file by traversing up the directory tree.
111    pub fn find_cargo_toml(start_path: &str) -> io::Result<PathBuf> {
112        let mut path = PathBuf::from(start_path);
113        loop {
114            path = match path.parent() {
115                Some(parent) => parent.to_path_buf(),
116                None => env::current_dir()?,
117            };
118
119            let cargo_path = path.join("Cargo.toml");
120            if cargo_path.exists() {
121                return Ok(cargo_path);
122            }
123
124            if path.as_os_str().is_empty() {
125                return Err(io::Error::new(
126                    io::ErrorKind::NotFound,
127                    "Cargo.toml not found in any parent directory",
128                ));
129            }
130        }
131    }
132
133    pub fn extract_app_data_with_sections<'a>(
134        data: &'a str,
135        sections: &[(&str, &[&str])],
136    ) -> HashMap<&'a str, HashMap<&'a str, String>> {
137        let mut app_data = HashMap::new();
138        let mut current_section = "";
139        let mut current_key = "";
140        let mut multi_line_value = String::new();
141
142        for line in data.lines() {
143            let trimmed_line = line.trim();
144
145            // Skip empty lines and full-line comments
146            if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
147                continue;
148            }
149
150            // Remove inline comments
151            let line_without_comment = trimmed_line.split('#').next().unwrap().trim();
152
153            if line_without_comment.starts_with('[') && line_without_comment.ends_with(']') {
154                current_section = line_without_comment.trim_matches(&['[', ']'][..]);
155            } else if let Some((key, value)) = line_without_comment.split_once('=') {
156                let key = key.trim();
157                if sections
158                    .iter()
159                    .any(|&(s, keys)| s == current_section && keys.contains(&key))
160                {
161                    let value = value.trim().trim_matches('"');
162                    current_key = key;
163                    if value.starts_with('[') && !value.ends_with(']') {
164                        multi_line_value = value.to_string();
165                    } else {
166                        app_data
167                            .entry(current_section)
168                            .or_insert_with(HashMap::new)
169                            .insert(key, value.to_string());
170                    }
171                }
172            } else if !line_without_comment.is_empty() && !multi_line_value.is_empty() {
173                multi_line_value.push_str(line_without_comment);
174                if line_without_comment.ends_with(']') {
175                    app_data
176                        .entry(current_section)
177                        .or_insert_with(HashMap::new)
178                        .insert(
179                            current_key,
180                            multi_line_value.trim_matches(&['[', ']'][..]).to_string(),
181                        );
182                    multi_line_value.clear();
183                }
184            }
185        }
186
187        app_data
188    }
189
190    pub fn print_extracted_data(
191        app_data: &HashMap<&str, HashMap<&str, String>>,
192        skip_keys: &[&str],
193    ) {
194        for (section, data) in app_data {
195            println!("{}:", section.style(Style::Bold));
196            for (key, value) in data {
197                if !skip_keys.contains(key) {
198                    println!("\t{key}: {}", value.style(Style::Italic).style(Style::Dim));
199                }
200            }
201            println!();
202        }
203    }
204}
205
206#[macro_export]
207macro_rules! app_dt {
208    ($file_path:expr $(, $($section:expr => [$($key:expr),+ $(,)?]),* $(,)?)?) => {{
209        use std::io::Write;
210        use $crate::format::*;
211        use $crate::helpers::{find_cargo_toml, extract_app_data_with_sections, print_extracted_data};
212
213        // Clear the terminal screen
214        print!("\x1B[2J\x1B[1;1H");
215        let _ = std::io::stdout().flush();
216
217        // Find and read Cargo.toml
218        let cargo_toml_path = find_cargo_toml($file_path).expect("Failed to find Cargo.toml");
219        let cargo_toml = std::fs::read_to_string(cargo_toml_path).expect("Failed to read Cargo.toml");
220
221        // Extract all data in a single call
222        let all_data = extract_app_data_with_sections(&cargo_toml, &[
223            ("package", &["name", "version"]),
224            $( $(($section, &[$($key),+])),* )?
225        ]);
226
227        let package_data = all_data.get("package").expect("Failed to extract package data");
228
229        println!("{} v{}\n",
230            package_data.get("name").unwrap().color(Color::new(16, 192, 16)),
231            package_data.get("version").unwrap().color(Color::new(8, 64, 224)).style(Style::Italic),
232        );
233
234        // Only print additional data if any optional fields were provided
235        if all_data.len() > 1 || all_data.get("package").map_or(false, |p| p.len() > 2) {
236            print_extracted_data(&all_data, &["name", "version"]);
237        }
238    }};
239}
240
241// Example usage and testing
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn some_useful_test() {
248        app_dt!(file!()); // Print package name and version from Cargo.toml
249    }
250}