1use std::borrow::Cow;
2use std::env;
3use std::path::PathBuf;
4
5use eyre::{eyre, Result};
6
7use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
8use getrandom::getrandom;
9use uuid::Uuid;
10
11pub fn crypto_random_bytes<const N: usize>() -> [u8; N] {
13 let mut ret = [0u8; N];
16
17 getrandom(&mut ret).expect("Failed to generate random bytes!");
18
19 ret
20}
21
22pub fn crypto_random_string<const N: usize>() -> String {
24 let bytes = crypto_random_bytes::<N>();
25
26 BASE64_URL_SAFE_NO_PAD.encode(bytes)
29}
30
31pub fn uuid_v7() -> Uuid {
32 Uuid::now_v7()
33}
34
35pub fn uuid_v4() -> String {
36 Uuid::new_v4().as_simple().to_string()
37}
38
39pub fn has_git_dir(path: &str) -> bool {
40 let mut gitdir = PathBuf::from(path);
41 gitdir.push(".git");
42
43 gitdir.exists()
44}
45
46pub fn in_git_repo(path: &str) -> Option<PathBuf> {
50 let mut gitdir = PathBuf::from(path);
51
52 while gitdir.parent().is_some() && !has_git_dir(gitdir.to_str().unwrap()) {
53 gitdir.pop();
54 }
55
56 if gitdir.parent().is_some() {
58 return Some(gitdir);
59 }
60
61 None
62}
63
64#[cfg(not(target_os = "windows"))]
69pub fn home_dir() -> PathBuf {
70 let home = std::env::var("HOME").expect("$HOME not found");
71 PathBuf::from(home)
72}
73
74#[cfg(target_os = "windows")]
75pub fn home_dir() -> PathBuf {
76 let home = std::env::var("USERPROFILE").expect("%userprofile% not found");
77 PathBuf::from(home)
78}
79
80pub fn config_dir() -> PathBuf {
81 let config_dir =
82 std::env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
83 config_dir.join("atuin")
84}
85
86pub fn data_dir() -> PathBuf {
87 let data_dir = std::env::var("XDG_DATA_HOME")
88 .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
89
90 data_dir.join("atuin")
91}
92
93pub fn runtime_dir() -> PathBuf {
94 std::env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from)
95}
96
97pub fn dotfiles_cache_dir() -> PathBuf {
98 let data_dir = std::env::var("XDG_DATA_HOME")
100 .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
101
102 data_dir.join("atuin").join("dotfiles").join("cache")
103}
104
105pub fn get_current_dir() -> String {
106 match env::var("PWD") {
108 Ok(v) => v,
109 Err(_) => match env::current_dir() {
110 Ok(dir) => dir.display().to_string(),
111 Err(_) => String::from(""),
112 },
113 }
114}
115
116pub fn is_zsh() -> bool {
117 env::var("ATUIN_SHELL_ZSH").is_ok()
119}
120
121pub fn is_fish() -> bool {
122 env::var("ATUIN_SHELL_FISH").is_ok()
124}
125
126pub fn is_bash() -> bool {
127 env::var("ATUIN_SHELL_BASH").is_ok()
129}
130
131pub fn is_xonsh() -> bool {
132 env::var("ATUIN_SHELL_XONSH").is_ok()
134}
135
136pub trait Escapable: AsRef<str> {
143 fn escape_control(&self) -> Cow<str> {
144 if !self.as_ref().contains(|c: char| c.is_ascii_control()) {
145 self.as_ref().into()
146 } else {
147 let mut remaining = self.as_ref();
148 let mut buf = String::with_capacity(remaining.as_bytes().len());
150 while let Some(i) = remaining.find(|c: char| c.is_ascii_control()) {
151 buf.push_str(&remaining[..i]);
153 buf.push('^');
154 buf.push(match remaining.as_bytes()[i] {
155 0x7F => '?',
156 code => char::from_u32(u32::from(code) + 64).unwrap(),
157 });
158 remaining = &remaining[i + 1..];
159 }
160 buf.push_str(remaining);
161 buf.into()
162 }
163 }
164}
165
166pub fn unquote(s: &str) -> Result<String> {
167 if s.chars().count() < 2 {
168 return Err(eyre!("not enough chars"));
169 }
170
171 let quote = s.chars().next().unwrap();
172
173 if quote != '"' && quote != '\'' && quote != '`' {
175 return Ok(s.to_string());
176 }
177
178 if s.chars().last().unwrap() != quote {
179 return Err(eyre!("unexpected eof, quotes do not match"));
180 }
181
182 let s = &s[1..s.len() - 1];
186
187 Ok(s.to_string())
188}
189
190impl<T: AsRef<str>> Escapable for T {}
191
192#[cfg(test)]
193mod tests {
194 use pretty_assertions::assert_ne;
195 use time::Month;
196
197 use super::*;
198 use std::env;
199
200 use std::collections::HashSet;
201
202 #[cfg(not(windows))]
203 #[test]
204 fn test_dirs() {
205 test_config_dir_xdg();
207 test_config_dir();
208 test_data_dir_xdg();
209 test_data_dir();
210 }
211
212 fn test_config_dir_xdg() {
213 env::remove_var("HOME");
214 env::set_var("XDG_CONFIG_HOME", "/home/user/custom_config");
215 assert_eq!(
216 config_dir(),
217 PathBuf::from("/home/user/custom_config/atuin")
218 );
219 env::remove_var("XDG_CONFIG_HOME");
220 }
221
222 fn test_config_dir() {
223 env::set_var("HOME", "/home/user");
224 env::remove_var("XDG_CONFIG_HOME");
225
226 assert_eq!(config_dir(), PathBuf::from("/home/user/.config/atuin"));
227
228 env::remove_var("HOME");
229 }
230
231 fn test_data_dir_xdg() {
232 env::remove_var("HOME");
233 env::set_var("XDG_DATA_HOME", "/home/user/custom_data");
234 assert_eq!(data_dir(), PathBuf::from("/home/user/custom_data/atuin"));
235 env::remove_var("XDG_DATA_HOME");
236 }
237
238 fn test_data_dir() {
239 env::set_var("HOME", "/home/user");
240 env::remove_var("XDG_DATA_HOME");
241 assert_eq!(data_dir(), PathBuf::from("/home/user/.local/share/atuin"));
242 env::remove_var("HOME");
243 }
244
245 #[test]
246 fn days_from_month() {
247 assert_eq!(time::util::days_in_year_month(2023, Month::January), 31);
248 assert_eq!(time::util::days_in_year_month(2023, Month::February), 28);
249 assert_eq!(time::util::days_in_year_month(2023, Month::March), 31);
250 assert_eq!(time::util::days_in_year_month(2023, Month::April), 30);
251 assert_eq!(time::util::days_in_year_month(2023, Month::May), 31);
252 assert_eq!(time::util::days_in_year_month(2023, Month::June), 30);
253 assert_eq!(time::util::days_in_year_month(2023, Month::July), 31);
254 assert_eq!(time::util::days_in_year_month(2023, Month::August), 31);
255 assert_eq!(time::util::days_in_year_month(2023, Month::September), 30);
256 assert_eq!(time::util::days_in_year_month(2023, Month::October), 31);
257 assert_eq!(time::util::days_in_year_month(2023, Month::November), 30);
258 assert_eq!(time::util::days_in_year_month(2023, Month::December), 31);
259
260 assert_eq!(time::util::days_in_year_month(2024, Month::February), 29);
262 }
263
264 #[test]
265 fn uuid_is_unique() {
266 let how_many: usize = 1000000;
267
268 let mut uuids: HashSet<Uuid> = HashSet::with_capacity(how_many);
270
271 for _ in 0..how_many {
273 let uuid = uuid_v7();
274 uuids.insert(uuid);
275 }
276
277 assert_eq!(uuids.len(), how_many);
278 }
279
280 #[test]
281 fn escape_control_characters() {
282 use super::Escapable;
283 assert_eq!("\x1b[31mfoo".escape_control(), "^[[31mfoo");
285
286 assert_eq!("foo\tbar".escape_control(), "foo^Ibar");
288
289 assert_eq!("two words".escape_control(), "two words");
291
292 let s = "🐢\x1b[32m🦀";
294 assert_eq!(s.escape_control(), s.replace("\x1b", "^["));
295 }
296
297 #[test]
298 fn escape_no_control_characters() {
299 use super::Escapable as _;
300 assert!(matches!(
301 "no control characters".escape_control(),
302 Cow::Borrowed(_)
303 ));
304 assert!(matches!(
305 "with \x1b[31mcontrol\x1b[0m characters".escape_control(),
306 Cow::Owned(_)
307 ));
308 }
309
310 #[test]
311 fn dumb_random_test() {
312 assert_ne!(crypto_random_string::<1>(), crypto_random_string::<1>());
316 assert_ne!(crypto_random_string::<2>(), crypto_random_string::<2>());
317 assert_ne!(crypto_random_string::<4>(), crypto_random_string::<4>());
318 assert_ne!(crypto_random_string::<8>(), crypto_random_string::<8>());
319 assert_ne!(crypto_random_string::<16>(), crypto_random_string::<16>());
320 assert_ne!(crypto_random_string::<32>(), crypto_random_string::<32>());
321 }
322}