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 .output()
38 .ok()?;
39
40 if !output.status.success() {
41 return None;
42 }
43
44 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
45 if branch.is_empty() {
46 None
47 } else {
48 Some(branch)
49 }
50}
51
52pub fn is_worktree(dir: &str) -> bool {
54 let git_dir = Command::new("git")
55 .args(["-C", dir, "rev-parse", "--git-dir"])
56 .output()
57 .ok();
58 let common_dir = Command::new("git")
59 .args(["-C", dir, "rev-parse", "--git-common-dir"])
60 .output()
61 .ok();
62
63 match (git_dir, common_dir) {
64 (Some(gd), Some(cd)) if gd.status.success() && cd.status.success() => {
65 let gd_str = String::from_utf8_lossy(&gd.stdout).trim().to_string();
66 let cd_str = String::from_utf8_lossy(&cd.stdout).trim().to_string();
67 gd_str != cd_str
70 }
71 _ => false,
72 }
73}
74
75pub fn build_window_name(base: &str, dir: &str) -> String {
78 let branch = git_branch(dir);
79 let wt = is_worktree(dir);
80
81 match branch {
82 Some(branch) => {
83 let raw = format!("{base}-{branch}");
84 let name = sanitize_name(&raw);
85 if wt { format!("{name}(wt)") } else { name }
86 }
87 None => sanitize_name(base),
88 }
89}
90
91#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn simple_name() {
99 assert_eq!(sanitize_name("cove-main"), "cove-main");
100 }
101
102 #[test]
103 fn replaces_dots_colons_slashes() {
104 assert_eq!(sanitize_name("feature/add-auth"), "feature-add-auth");
105 assert_eq!(sanitize_name("v1.2.3"), "v1-2-3");
106 assert_eq!(sanitize_name("host:port"), "host-port");
107 }
108
109 #[test]
110 fn collapses_consecutive_dashes() {
111 assert_eq!(sanitize_name("a--b---c"), "a-b-c");
112 assert_eq!(sanitize_name("a/../b"), "a-b");
113 }
114
115 #[test]
116 fn strips_leading_trailing_dashes() {
117 assert_eq!(sanitize_name("-hello-"), "hello");
118 assert_eq!(sanitize_name("---test---"), "test");
119 }
120
121 #[test]
122 fn truncates_long_names() {
123 let long = "a".repeat(50);
124 let result = sanitize_name(&long);
125 assert!(result.len() <= 30);
126 }
127
128 #[test]
129 fn truncates_on_dash_boundary() {
130 let name = "abcdefghijklmno-pqrstuvwxyz-12345";
132 let result = sanitize_name(name);
133 assert!(result.len() <= 30);
134 assert_eq!(result, "abcdefghijklmno-pqrstuvwxyz");
136 }
137
138 #[test]
139 fn empty_string() {
140 assert_eq!(sanitize_name(""), "");
141 }
142
143 #[test]
144 fn only_special_chars() {
145 assert_eq!(sanitize_name("..."), "");
146 }
147
148 #[test]
149 fn build_name_no_git() {
150 let name = build_window_name("myapp", "/tmp");
152 assert_eq!(name, "myapp");
153 }
154}