use std::path::{Path, PathBuf};
use crate::terminal_setup::detector::TerminalType;
use crate::terminal_setup::features::multiline;
use anyhow::{Context, Result};
use vtcode_commons::VtCodePaths;
use vtcode_commons::terminal_detection::{
ITERM2_DYNAMIC_PROFILE_FILENAME, ITERM2_ICON_MODE_CUSTOM, ITERM2_PROFILE_NAME, installed_iterm2_profile_path,
iterm2_dynamic_profiles_dir,
};
pub fn generate_config(features: &[crate::terminal_setup::detector::TerminalFeature]) -> Result<String> {
let mut instructions = vec![
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".to_string(),
" iTerm2 Manual Configuration Instructions".to_string(),
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".to_string(),
String::new(),
"iTerm2 requires manual configuration via the GUI.".to_string(),
"Follow these steps to configure each feature:".to_string(),
String::new(),
];
for (i, feature) in features.iter().enumerate() {
match feature {
crate::terminal_setup::detector::TerminalFeature::Multiline => {
instructions.push(format!("{}. MULTILINE INPUT (Shift+Enter)", i + 1));
instructions.push(String::new());
let multiline_instructions = multiline::generate_config(TerminalType::ITerm2)?;
instructions.push(multiline_instructions);
instructions.push(String::new());
}
crate::terminal_setup::detector::TerminalFeature::CopyPaste => {
instructions.push(format!("{}. COPY/PASTE INTEGRATION", i + 1));
instructions.push(String::new());
instructions.push("1. Open iTerm2 Preferences (Cmd+,)".to_string());
instructions.push("2. Go to General → Selection".to_string());
instructions.push("3. Enable 'Copy to pasteboard on selection'".to_string());
instructions.push("4. Go to Pointer tab".to_string());
instructions.push("5. Set middle-click action to 'Paste from Clipboard'".to_string());
instructions.push(String::new());
}
crate::terminal_setup::detector::TerminalFeature::ShellIntegration => {
instructions.push(format!("{}. SHELL INTEGRATION", i + 1));
instructions.push(String::new());
instructions.push("1. Open iTerm2 Preferences (Cmd+,)".to_string());
instructions.push("2. Go to Profiles → General".to_string());
instructions.push("3. Under 'Command', select your shell".to_string());
instructions.push("4. iTerm2's shell integration will auto-install on first launch".to_string());
instructions.push("5. Or manually install: curl -L https://iterm2.com/shell_integration/install_shell_integration.sh | bash".to_string());
instructions.push(String::new());
}
crate::terminal_setup::detector::TerminalFeature::ThemeSync => {
instructions.push(format!("{}. THEME SYNCHRONIZATION", i + 1));
instructions.push(String::new());
instructions.push("1. Open iTerm2 Preferences (Cmd+,)".to_string());
instructions.push("2. Go to Profiles → Colors".to_string());
instructions.push("3. Choose a color preset or customize manually".to_string());
instructions.push("4. VT Code theme colors can be manually configured here".to_string());
instructions.push(String::new());
}
crate::terminal_setup::detector::TerminalFeature::Notifications => {
instructions.push(format!("{}. SYSTEM NOTIFICATIONS", i + 1));
instructions.push(String::new());
instructions.push("1. Open iTerm2 Preferences (Cmd+,)".to_string());
instructions.push("2. Navigate to Profiles → Terminal".to_string());
instructions.push(
"3. Enable 'Silence bell' and Filter Alerts → 'Send escape sequence-generated alerts'".to_string(),
);
instructions.push("4. Set your preferred notification delay".to_string());
instructions.push(
"5. For shell integration notifications, consider using tools like terminal-notifier".to_string(),
);
instructions.push(String::new());
}
}
}
instructions.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".to_string());
instructions.push("After configuration, restart iTerm2 for changes to take effect.".to_string());
instructions.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".to_string());
Ok(instructions.join("\n"))
}
pub const PROFILE_GUID: &str = "1FC21C70-F2B1-4F0F-BB03-D1AE12EF900E";
pub const PROFILE_ICON_FILENAME: &str = "vtcode-profile-120.png";
const PROFILE_ICON_BYTES: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/embedded_assets/resources/icons/vtcode-profile-120.png"));
pub struct ProfileIconInstallReport {
pub profile_path: PathBuf,
pub icon_path: PathBuf,
}
pub fn dynamic_profile_json(icon_path: &Path) -> Result<String> {
let document = serde_json::json!({
"Profiles": [{
"Name": ITERM2_PROFILE_NAME,
"Guid": PROFILE_GUID,
"Icon": ITERM2_ICON_MODE_CUSTOM,
"Custom Icon Path": icon_path.to_string_lossy(),
"Automatic Profile Switching": ["&vtcode"],
}],
});
serde_json::to_string_pretty(&document).context("failed to serialize iTerm2 dynamic profile")
}
pub fn installed_icon_path(data_dir: &Path) -> PathBuf {
data_dir.join("icons").join(PROFILE_ICON_FILENAME)
}
pub fn install_profile_icon(home: &Path, data_dir: &Path) -> Result<ProfileIconInstallReport> {
if !cfg!(target_os = "macos") {
anyhow::bail!("iTerm2 profile icons are only available on macOS");
}
let icon_path = installed_icon_path(data_dir);
if let Some(parent) = icon_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create icon directory {}", parent.display()))?;
}
std::fs::write(&icon_path, PROFILE_ICON_BYTES)
.with_context(|| format!("failed to write icon {}", icon_path.display()))?;
let profiles_dir = iterm2_dynamic_profiles_dir(home);
std::fs::create_dir_all(&profiles_dir).with_context(|| format!("failed to create {}", profiles_dir.display()))?;
let profile_path = profiles_dir.join(ITERM2_DYNAMIC_PROFILE_FILENAME);
let json = dynamic_profile_json(&icon_path)?;
std::fs::write(&profile_path, json).with_context(|| format!("failed to write {}", profile_path.display()))?;
Ok(ProfileIconInstallReport { profile_path, icon_path })
}
pub fn default_install_paths() -> Result<(PathBuf, PathBuf)> {
let home = dirs::home_dir().context("failed to determine home directory")?;
let data_dir = VtCodePaths::resolve()?.data_dir().to_path_buf();
Ok((home, data_dir))
}
pub fn profile_icon_instructions() -> Vec<String> {
vec![
"TAB ICON (profile image):".to_string(),
"1. Run `/terminal-setup install-iterm2-icon` to install the VT Code tab icon automatically.".to_string(),
"2. Or set it manually: Settings → Profiles → General → Icon → Custom, then pick a PNG from resources/icons/."
.to_string(),
]
}
pub fn run_profile_icon_install(renderer: &mut crate::utils::ansi::AnsiRenderer) -> Result<()> {
use crate::terminal_setup::detector::TerminalType;
use crate::utils::ansi::MessageStyle;
let terminal = TerminalType::detect()?;
if !matches!(terminal, TerminalType::ITerm2) {
renderer.line(MessageStyle::Error, &format!("This installer needs iTerm2 (detected {}).", terminal.name()))?;
return Ok(());
}
let (home, data_dir) = default_install_paths()?;
renderer.line(MessageStyle::Info, "Installing VT Code iTerm2 tab icon...")?;
let report = install_profile_icon(&home, &data_dir)?;
renderer.line(MessageStyle::Status, &format!("✓ Profile written: {}", report.profile_path.display()))?;
renderer.line(MessageStyle::Status, &format!("✓ Icon installed: {}", report.icon_path.display()))?;
renderer.line(
MessageStyle::Info,
"Open a new iTerm2 tab so it picks up the profile; the logo replaces the generic tab glyph while VT Code runs.",
)?;
Ok(())
}
pub fn ensure_profile_icon() -> Result<Option<ProfileIconInstallReport>> {
if !cfg!(target_os = "macos") {
return Ok(None);
}
let (home, data_dir) = default_install_paths()?;
let iterm_session = std::env::var("ITERM_SESSION_ID").is_ok();
let tmux_session = std::env::var("TMUX").is_ok();
ensure_profile_icon_at(&home, &data_dir, iterm_session, tmux_session)
}
pub fn ensure_profile_icon_at(
home: &Path,
data_dir: &Path,
iterm_session: bool,
tmux_session: bool,
) -> Result<Option<ProfileIconInstallReport>> {
if !cfg!(target_os = "macos") || !iterm_session || tmux_session {
return Ok(None);
}
let profile_path = installed_iterm2_profile_path(home);
let icon_path = installed_icon_path(data_dir);
let icon_fresh = std::fs::read(&icon_path)
.map(|bytes| bytes == PROFILE_ICON_BYTES)
.unwrap_or(false);
if profile_path.exists() && icon_fresh {
return Ok(None);
}
install_profile_icon(home, data_dir).map(Some)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal_setup::detector::TerminalFeature;
#[test]
fn test_generate_instructions() {
let features = vec![TerminalFeature::Multiline, TerminalFeature::CopyPaste];
let instructions = generate_config(&features).unwrap();
assert!(instructions.contains("iTerm2"));
assert!(instructions.contains("Preferences"));
assert!(instructions.contains("MULTILINE"));
assert!(instructions.contains("COPY/PASTE"));
}
#[test]
fn dynamic_profile_json_points_at_given_icon() {
let first = dynamic_profile_json(Path::new("/data/vtcode/icons/vtcode-profile-120.png")).unwrap();
let second = dynamic_profile_json(Path::new("/other/icons/vtcode-profile-120.png")).unwrap();
assert_ne!(first, second);
let value: serde_json::Value = serde_json::from_str(&first).unwrap();
let profile = &value["Profiles"][0];
assert_eq!(profile["Name"], ITERM2_PROFILE_NAME);
assert_eq!(profile["Guid"], PROFILE_GUID);
assert_eq!(profile["Icon"], ITERM2_ICON_MODE_CUSTOM);
assert_eq!(profile["Custom Icon Path"], "/data/vtcode/icons/vtcode-profile-120.png");
assert!(
profile["Automatic Profile Switching"]
.as_array()
.is_some_and(|rules| rules.iter().any(|rule| rule == "&vtcode"))
);
}
#[cfg(target_os = "macos")]
#[test]
fn install_profile_icon_writes_artwork_and_profile() {
let home = tempfile::tempdir().unwrap();
let data = tempfile::tempdir().unwrap();
let first = install_profile_icon(home.path(), data.path()).unwrap();
assert!(first.profile_path.exists());
assert!(first.icon_path.exists());
assert_eq!(std::fs::read(&first.icon_path).unwrap(), PROFILE_ICON_BYTES);
let stored: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&first.profile_path).unwrap()).unwrap();
assert_eq!(
stored["Profiles"][0]["Custom Icon Path"].as_str().unwrap().to_string(),
first.icon_path.to_string_lossy().into_owned()
);
let second = install_profile_icon(home.path(), data.path()).unwrap();
assert_eq!(second.profile_path, first.profile_path);
assert_eq!(second.icon_path, first.icon_path);
}
#[test]
fn profile_icon_instructions_point_at_installer() {
let lines = profile_icon_instructions();
assert!(lines.iter().any(|line| line.contains("install-iterm2-icon")));
}
#[test]
fn ensure_skips_without_iterm_session_and_writes_nothing() {
let home = tempfile::tempdir().unwrap();
let data = tempfile::tempdir().unwrap();
let report = ensure_profile_icon_at(home.path(), data.path(), false, false).unwrap();
assert!(report.is_none());
assert!(!iterm2_dynamic_profiles_dir(home.path()).exists());
}
#[test]
fn ensure_skips_under_tmux_and_writes_nothing() {
let home = tempfile::tempdir().unwrap();
let data = tempfile::tempdir().unwrap();
let report = ensure_profile_icon_at(home.path(), data.path(), true, true).unwrap();
assert!(report.is_none());
assert!(!iterm2_dynamic_profiles_dir(home.path()).exists());
}
#[cfg(target_os = "macos")]
#[test]
fn ensure_installs_once_then_refreshes_stale_artwork() {
let home = tempfile::tempdir().unwrap();
let data = tempfile::tempdir().unwrap();
let first = ensure_profile_icon_at(home.path(), data.path(), true, false).unwrap();
assert!(first.is_some());
let second = ensure_profile_icon_at(home.path(), data.path(), true, false).unwrap();
assert!(second.is_none());
let icon_path = installed_icon_path(data.path());
std::fs::write(&icon_path, b"stale-bytes").unwrap();
let third = ensure_profile_icon_at(home.path(), data.path(), true, false).unwrap();
let report = third.expect("stale artwork must trigger a refresh");
assert_eq!(std::fs::read(&report.icon_path).unwrap(), PROFILE_ICON_BYTES);
}
}