1use std::process::Command;
7
8pub fn sanitize_name(name: &str) -> String {
10 let replaced: String = name
11 .chars()
12 .map(|c| if matches!(c, '.' | ':' | '/') { '-' } else { c })
13 .collect();
14
15 let collapsed = replaced
17 .split('-')
18 .filter(|s| !s.is_empty())
19 .collect::<Vec<_>>()
20 .join("-");
21
22 if collapsed.len() <= 30 {
24 collapsed
25 } else {
26 match collapsed[..30].rfind('-') {
27 Some(pos) if pos > 10 => collapsed[..pos].to_string(),
28 _ => collapsed[..30].to_string(),
29 }
30 }
31}
32
33pub fn git_branch(dir: &str) -> Option<String> {
35 let output = Command::new("git")
36 .args(["-C", dir, "rev-parse", "--abbrev-ref", "HEAD"])
37 .env_remove("GIT_DIR")
38 .env_remove("GIT_WORK_TREE")
39 .output()
40 .ok()?;
41
42 if !output.status.success() {
43 return None;
44 }
45
46 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
47 if branch.is_empty() {
48 None
49 } else {
50 Some(branch)
51 }
52}
53
54pub fn is_worktree(dir: &str) -> bool {
56 let git_dir = Command::new("git")
57 .args(["-C", dir, "rev-parse", "--git-dir"])
58 .env_remove("GIT_DIR")
59 .env_remove("GIT_WORK_TREE")
60 .output()
61 .ok();
62 let common_dir = Command::new("git")
63 .args(["-C", dir, "rev-parse", "--git-common-dir"])
64 .env_remove("GIT_DIR")
65 .env_remove("GIT_WORK_TREE")
66 .output()
67 .ok();
68
69 match (git_dir, common_dir) {
70 (Some(gd), Some(cd)) if gd.status.success() && cd.status.success() => {
71 let gd_str = String::from_utf8_lossy(&gd.stdout).trim().to_string();
72 let cd_str = String::from_utf8_lossy(&cd.stdout).trim().to_string();
73 gd_str != cd_str
76 }
77 _ => false,
78 }
79}
80
81pub fn build_window_name(base: &str, dir: &str) -> String {
84 let branch = git_branch(dir);
85 let wt = is_worktree(dir);
86
87 match branch {
88 Some(branch) => {
89 let raw = format!("{base}-{branch}");
90 let name = sanitize_name(&raw);
91 if wt { format!("{name}(wt)") } else { name }
92 }
93 None => sanitize_name(base),
94 }
95}
96
97#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn simple_name() {
105 assert_eq!(sanitize_name("cove-main"), "cove-main");
106 }
107
108 #[test]
109 fn replaces_dots_colons_slashes() {
110 assert_eq!(sanitize_name("feature/add-auth"), "feature-add-auth");
111 assert_eq!(sanitize_name("v1.2.3"), "v1-2-3");
112 assert_eq!(sanitize_name("host:port"), "host-port");
113 }
114
115 #[test]
116 fn collapses_consecutive_dashes() {
117 assert_eq!(sanitize_name("a--b---c"), "a-b-c");
118 assert_eq!(sanitize_name("a/../b"), "a-b");
119 }
120
121 #[test]
122 fn strips_leading_trailing_dashes() {
123 assert_eq!(sanitize_name("-hello-"), "hello");
124 assert_eq!(sanitize_name("---test---"), "test");
125 }
126
127 #[test]
128 fn truncates_long_names() {
129 let long = "a".repeat(50);
130 let result = sanitize_name(&long);
131 assert!(result.len() <= 30);
132 }
133
134 #[test]
135 fn truncates_on_dash_boundary() {
136 let name = "abcdefghijklmno-pqrstuvwxyz-12345";
138 let result = sanitize_name(name);
139 assert!(result.len() <= 30);
140 assert_eq!(result, "abcdefghijklmno-pqrstuvwxyz");
142 }
143
144 #[test]
145 fn empty_string() {
146 assert_eq!(sanitize_name(""), "");
147 }
148
149 #[test]
150 fn only_special_chars() {
151 assert_eq!(sanitize_name("..."), "");
152 }
153
154 #[test]
155 fn build_name_no_git() {
156 let name = build_window_name("myapp", "/tmp");
158 assert_eq!(name, "myapp");
159 }
160}