1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct WorkspaceStatus {
6 pub display_dir: String,
7 pub git_ref: Option<String>,
8}
9
10impl WorkspaceStatus {
11 pub fn new(display_dir: impl Into<String>, git_ref: Option<String>) -> Self {
12 Self { display_dir: display_dir.into(), git_ref }
13 }
14
15 pub fn label(&self) -> String {
16 self.git_ref
17 .as_ref()
18 .map_or_else(|| self.display_dir.clone(), |git_ref| format!("{} · {git_ref}", self.display_dir))
19 }
20
21 pub fn resolve(cwd: &Path) -> Self {
22 let display_dir = home_relative_path(cwd);
23 let git_ref = resolve_git_ref(cwd);
24 Self::new(display_dir, git_ref)
25 }
26}
27
28fn resolve_git_ref(cwd: &Path) -> Option<String> {
29 if let Some(branch) = git_stdout(cwd, &["branch", "--show-current"]) {
30 return Some(branch);
31 }
32 git_stdout(cwd, &["rev-parse", "--short", "HEAD"])
33}
34
35fn git_stdout(cwd: &Path, args: &[&str]) -> Option<String> {
36 let output = Command::new("git").args(args).current_dir(cwd).output().ok()?;
37
38 if !output.status.success() {
39 return None;
40 }
41
42 let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
43 if text.is_empty() { None } else { Some(text) }
44}
45
46pub fn home_relative_path(path: &Path) -> String {
47 home_dir().map_or_else(|| path.display().to_string(), |home| home_relative_path_with_home(path, &home))
48}
49
50fn home_dir() -> Option<PathBuf> {
51 std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).map(PathBuf::from)
52}
53
54fn home_relative_path_with_home(path: &Path, home: &Path) -> String {
55 if path == home {
56 return "~".to_string();
57 }
58
59 path.strip_prefix(home)
60 .ok()
61 .filter(|relative| !relative.as_os_str().is_empty())
62 .map_or_else(|| path.display().to_string(), |relative| format!("~/{}", relative.display()))
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn label_combines_dir_and_ref() {
71 let status = WorkspaceStatus::new("~/code/aether-2", Some("main".to_string()));
72 assert_eq!(status.label(), "~/code/aether-2 · main");
73 }
74
75 #[test]
76 fn label_omits_ref_when_absent() {
77 let status = WorkspaceStatus::new("~/scratch", None);
78 assert_eq!(status.label(), "~/scratch");
79 }
80
81 #[test]
82 fn home_relative_path_rewrites_home_child() {
83 let path = Path::new("/Users/josh/code/aether-2");
84 let home = Path::new("/Users/josh");
85 assert_eq!(home_relative_path_with_home(path, home), "~/code/aether-2");
86 }
87
88 #[test]
89 fn home_relative_path_handles_home_itself() {
90 let home = Path::new("/Users/josh");
91 assert_eq!(home_relative_path_with_home(home, home), "~");
92 }
93
94 #[test]
95 fn home_relative_path_leaves_external_path_absolute() {
96 let path = Path::new("/opt/work/aether-2");
97 let home = Path::new("/Users/josh");
98 assert_eq!(home_relative_path_with_home(path, home), "/opt/work/aether-2");
99 }
100}