use anyhow::{Context, Result};
use console::style;
use std::path::PathBuf;
use std::process::Command;
use tokio::fs;
use crate::workspace::templates::DEFAULT_ITERMOCIL_TEMPLATE;
use crate::workspace::{Repository, TemplateManager, WorkspaceConfig};
fn is_itermocil_available() -> bool {
Command::new("which")
.arg("itermocil")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn get_itermocil_config_dir() -> PathBuf {
if let Some(home) = dirs::home_dir() {
let itermocil_dir = home.join(".itermocil");
if itermocil_dir.exists() {
return itermocil_dir;
}
home.join(".teamocil")
} else {
PathBuf::from(".itermocil")
}
}
#[allow(dead_code)]
pub async fn open_with_iterm2(
config: &WorkspaceConfig,
repo: &Repository,
template_manager: &TemplateManager,
) -> Result<()> {
open_with_iterm2_options(config, repo, template_manager, false).await
}
pub async fn open_with_iterm2_options(
config: &WorkspaceConfig,
repo: &Repository,
template_manager: &TemplateManager,
no_itermocil: bool,
) -> Result<()> {
let iterm2_integration = config
.apps
.iterm2
.as_ref()
.context("iTerm2 integration is not configured")?;
if !iterm2_integration.enabled {
anyhow::bail!("iTerm2 integration is disabled in configuration");
}
if !no_itermocil && is_itermocil_available() {
println!(
"{} Using iTermocil for advanced pane layout",
style("đ¯").blue()
);
match create_and_launch_itermocil_layout(config, repo, template_manager).await {
Ok(_) => return Ok(()),
Err(e) => {
println!("{} iTermocil launch failed: {}", style("â ī¸").yellow(), e);
println!(
"{} Falling back to Dynamic Profile method",
style("âšī¸").blue()
);
}
}
} else {
if no_itermocil {
println!(
"{} iTermocil disabled by --no-itermocil flag",
style("âšī¸").blue()
);
} else {
println!(
"{} iTermocil not found. Install it for automatic pane layouts:",
style("đĄ").yellow()
);
println!(" brew install TomAnthony/brews/itermocil");
}
println!("{} Using Dynamic Profile instead", style("âšī¸").blue());
}
let template_name = repo
.get_app_template("iterm2")
.unwrap_or(&iterm2_integration.default_template);
let template_content = template_manager
.load_template("iterm2", template_name)
.await
.with_context(|| format!("Failed to load template '{template_name}'"))?;
let variables = TemplateManager::create_variables(config, repo);
let profile_content = template_manager.substitute_variables(&template_content, &variables);
let profile_name = format!("vibe-{}-{}.json", config.workspace.name, repo.name);
let profile_path = iterm2_integration.config_dir.join(&profile_name);
let config_dir = &iterm2_integration.config_dir;
if let Some(parent) = profile_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to create iTerm2 config directory: {}",
parent.display()
)
})?;
} else {
fs::create_dir_all(config_dir).await.with_context(|| {
format!(
"Failed to create iTerm2 config directory: {}",
config_dir.display()
)
})?;
}
fs::write(&profile_path, profile_content)
.await
.with_context(|| format!("Failed to write iTerm2 profile: {}", profile_path.display()))?;
println!(
"{} Created iTerm2 dynamic profile: {}",
style("â
").green(),
style(profile_path.display()).cyan()
);
let profile_guid = format!("vibe-{}-{}", config.workspace.name, repo.name);
let profile_name = format!("{} - {}", config.workspace.name, repo.name);
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let applescript = format!(
r#"tell application "System Events"
if not (exists process "iTerm2") then
tell application "iTerm2" to activate
delay 1
end if
end tell
tell application "iTerm2"
activate
-- Check if any windows exist
if (count of windows) = 0 then
-- Create a new window if none exist
create window with default profile
delay 0.5
end if
try
-- Try to find profile by name first
set newWindow to create window with profile "{}"
on error
try
-- Try to find profile by GUID
set newWindow to create window with profile "{}"
on error
-- If profile still not found, create a new window and navigate manually
set newWindow to create window with default profile
tell current session of newWindow
write text "cd '{}'"
write text "clear"
write text "echo 'đ Welcome to {} repository'"
write text "echo 'đ Branch: {}'"
write text "echo ''"
write text "git status"
end tell
end try
end try
end tell"#,
profile_name,
profile_guid,
config.workspace.root.join(&repo.path).display(),
repo.name,
repo.branch.as_deref().unwrap_or("main")
);
let result = Command::new("osascript")
.arg("-e")
.arg(&applescript)
.output();
match result {
Ok(output) => {
if output.status.success() {
println!(
"{} Opened iTerm2 with repository",
style("â").green().bold()
);
} else {
let error = String::from_utf8_lossy(&output.stderr);
println!(
"{} iTerm2 opened but profile may not have loaded: {}",
style("âšī¸").blue(),
error
);
println!("Repository should still be accessible in the new terminal window");
}
}
Err(e) => {
println!(
"{} Failed to execute AppleScript: {}",
style("â ī¸").yellow(),
e
);
println!("\n{} Manual instructions:", style("đ").blue());
println!("1. Open iTerm2");
println!("2. Look for profile '{profile_name}' or '{profile_guid}'");
println!(
"3. Or manually navigate to: {}",
config.workspace.root.join(&repo.path).display()
);
}
}
Ok(())
}
pub async fn cleanup_iterm2_config(config: &WorkspaceConfig, repo: &Repository) -> Result<()> {
let iterm2_integration = config
.apps
.iterm2
.as_ref()
.context("iTerm2 integration is not configured")?;
if !iterm2_integration.enabled {
return Ok(());
}
let profile_name = format!("vibe-{}-{}.json", config.workspace.name, repo.name);
let profile_path = iterm2_integration.config_dir.join(&profile_name);
if profile_path.exists() {
fs::remove_file(&profile_path).await.with_context(|| {
format!(
"Failed to remove iTerm2 profile: {}",
profile_path.display()
)
})?;
println!(
"{} Removed iTerm2 dynamic profile: {}",
style("đī¸").red(),
style(profile_path.display()).cyan()
);
}
Ok(())
}
async fn create_and_launch_itermocil_layout(
config: &WorkspaceConfig,
repo: &Repository,
template_manager: &TemplateManager,
) -> Result<()> {
let iterm2_integration = config
.apps
.iterm2
.as_ref()
.context("iTerm2 integration is not configured")?;
let template_name = repo
.get_app_template("iterm2")
.unwrap_or(&iterm2_integration.default_template);
let yaml_content = match template_manager
.load_template("iterm2", &format!("{template_name}-itermocil"))
.await
{
Ok(content) => {
let variables = TemplateManager::create_variables(config, repo);
template_manager.substitute_variables(&content, &variables)
}
Err(_) => {
generate_itermocil_yaml(config, repo)
}
};
let layout_name = format!("vibe-{}-{}", config.workspace.name, repo.name);
let itermocil_dir = get_itermocil_config_dir();
let layout_path = itermocil_dir.join(format!("{layout_name}.yml"));
if let Some(parent) = layout_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!("Failed to create iTermocil directory: {}", parent.display())
})?;
} else {
fs::create_dir_all(&itermocil_dir).await.with_context(|| {
format!(
"Failed to create iTermocil directory: {}",
itermocil_dir.display()
)
})?;
}
fs::write(&layout_path, yaml_content)
.await
.with_context(|| {
format!(
"Failed to write iTermocil layout: {}",
layout_path.display()
)
})?;
println!(
"{} Created iTermocil layout: {}",
style("â
").green(),
style(layout_path.display()).cyan()
);
let result = Command::new("itermocil").arg(&layout_name).spawn();
match result {
Ok(_) => {
println!(
"{} Launched iTerm2 with 3-pane layout via iTermocil",
style("â").green().bold()
);
println!("{} Panes:", style("đ").blue());
println!(" âĸ Left: Agent launcher");
println!(" âĸ Top-right: Git manager");
println!(" âĸ Bottom-right: Project commands");
}
Err(e) => {
anyhow::bail!("Failed to launch iTermocil: {}", e);
}
}
Ok(())
}
fn generate_itermocil_yaml(config: &WorkspaceConfig, repo: &Repository) -> String {
let template = DEFAULT_ITERMOCIL_TEMPLATE;
let variables = TemplateManager::create_variables(config, repo);
let temp_dir = std::env::temp_dir();
let template_manager = TemplateManager::new(temp_dir);
template_manager.substitute_variables(template, &variables)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workspace::{AppIntegrations, ITerm2Integration, Repository, WorkspaceInfo};
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_config() -> WorkspaceConfig {
let temp_dir = TempDir::new().unwrap();
WorkspaceConfig {
workspace: WorkspaceInfo {
name: "test-workspace".to_string(),
root: PathBuf::from("/tmp/test"),
auto_discover: false,
},
repositories: vec![
Repository::new("frontend", "./frontend"),
Repository::new("backend", "./backend"),
],
groups: vec![],
apps: AppIntegrations {
github: None,
warp: None,
vscode: None,
iterm2: Some(ITerm2Integration {
enabled: true,
config_dir: temp_dir.path().to_path_buf(),
template_dir: temp_dir.path().join("templates").join("iterm2"),
default_template: "default".to_string(),
}),
wezterm: None,
cursor: None,
windsurf: None,
},
preferences: None,
}
}
}