git_warp/
terminal.rs

1use crate::error::{GitWarpError, Result};
2use std::path::Path;
3use std::process::Command;
4
5#[derive(Debug, Clone)]
6pub enum TerminalMode {
7    Tab,
8    Window,
9    InPlace,
10    Echo,
11}
12
13impl TerminalMode {
14    pub fn from_str(s: &str) -> Option<Self> {
15        match s.to_lowercase().as_str() {
16            "tab" => Some(Self::Tab),
17            "window" => Some(Self::Window),
18            "inplace" => Some(Self::InPlace),
19            "echo" => Some(Self::Echo),
20            _ => None,
21        }
22    }
23}
24
25pub trait Terminal {
26    fn open_tab(&self, path: &Path, session_id: Option<&str>) -> Result<()>;
27    fn open_window(&self, path: &Path, session_id: Option<&str>) -> Result<()>;
28    fn switch_to_directory(&self, path: &Path) -> Result<()>;
29    fn echo_commands(&self, path: &Path) -> Result<()>;
30    fn is_supported(&self) -> bool;
31}
32
33#[cfg(target_os = "macos")]
34pub struct ITerm2;
35
36#[cfg(target_os = "macos")]
37impl Terminal for ITerm2 {
38    fn open_tab(&self, path: &Path, _session_id: Option<&str>) -> Result<()> {
39        let script = format!(r#"
40tell application "iTerm"
41    tell current window
42        create tab with default profile
43        tell current tab
44            tell current session
45                write text "cd '{}'"
46            end tell
47        end tell
48    end tell
49end tell
50"#, path.display());
51        
52        self.run_applescript(&script)
53    }
54    
55    fn open_window(&self, path: &Path, _session_id: Option<&str>) -> Result<()> {
56        let script = format!(r#"
57tell application "iTerm"
58    create window with default profile
59    tell current window
60        tell current tab
61            tell current session
62                write text "cd '{}'"
63            end tell
64        end tell
65    end tell
66end tell
67"#, path.display());
68        
69        self.run_applescript(&script)
70    }
71    
72    fn switch_to_directory(&self, path: &Path) -> Result<()> {
73        println!("cd '{}'", path.display());
74        Ok(())
75    }
76    
77    fn echo_commands(&self, path: &Path) -> Result<()> {
78        println!("# Navigate to worktree:");
79        println!("cd '{}'", path.display());
80        Ok(())
81    }
82    
83    fn is_supported(&self) -> bool {
84        // Check if iTerm2 is available
85        Command::new("osascript")
86            .args(&["-e", "tell application \"iTerm\" to get version"])
87            .output()
88            .map(|output| output.status.success())
89            .unwrap_or(false)
90    }
91}
92
93#[cfg(target_os = "macos")]
94impl ITerm2 {
95    fn run_applescript(&self, script: &str) -> Result<()> {
96        let output = Command::new("osascript")
97            .args(&["-e", script])
98            .output()
99            .map_err(|e| anyhow::anyhow!("Failed to execute AppleScript: {}", e))?;
100        
101        if !output.status.success() {
102            let error = String::from_utf8_lossy(&output.stderr);
103            return Err(anyhow::anyhow!("AppleScript failed: {}", error).into());
104        }
105        
106        Ok(())
107    }
108}
109
110#[cfg(target_os = "macos")]
111pub struct AppleTerminal;
112
113#[cfg(target_os = "macos")]
114impl Terminal for AppleTerminal {
115    fn open_tab(&self, path: &Path, _session_id: Option<&str>) -> Result<()> {
116        let script = format!(r#"
117tell application "Terminal"
118    tell window 1
119        do script "cd '{}'" in (make new tab)
120    end tell
121end tell
122"#, path.display());
123        
124        self.run_applescript(&script)
125    }
126    
127    fn open_window(&self, path: &Path, _session_id: Option<&str>) -> Result<()> {
128        let script = format!(r#"
129tell application "Terminal"
130    do script "cd '{}'"
131end tell
132"#, path.display());
133        
134        self.run_applescript(&script)
135    }
136    
137    fn switch_to_directory(&self, path: &Path) -> Result<()> {
138        println!("cd '{}'", path.display());
139        Ok(())
140    }
141    
142    fn echo_commands(&self, path: &Path) -> Result<()> {
143        println!("# Navigate to worktree:");
144        println!("cd '{}'", path.display());
145        Ok(())
146    }
147    
148    fn is_supported(&self) -> bool {
149        true // Terminal.app is always available on macOS
150    }
151}
152
153#[cfg(target_os = "macos")]
154impl AppleTerminal {
155    fn run_applescript(&self, script: &str) -> Result<()> {
156        let output = Command::new("osascript")
157            .args(&["-e", script])
158            .output()
159            .map_err(|e| anyhow::anyhow!("Failed to execute AppleScript: {}", e))?;
160        
161        if !output.status.success() {
162            let error = String::from_utf8_lossy(&output.stderr);
163            return Err(anyhow::anyhow!("AppleScript failed: {}", error).into());
164        }
165        
166        Ok(())
167    }
168}
169
170pub struct TerminalManager;
171
172impl TerminalManager {
173    pub fn get_default_terminal() -> Result<Box<dyn Terminal>> {
174        #[cfg(target_os = "macos")]
175        {
176            let iterm2 = ITerm2;
177            if iterm2.is_supported() {
178                Ok(Box::new(iterm2))
179            } else {
180                Ok(Box::new(AppleTerminal))
181            }
182        }
183        
184        #[cfg(not(target_os = "macos"))]
185        {
186            Err(GitWarpError::TerminalNotSupported.into())
187        }
188    }
189    
190    pub fn switch_to_worktree<P: AsRef<Path>>(
191        &self,
192        path: P,
193        mode: TerminalMode,
194        session_id: Option<&str>,
195    ) -> Result<()> {
196        let terminal = Self::get_default_terminal()?;
197        let path = path.as_ref();
198        
199        match mode {
200            TerminalMode::Tab => terminal.open_tab(path, session_id),
201            TerminalMode::Window => terminal.open_window(path, session_id),
202            TerminalMode::InPlace => terminal.switch_to_directory(path),
203            TerminalMode::Echo => terminal.echo_commands(path),
204        }
205    }
206}