Skip to main content

git_worktree_manager/
constants.rs

1/// Constants and default values for git-worktree-manager.
2///
3/// Mirrors src/git_worktree_manager/constants.py.
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9
10/// Terminal launch methods for AI tool execution.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum LaunchMethod {
14    Foreground,
15    Detach,
16    // iTerm (macOS)
17    ItermWindow,
18    ItermTab,
19    ItermPaneH,
20    ItermPaneV,
21    // tmux
22    Tmux,
23    TmuxWindow,
24    TmuxPaneH,
25    TmuxPaneV,
26    // Zellij
27    Zellij,
28    ZellijTab,
29    ZellijPaneH,
30    ZellijPaneV,
31    // WezTerm
32    WeztermWindow,
33    WeztermTab,
34    WeztermPaneH,
35    WeztermPaneV,
36}
37
38impl LaunchMethod {
39    /// Convert to the canonical kebab-case string.
40    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    /// Parse from a kebab-case string.
64    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
95/// Build the alias map for launch methods.
96/// First letter: i=iTerm, t=tmux, z=Zellij, w=WezTerm
97/// Second: w=window, t=tab, p=pane
98/// For panes: h=horizontal, v=vertical
99pub fn launch_method_aliases() -> HashMap<&'static str, &'static str> {
100    HashMap::from([
101        ("fg", "foreground"),
102        ("d", "detach"),
103        // iTerm
104        ("i-w", "iterm-window"),
105        ("i-t", "iterm-tab"),
106        ("i-p-h", "iterm-pane-h"),
107        ("i-p-v", "iterm-pane-v"),
108        // tmux
109        ("t", "tmux"),
110        ("t-w", "tmux-window"),
111        ("t-p-h", "tmux-pane-h"),
112        ("t-p-v", "tmux-pane-v"),
113        // Zellij
114        ("z", "zellij"),
115        ("z-t", "zellij-tab"),
116        ("z-p-h", "zellij-pane-h"),
117        ("z-p-v", "zellij-pane-v"),
118        // WezTerm
119        ("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
126/// Maximum session name length for tmux/zellij compatibility.
127/// Zellij uses Unix sockets which have a ~108 byte path limit.
128pub const MAX_SESSION_NAME_LENGTH: usize = 50;
129
130/// Claude native session path prefix length threshold.
131pub const CLAUDE_SESSION_PREFIX_LENGTH: usize = 200;
132
133/// Git config key templates for metadata storage.
134pub 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
138/// Format a git config key by replacing `{}` with the branch name.
139pub fn format_config_key(template: &str, branch: &str) -> String {
140    template.replace("{}", branch)
141}
142
143/// Convert branch name to safe directory name.
144///
145/// Handles branch names with slashes (feat/auth), special characters,
146/// and other filesystem-unsafe characters.
147///
148/// # Examples
149/// ```
150/// use git_worktree_manager::constants::sanitize_branch_name;
151/// assert_eq!(sanitize_branch_name("feat/auth"), "feat-auth");
152/// assert_eq!(sanitize_branch_name("feature/user@login"), "feature-user-login");
153/// assert_eq!(sanitize_branch_name("hotfix/v2.0"), "hotfix-v2.0");
154/// ```
155pub fn sanitize_branch_name(branch_name: &str) -> String {
156    // Characters unsafe for directory names across platforms
157    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
173/// Generate default worktree path: `../<repo>-<branch>`.
174pub 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
192/// Strip Windows UNC path prefix (`\\?\`) which `canonicalize()` adds.
193/// Git doesn't understand UNC paths, so we must strip them.
194pub 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}