1#[doc(hidden)]
2pub mod internal {
3
4 use argh::FromArgs;
5 use human_time::ToHumanTimeString;
6 use regex_lite::Regex;
7 use serde::Deserialize;
8 use std::env;
9 use std::fs;
10 use std::path::{Path, PathBuf};
11 use std::time::Duration;
12
13 #[derive(FromArgs)]
15 #[argh(
16 name = "human-time",
17 description = "Converts a time duration to a human-readable format"
18 )]
19 pub struct Args {
20 #[argh(positional)]
22 pub time_value: Option<u64>,
23
24 #[argh(option, short = 'u', long = "unit")]
26 pub unit: Option<String>,
27
28 #[argh(switch, short = 'c', long = "config")]
30 pub config: bool,
31 }
32
33 #[derive(Deserialize)]
34 pub struct Config {
35 pub default_time_value_units: String,
36 pub formatting: Formatting,
37 pub units: Units,
38 }
39
40 #[derive(Deserialize)]
41 pub struct Formatting {
42 pub format: String,
43 pub delimiter_text: String,
44 }
45
46 #[derive(Deserialize)]
47 pub struct Units {
48 pub d: String,
49 pub h: String,
50 pub m: String,
51 pub s: String,
52 pub ms: String,
53 pub us: String,
54 }
55
56 impl Default for Config {
57 fn default() -> Self {
58 Config {
59 default_time_value_units: "seconds".to_string(),
60 formatting: Formatting {
61 format: "{}{}".to_string(),
62 delimiter_text: ",".to_string(),
63 },
64 units: Units {
65 d: "d".to_string(),
66 h: "h".to_string(),
67 m: "m".to_string(),
68 s: "s".to_string(),
69 ms: "ms".to_string(),
70 us: "µs".to_string(),
71 },
72 }
73 }
74 }
75
76 const MILLI_REGEX: &str = r"^(?:milli(?:second|sec)?s?|ms)$";
77 const MICRO_REGEX: &str = r"^micro(?:second|sec)?s?$";
78 const SEC_REGEX: &str = r"^(?:sec(?:ond)?s?|s)$";
79
80 pub fn print_error_and_exit(error_message: &str) -> ! {
81 eprintln!("{error_message}");
82 eprintln!(
83 r#"Usage: human-time-cli [OPTIONS] <TIME_DURATION>
84Options:
85 -u, --unit <UNIT> specify the unit of the time value (milli, micro). If not specified, defaults to seconds.
86 -c, --config specify if there is a config file"#
87 );
88 std::process::exit(1);
89 }
90
91 pub fn validate_config(config: &Config) -> Result<(), String> {
92 let unit = config.default_time_value_units.to_lowercase();
93 let milli_regex = Regex::new(MILLI_REGEX).unwrap();
94 let micro_regex = Regex::new(MICRO_REGEX).unwrap();
95 let sec_regex = Regex::new(SEC_REGEX).unwrap();
96
97 if !(milli_regex.is_match(&unit)
98 || micro_regex.is_match(&unit)
99 || sec_regex.is_match(&unit))
100 {
101 return Err(format!(
102 "Invalid default_time_value_units: {}. Valid options are: milliseconds, microseconds, or seconds.",
103 config.default_time_value_units
104 ));
105 }
106
107 let format = &config.formatting.format;
109 let placeholder_count = format.matches("{}").count();
110 if placeholder_count != 2 {
111 return Err(format!(
112 "Invalid formatting.format: {}. It must contain exactly two sets of {{}}.",
113 format
114 ));
115 }
116
117 Ok(())
118 }
119
120 pub fn convert_time(time_value: u64, unit: Option<&str>) -> Result<Duration, String> {
121 let unit = unit.unwrap_or("sec").to_lowercase();
122
123 let milli_regex = Regex::new(MILLI_REGEX).unwrap();
124 let micro_regex = Regex::new(MICRO_REGEX).unwrap();
125 let sec_regex = Regex::new(SEC_REGEX).unwrap();
126
127 let duration = if milli_regex.is_match(&unit) {
128 Duration::from_millis(time_value)
129 } else if micro_regex.is_match(&unit) {
130 Duration::from_micros(time_value)
131 } else if sec_regex.is_match(&unit) {
132 Duration::from_secs(time_value)
133 } else {
134 return Err(format!(
135 "Invalid unit '{}'. Please specify one of: milli, micro, or leave empty for seconds.",
136 unit
137 ));
138 };
139
140 Ok(duration)
141 }
142
143 pub fn read_config<P: AsRef<Path>>(path: P) -> Config {
144 match fs::read_to_string(&path) {
145 Ok(config_content) => match toml::from_str(&config_content) {
146 Ok(config) => {
147 if let Err(err) = validate_config(&config) {
148 eprintln!("{}", err);
149 std::process::exit(1);
150 }
151 config
152 }
153 Err(_) => {
154 eprintln!("Error: Failed to parse the config file.");
155 std::process::exit(1);
156 }
157 },
158 Err(_) => {
159 eprintln!(
160 "Error: Config file not found at this location: {}",
161 path.as_ref().display()
162 );
163 std::process::exit(1);
164 }
165 }
166 }
167
168 pub fn find_config_file() -> Option<PathBuf> {
169 let exe_path = env::current_exe().ok()?;
170 let exe_dir = exe_path.parent()?;
171 let config_file_name = "human-time.toml";
172
173 let exe_config_path = exe_dir.join(config_file_name);
174 if exe_config_path.exists() {
175 return Some(exe_config_path);
176 }
177
178 let home_dir = dirs::home_dir()?;
179 let home_config_path = home_dir.join(config_file_name);
180 if home_config_path.exists() {
181 return Some(home_config_path);
182 }
183 None
184 }
185
186 pub fn format_duration(time_value: u64, unit: &str, config: &Config) -> Result<String, String> {
187 match convert_time(time_value, Some(unit)) {
188 Ok(duration) => {
189 let formatted_duration = duration.to_human_time_string_with_format(
190 |n, unit| {
191 let unit_str = match unit {
192 "d" => &config.units.d,
193 "h" => &config.units.h,
194 "m" => &config.units.m,
195 "s" => &config.units.s,
196 "ms" => &config.units.ms,
197 _ => &config.units.us,
198 };
199
200 let unit_str = if n == 1 {
201 unit_str.replace("(s)", "")
202 } else {
203 unit_str.replace("(s)", "s")
204 };
205
206 config
207 .formatting
208 .format
209 .replacen("{}", &n.to_string(), 1)
210 .replacen("{}", &unit_str, 1)
211 },
212 |acc, item| format!("{}{}{}", acc, config.formatting.delimiter_text, item),
213 );
214 Ok(formatted_duration)
215 }
216 Err(err) => Err(err),
217 }
218 }
219}
220
221#[doc(hidden)]
223pub use internal::*;