aboutsummaryrefslogtreecommitdiffstats
path: root/crates/client/src/atuin_common/utils.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 22:05:46 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 22:05:46 +0200
commit92a8fd40209c5595475fb0288741d0347fe244c4 (patch)
treebc6a275c32bfe9f71dc98bd39146c82938631c82 /crates/client/src/atuin_common/utils.rs
parentchore: Add more things (diff)
downloadatuin-92a8fd40209c5595475fb0288741d0347fe244c4.zip
chore: Move more stuff out of atuin-client
Diffstat (limited to 'crates/client/src/atuin_common/utils.rs')
-rw-r--r--crates/client/src/atuin_common/utils.rs257
1 files changed, 0 insertions, 257 deletions
diff --git a/crates/client/src/atuin_common/utils.rs b/crates/client/src/atuin_common/utils.rs
deleted file mode 100644
index dbe9dfbc..00000000
--- a/crates/client/src/atuin_common/utils.rs
+++ /dev/null
@@ -1,257 +0,0 @@
-use std::borrow::Cow;
-use std::env;
-use std::path::{Path, PathBuf};
-
-use uuid::Uuid;
-
-pub(crate) fn uuid_v7() -> Uuid {
- Uuid::now_v7()
-}
-
-pub(crate) fn has_git_dir(path: &str) -> bool {
- let mut gitdir = PathBuf::from(path);
- gitdir.push(".git");
-
- gitdir.exists()
-}
-
-// in a git worktree, .git is a file containing "gitdir: <path>" pointing
-// to the main repo's .git/worktrees/<name> directory. follow the pointer
-// back to the main repo root so all worktrees share a workspace.
-fn resolve_git_worktree(path: &Path) -> Option<PathBuf> {
- let git_path = path.join(".git");
-
- if !git_path.is_file() {
- return None;
- }
-
- let contents = std::fs::read_to_string(&git_path).ok()?;
- let gitdir_str = contents.strip_prefix("gitdir: ")?.trim();
-
- let gitdir = PathBuf::from(gitdir_str);
- let gitdir = if gitdir.is_absolute() {
- gitdir
- } else {
- path.join(gitdir_str)
- };
-
- // walk up from e.g. /repo/.git/worktrees/feature to find /repo
- let mut candidate = gitdir.as_path();
- while let Some(parent) = candidate.parent() {
- if parent.join(".git").is_dir() {
- return Some(parent.to_path_buf());
- }
- candidate = parent;
- }
-
- None
-}
-
-// detect if any parent dir has a git repo in it
-// I really don't want to bring in libgit for something simple like this
-// If we start to do anything more advanced, then perhaps
-pub(crate) fn in_git_repo(path: &str) -> Option<PathBuf> {
- let mut gitdir = PathBuf::from(path);
-
- while gitdir.parent().is_some() && !has_git_dir(gitdir.to_str().unwrap()) {
- gitdir.pop();
- }
-
- // No parent? then we hit root, finding no git
- if gitdir.parent().is_some() {
- // if .git is a file (worktree), resolve to the main repo root
- if let Some(main_repo) = resolve_git_worktree(&gitdir) {
- return Some(main_repo);
- }
- return Some(gitdir);
- }
-
- None
-}
-
-// TODO: more reliable, more tested
-// I don't want to use ProjectDirs, it puts config in awkward places on
-// mac. Data too. Seems to be more intended for GUI apps.
-
-pub(crate) fn home_dir() -> PathBuf {
- directories::BaseDirs::new()
- .map(|d| d.home_dir().to_path_buf())
- .expect("could not determine home directory")
-}
-
-pub(crate) fn config_dir() -> PathBuf {
- let config_dir =
- env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
- config_dir.join("atuin")
-}
-
-pub(crate) fn data_dir() -> PathBuf {
- let data_dir = env::var("XDG_DATA_HOME")
- .map_or_else(|_| home_dir().join(".local").join("share"), PathBuf::from);
-
- data_dir.join("atuin")
-}
-
-pub(crate) fn runtime_dir() -> PathBuf {
- env::var("XDG_RUNTIME_DIR").map_or_else(|_| data_dir(), PathBuf::from)
-}
-
-pub(crate) fn logs_dir() -> PathBuf {
- home_dir().join(".atuin").join("logs")
-}
-
-pub(crate) fn get_current_dir() -> String {
- // Prefer PWD environment variable over cwd if available to better support symbolic links
- env::var("PWD").unwrap_or_else(|_| {
- env::current_dir().map_or_else(|_| String::new(), |dir| dir.display().to_string())
- })
-}
-
-pub(crate) fn broken_symlink<P: Into<PathBuf>>(path: P) -> bool {
- let path = path.into();
- path.is_symlink() && !path.exists()
-}
-
-/// Extension trait for anything that can behave like a string to make it easy to escape control
-/// characters.
-///
-/// Intended to help prevent control characters being printed and interpreted by the terminal when
-/// printing history as well as to ensure the commands that appear in the interactive search
-/// reflect the actual command run rather than just the printable characters.
-pub(crate) trait Escapable: AsRef<str> {
- fn escape_control(&self) -> Cow<'_, str> {
- if self.as_ref().contains(|c: char| c.is_ascii_control()) {
- let mut remaining = self.as_ref();
- // Not a perfect way to reserve space but should reduce the allocations
- let mut buf = String::with_capacity(remaining.len());
- while let Some(i) = remaining.find(|c: char| c.is_ascii_control()) {
- // safe to index with `..i`, `i` and `i+1..` as part[i] is a single byte ascii char
- buf.push_str(&remaining[..i]);
- buf.push('^');
- buf.push(match remaining.as_bytes()[i] {
- 0x7F => '?',
- code => char::from_u32(u32::from(code) + 64).unwrap(),
- });
- remaining = &remaining[i + 1..];
- }
- buf.push_str(remaining);
- buf.into()
- } else {
- self.as_ref().into()
- }
- }
-}
-
-impl<T: AsRef<str>> Escapable for T {}
-
-#[cfg(test)]
-mod tests {
- use super::{Cow, Escapable, Uuid, env, in_git_repo, uuid_v7};
-
- use std::collections::HashSet;
-
- #[test]
- fn uuid_is_unique() {
- let how_many: usize = 1_000_000;
-
- // for peace of mind
- let mut uuids: HashSet<Uuid> = HashSet::with_capacity(how_many);
-
- // there will be many in the same millisecond
- for _ in 0..how_many {
- let uuid = uuid_v7();
- uuids.insert(uuid);
- }
-
- assert_eq!(uuids.len(), how_many);
- }
-
- #[test]
- fn escape_control_characters() {
- use super::Escapable;
- // CSI colour sequence
- assert_eq!("\x1b[31mfoo".escape_control(), "^[[31mfoo");
-
- // Tabs count as control chars
- assert_eq!("foo\tbar".escape_control(), "foo^Ibar");
-
- // space is in control char range but should be excluded
- assert_eq!("two words".escape_control(), "two words");
-
- // unicode multi-byte characters
- let s = "🐢\x1b[32m🦀";
- assert_eq!(s.escape_control(), s.replace("\x1b", "^["));
- }
-
- #[test]
- fn escape_no_control_characters() {
- use super::Escapable as _;
- assert!(matches!(
- "no control characters".escape_control(),
- Cow::Borrowed(_)
- ));
- assert!(matches!(
- "with \x1b[31mcontrol\x1b[0m characters".escape_control(),
- Cow::Owned(_)
- ));
- }
-
- #[cfg(not(windows))]
- #[test]
- fn in_git_repo_regular() {
- // regular git repo should resolve to the directory containing .git
- let tmp = env::temp_dir().join("atuin-test-regular-git");
- drop(std::fs::remove_dir_all(&tmp));
- let subdir = tmp.join("src").join("deep");
- std::fs::create_dir_all(&subdir).unwrap();
- std::fs::create_dir_all(tmp.join(".git")).unwrap();
-
- let result = in_git_repo(subdir.to_str().unwrap());
- assert_eq!(result, Some(tmp.clone()));
-
- std::fs::remove_dir_all(&tmp).unwrap();
- }
-
- #[cfg(not(windows))]
- #[test]
- fn in_git_repo_worktree_resolves_to_main_repo() {
- // worktree .git is a file pointing back to the main repo —
- // in_git_repo should follow it so all worktrees share a workspace
- let tmp = env::temp_dir().join("atuin-test-worktree-git");
- drop(std::fs::remove_dir_all(&tmp));
-
- // main repo at tmp/main with a real .git directory
- let main_repo = tmp.join("main");
- let worktree_git_dir = main_repo.join(".git").join("worktrees").join("feature");
- std::fs::create_dir_all(&worktree_git_dir).unwrap();
-
- // worktree at tmp/worktree with a .git file
- let worktree = tmp.join("worktree");
- let worktree_subdir = worktree.join("src");
- std::fs::create_dir_all(&worktree_subdir).unwrap();
- std::fs::write(
- worktree.join(".git"),
- format!("gitdir: {}", worktree_git_dir.to_str().unwrap()),
- )
- .unwrap();
-
- // should resolve to the main repo root, not the worktree root
- let result = in_git_repo(worktree_subdir.to_str().unwrap());
- assert_eq!(result, Some(main_repo.clone()));
-
- std::fs::remove_dir_all(&tmp).unwrap();
- }
-
- #[test]
- fn dumb_random_test() {
- // Obviously not a test of randomness, but make sure we haven't made some
- // catastrophic error
-
- assert_ne!(crypto_random_string::<1>(), crypto_random_string::<1>());
- assert_ne!(crypto_random_string::<2>(), crypto_random_string::<2>());
- assert_ne!(crypto_random_string::<4>(), crypto_random_string::<4>());
- assert_ne!(crypto_random_string::<8>(), crypto_random_string::<8>());
- assert_ne!(crypto_random_string::<16>(), crypto_random_string::<16>());
- assert_ne!(crypto_random_string::<32>(), crypto_random_string::<32>());
- }
-}