1use std::borrow::Cow;
2use std::env;
3use std::path::PathBuf;
4
5use eyre::{Result, eyre};
6
7use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
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 broken_symlink<P: Into<PathBuf>>(path: P) -> bool {
117 let path = path.into();
118 path.is_symlink() && !path.exists()
119}
120
121pub fn is_zsh() -> bool {
122 env::var("ATUIN_SHELL_ZSH").is_ok()
124}
125
126pub fn is_fish() -> bool {
127 env::var("ATUIN_SHELL_FISH").is_ok()
129}
130
131pub fn is_bash() -> bool {
132 env::var("ATUIN_SHELL_BASH").is_ok()
134}
135
136pub fn is_xonsh() -> bool {
137 env::var("ATUIN_SHELL_XONSH").is_ok()
139}
140
141pub trait Escapable: AsRef<str> {
148 fn escape_control(&self) -> Cow<str> {
149 if !self.as_ref().contains(|c: char| c.is_ascii_control()) {
150 self.as_ref().into()
151 } else {
152 let mut remaining = self.as_ref();
153 let mut buf = String::with_capacity(remaining.len());
155 while let Some(i) = remaining.find(|c: char| c.is_ascii_control()) {
156 buf.push_str(&remaining[..i]);
158 buf.push('^');
159 buf.push(match remaining.as_bytes()[i] {
160 0x7F => '?',
161 code => char::from_u32(u32::from(code) + 64).unwrap(),
162 });
163 remaining = &remaining[i + 1..];
164 }
165 buf.push_str(remaining);
166 buf.into()
167 }
168 }
169}
170
171pub fn unquote(s: &str) -> Result<String> {
172 if s.chars().count() < 2 {
173 return Err(eyre!("not enough chars"));
174 }
175
176 let quote = s.chars().next().unwrap();
177
178 if quote != '"' && quote != '\'' && quote != '`' {
180 return Ok(s.to_string());
181 }
182
183 if s.chars().last().unwrap() != quote {
184 return Err(eyre!("unexpected eof, quotes do not match"));
185 }
186
187 let s = &s[1..s.len() - 1];
191
192 Ok(s.to_string())
193}
194
195impl<T: AsRef<str>> Escapable for T {}
196
197#[allow(unsafe_code)]
198#[cfg(test)]
199mod tests {
200 use pretty_assertions::assert_ne;
201
202 use super::*;
203 use std::env;
204
205 use std::collections::HashSet;
206
207 #[cfg(not(windows))]
208 #[test]
209 fn test_dirs() {
210 test_config_dir_xdg();
212 test_config_dir();
213 test_data_dir_xdg();
214 test_data_dir();
215 }
216
217 fn test_config_dir_xdg() {
218 unsafe { env::remove_var("HOME") };
220 unsafe { env::set_var("XDG_CONFIG_HOME", "/home/user/custom_config") };
222 assert_eq!(
223 config_dir(),
224 PathBuf::from("/home/user/custom_config/atuin")
225 );
226 unsafe { env::remove_var("XDG_CONFIG_HOME") };
228 }
229
230 fn test_config_dir() {
231 unsafe { env::set_var("HOME", "/home/user") };
233 unsafe { env::remove_var("XDG_CONFIG_HOME") };
235
236 assert_eq!(config_dir(), PathBuf::from("/home/user/.config/atuin"));
237
238 unsafe { env::remove_var("HOME") };
240 }
241
242 fn test_data_dir_xdg() {
243 unsafe { env::remove_var("HOME") };
245 unsafe { env::set_var("XDG_DATA_HOME", "/home/user/custom_data") };
247 assert_eq!(data_dir(), PathBuf::from("/home/user/custom_data/atuin"));
248 unsafe { env::remove_var("XDG_DATA_HOME") };
250 }
251
252 fn test_data_dir() {
253 unsafe { env::set_var("HOME", "/home/user") };
255 unsafe { env::remove_var("XDG_DATA_HOME") };
257 assert_eq!(data_dir(), PathBuf::from("/home/user/.local/share/atuin"));
258 unsafe { env::remove_var("HOME") };
260 }
261
262 #[test]
263 fn uuid_is_unique() {
264 let how_many: usize = 1000000;
265
266 let mut uuids: HashSet<Uuid> = HashSet::with_capacity(how_many);
268
269 for _ in 0..how_many {
271 let uuid = uuid_v7();
272 uuids.insert(uuid);
273 }
274
275 assert_eq!(uuids.len(), how_many);
276 }
277
278 #[test]
279 fn escape_control_characters() {
280 use super::Escapable;
281 assert_eq!("\x1b[31mfoo".escape_control(), "^[[31mfoo");
283
284 assert_eq!("foo\tbar".escape_control(), "foo^Ibar");
286
287 assert_eq!("two words".escape_control(), "two words");
289
290 let s = "🐢\x1b[32m🦀";
292 assert_eq!(s.escape_control(), s.replace("\x1b", "^["));
293 }
294
295 #[test]
296 fn escape_no_control_characters() {
297 use super::Escapable as _;
298 assert!(matches!(
299 "no control characters".escape_control(),
300 Cow::Borrowed(_)
301 ));
302 assert!(matches!(
303 "with \x1b[31mcontrol\x1b[0m characters".escape_control(),
304 Cow::Owned(_)
305 ));
306 }
307
308 #[test]
309 fn dumb_random_test() {
310 assert_ne!(crypto_random_string::<1>(), crypto_random_string::<1>());
314 assert_ne!(crypto_random_string::<2>(), crypto_random_string::<2>());
315 assert_ne!(crypto_random_string::<4>(), crypto_random_string::<4>());
316 assert_ne!(crypto_random_string::<8>(), crypto_random_string::<8>());
317 assert_ne!(crypto_random_string::<16>(), crypto_random_string::<16>());
318 assert_ne!(crypto_random_string::<32>(), crypto_random_string::<32>());
319 }
320}