1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum LaunchMethod {
14 Foreground,
15 Detach,
16 ItermWindow,
18 ItermTab,
19 ItermPaneH,
20 ItermPaneV,
21 Tmux,
23 TmuxWindow,
24 TmuxPaneH,
25 TmuxPaneV,
26 Zellij,
28 ZellijTab,
29 ZellijPaneH,
30 ZellijPaneV,
31 WeztermWindow,
33 WeztermTab,
34 WeztermPaneH,
35 WeztermPaneV,
36}
37
38impl LaunchMethod {
39 pub fn as_str(&self) -> &'static str {
41 match self {
42 Self::Foreground => "foreground",
43 Self::Detach => "detach",
44 Self::ItermWindow => "iterm-window",
45 Self::ItermTab => "iterm-tab",
46 Self::ItermPaneH => "iterm-pane-h",
47 Self::ItermPaneV => "iterm-pane-v",
48 Self::Tmux => "tmux",
49 Self::TmuxWindow => "tmux-window",
50 Self::TmuxPaneH => "tmux-pane-h",
51 Self::TmuxPaneV => "tmux-pane-v",
52 Self::Zellij => "zellij",
53 Self::ZellijTab => "zellij-tab",
54 Self::ZellijPaneH => "zellij-pane-h",
55 Self::ZellijPaneV => "zellij-pane-v",
56 Self::WeztermWindow => "wezterm-window",
57 Self::WeztermTab => "wezterm-tab",
58 Self::WeztermPaneH => "wezterm-pane-h",
59 Self::WeztermPaneV => "wezterm-pane-v",
60 }
61 }
62
63 pub fn from_str_opt(s: &str) -> Option<Self> {
65 match s {
66 "foreground" => Some(Self::Foreground),
67 "detach" => Some(Self::Detach),
68 "iterm-window" => Some(Self::ItermWindow),
69 "iterm-tab" => Some(Self::ItermTab),
70 "iterm-pane-h" => Some(Self::ItermPaneH),
71 "iterm-pane-v" => Some(Self::ItermPaneV),
72 "tmux" => Some(Self::Tmux),
73 "tmux-window" => Some(Self::TmuxWindow),
74 "tmux-pane-h" => Some(Self::TmuxPaneH),
75 "tmux-pane-v" => Some(Self::TmuxPaneV),
76 "zellij" => Some(Self::Zellij),
77 "zellij-tab" => Some(Self::ZellijTab),
78 "zellij-pane-h" => Some(Self::ZellijPaneH),
79 "zellij-pane-v" => Some(Self::ZellijPaneV),
80 "wezterm-window" => Some(Self::WeztermWindow),
81 "wezterm-tab" => Some(Self::WeztermTab),
82 "wezterm-pane-h" => Some(Self::WeztermPaneH),
83 "wezterm-pane-v" => Some(Self::WeztermPaneV),
84 _ => None,
85 }
86 }
87}
88
89impl std::fmt::Display for LaunchMethod {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95pub fn launch_method_aliases() -> HashMap<&'static str, &'static str> {
100 HashMap::from([
101 ("fg", "foreground"),
102 ("d", "detach"),
103 ("i-w", "iterm-window"),
105 ("i-t", "iterm-tab"),
106 ("i-p-h", "iterm-pane-h"),
107 ("i-p-v", "iterm-pane-v"),
108 ("t", "tmux"),
110 ("t-w", "tmux-window"),
111 ("t-p-h", "tmux-pane-h"),
112 ("t-p-v", "tmux-pane-v"),
113 ("z", "zellij"),
115 ("z-t", "zellij-tab"),
116 ("z-p-h", "zellij-pane-h"),
117 ("z-p-v", "zellij-pane-v"),
118 ("w-w", "wezterm-window"),
120 ("w-t", "wezterm-tab"),
121 ("w-p-h", "wezterm-pane-h"),
122 ("w-p-v", "wezterm-pane-v"),
123 ])
124}
125
126pub const MAX_SESSION_NAME_LENGTH: usize = 50;
129
130pub const CLAUDE_SESSION_PREFIX_LENGTH: usize = 200;
132
133pub const CONFIG_KEY_BASE_BRANCH: &str = "branch.{}.worktreeBase";
135pub const CONFIG_KEY_BASE_PATH: &str = "worktree.{}.basePath";
136pub const CONFIG_KEY_INTENDED_BRANCH: &str = "worktree.{}.intendedBranch";
137
138pub fn format_config_key(template: &str, branch: &str) -> String {
140 template.replace("{}", branch)
141}
142
143pub fn sanitize_branch_name(branch_name: &str) -> String {
156 let unsafe_re = Regex::new(r#"[/<>:"|?*\\#@&;$`!~%^()\[\]{}=+]+"#).unwrap();
158 let whitespace_re = Regex::new(r"\s+").unwrap();
159 let multi_hyphen_re = Regex::new(r"-+").unwrap();
160
161 let safe = unsafe_re.replace_all(branch_name, "-");
162 let safe = whitespace_re.replace_all(&safe, "-");
163 let safe = multi_hyphen_re.replace_all(&safe, "-");
164 let safe = safe.trim_matches('-');
165
166 if safe.is_empty() {
167 "worktree".to_string()
168 } else {
169 safe.to_string()
170 }
171}
172
173pub fn default_worktree_path(repo_path: &Path, branch_name: &str) -> PathBuf {
175 let repo_path = strip_unc(
176 repo_path
177 .canonicalize()
178 .unwrap_or_else(|_| repo_path.to_path_buf()),
179 );
180 let safe_branch = sanitize_branch_name(branch_name);
181 let repo_name = repo_path
182 .file_name()
183 .map(|n| n.to_string_lossy().to_string())
184 .unwrap_or_else(|| "repo".to_string());
185
186 repo_path
187 .parent()
188 .unwrap_or(repo_path.as_path())
189 .join(format!("{}-{}", repo_name, safe_branch))
190}
191
192pub fn strip_unc(path: PathBuf) -> PathBuf {
195 #[cfg(target_os = "windows")]
196 {
197 let s = path.to_string_lossy();
198 if let Some(stripped) = s.strip_prefix(r"\\?\") {
199 return PathBuf::from(stripped);
200 }
201 }
202 path
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_sanitize_branch_name() {
211 assert_eq!(sanitize_branch_name("feat/auth"), "feat-auth");
212 assert_eq!(sanitize_branch_name("bugfix/issue-123"), "bugfix-issue-123");
213 assert_eq!(
214 sanitize_branch_name("feature/user@login"),
215 "feature-user-login"
216 );
217 assert_eq!(sanitize_branch_name("hotfix/v2.0"), "hotfix-v2.0");
218 assert_eq!(sanitize_branch_name("///"), "worktree");
219 assert_eq!(sanitize_branch_name(""), "worktree");
220 assert_eq!(sanitize_branch_name("simple"), "simple");
221 }
222
223 #[test]
224 fn test_launch_method_roundtrip() {
225 for method in [
226 LaunchMethod::Foreground,
227 LaunchMethod::Detach,
228 LaunchMethod::ItermWindow,
229 LaunchMethod::Tmux,
230 LaunchMethod::Zellij,
231 LaunchMethod::WeztermTab,
232 ] {
233 let s = method.as_str();
234 assert_eq!(LaunchMethod::from_str_opt(s), Some(method));
235 }
236 }
237
238 #[test]
239 fn test_format_config_key() {
240 assert_eq!(
241 format_config_key(CONFIG_KEY_BASE_BRANCH, "fix-auth"),
242 "branch.fix-auth.worktreeBase"
243 );
244 }
245}