use std::path::Path;
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use console::style;
use crate::config;
use crate::error::{CwError, Result};
use crate::git;
fn wait_for_shell_ready(pane_id: &str, timeout: f64) {
let poll_interval = Duration::from_millis(200);
let deadline = Instant::now() + Duration::from_secs_f64(timeout);
while Instant::now() < deadline {
if let Ok(output) = Command::new("wezterm")
.args(["cli", "get-text", "--pane-id", pane_id])
.output()
{
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
if !text.trim().is_empty() {
return; }
}
}
thread::sleep(poll_interval);
}
}
fn send_text(pane_id: &str, command: &str) -> Result<()> {
if pane_id.is_empty() {
return Err(CwError::Git(
"Failed to get pane ID from WezTerm spawn".to_string(),
));
}
let timeout = config::load_config()
.map(|c| c.launch.wezterm_ready_timeout)
.unwrap_or(5.0);
wait_for_shell_ready(pane_id, timeout);
let input_text = format!("{}\n", command);
let mut child = Command::new("wezterm")
.args(["cli", "send-text", "--pane-id", pane_id, "--no-paste"])
.stdin(std::process::Stdio::piped())
.spawn()
.map_err(|e| CwError::Git(format!("wezterm send-text failed: {}", e)))?;
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
let _ = stdin.write_all(input_text.as_bytes());
}
let _ = child.wait();
Ok(())
}
pub fn launch_window(path: &Path, command: &str, ai_tool_name: &str) -> Result<()> {
if !git::has_command("wezterm") {
return Err(CwError::Git(
"wezterm not installed. Install from https://wezterm.org/".to_string(),
));
}
let path_str = path.to_string_lossy().to_string();
let output = Command::new("wezterm")
.args(["cli", "spawn", "--new-window", "--cwd", &path_str])
.output()
.map_err(|e| CwError::Git(format!("wezterm spawn failed: {}", e)))?;
let pane_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
send_text(&pane_id, command)?;
println!(
"{} {} running in new WezTerm window\n",
style("*").green().bold(),
ai_tool_name
);
Ok(())
}
pub fn launch_tab(path: &Path, command: &str, ai_tool_name: &str) -> Result<()> {
if !git::has_command("wezterm") {
return Err(CwError::Git(
"wezterm not installed. Install from https://wezterm.org/".to_string(),
));
}
let path_str = path.to_string_lossy().to_string();
let output = Command::new("wezterm")
.args(["cli", "spawn", "--cwd", &path_str])
.output()
.map_err(|e| CwError::Git(format!("wezterm spawn failed: {}", e)))?;
let pane_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
send_text(&pane_id, command)?;
println!(
"{} {} running in new WezTerm tab\n",
style("*").green().bold(),
ai_tool_name
);
Ok(())
}
pub fn launch_tab_bg(path: &Path, command: &str, ai_tool_name: &str) -> Result<()> {
if !git::has_command("wezterm") {
return Err(CwError::Git(
"wezterm not installed. Install from https://wezterm.org/".to_string(),
));
}
let current_pane = std::env::var("WEZTERM_PANE").unwrap_or_default();
let original_tab_id = if !current_pane.is_empty() {
get_active_tab_in_same_window(¤t_pane)
} else {
None
};
let path_str = path.to_string_lossy().to_string();
let output = Command::new("wezterm")
.args(["cli", "spawn", "--cwd", &path_str])
.output()
.map_err(|e| CwError::Git(format!("wezterm spawn failed: {}", e)))?;
let pane_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
if let Some(tab_id) = original_tab_id {
let _ = Command::new("wezterm")
.args(["cli", "activate-tab", "--tab-id", &tab_id])
.status();
} else {
eprintln!(
"{} WEZTERM_PANE not set; cannot restore focus to original tab",
style("!").yellow()
);
}
send_text(&pane_id, command)?;
println!(
"{} {} running in new WezTerm tab (background)\n",
style("*").green().bold(),
ai_tool_name
);
Ok(())
}
fn get_active_tab_in_same_window(pane_id: &str) -> Option<String> {
let output = Command::new("wezterm")
.args(["cli", "list", "--format", "json"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let panes: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout).ok()?;
find_active_tab_in_window(&panes, pane_id)
}
fn find_active_tab_in_window(panes: &[serde_json::Value], pane_id: &str) -> Option<String> {
let target: u64 = pane_id.parse().ok()?;
let window_id = panes
.iter()
.find(|p| p["pane_id"].as_u64() == Some(target))
.and_then(|p| p["window_id"].as_u64())?;
panes
.iter()
.find(|p| {
p["window_id"].as_u64() == Some(window_id) && p["is_active"].as_bool() == Some(true)
})
.and_then(|p| p["tab_id"].as_u64())
.map(|t| t.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_pane(window_id: u64, tab_id: u64, pane_id: u64, is_active: bool) -> serde_json::Value {
json!({
"window_id": window_id,
"tab_id": tab_id,
"pane_id": pane_id,
"is_active": is_active,
})
}
#[test]
fn returns_active_tab_in_same_window() {
let panes = vec![make_pane(1, 10, 100, false), make_pane(1, 11, 101, true)];
assert_eq!(find_active_tab_in_window(&panes, "100"), Some("11".into()));
}
#[test]
fn ignores_active_tab_in_different_window() {
let panes = vec![make_pane(1, 10, 100, false), make_pane(2, 20, 200, true)];
assert_eq!(find_active_tab_in_window(&panes, "100"), None);
}
#[test]
fn returns_none_for_unknown_pane() {
let panes = vec![make_pane(1, 10, 100, true)];
assert_eq!(find_active_tab_in_window(&panes, "999"), None);
}
#[test]
fn returns_none_for_invalid_pane_id() {
let panes = vec![make_pane(1, 10, 100, true)];
assert_eq!(find_active_tab_in_window(&panes, "not-a-number"), None);
}
#[test]
fn handles_multi_pane_active_tab() {
let panes = vec![
make_pane(1, 10, 100, false), make_pane(1, 10, 101, true), ];
assert_eq!(find_active_tab_in_window(&panes, "100"), Some("10".into()));
}
#[test]
fn returns_none_for_empty_pane_list() {
let panes: Vec<serde_json::Value> = vec![];
assert_eq!(find_active_tab_in_window(&panes, "100"), None);
}
#[test]
fn returns_own_tab_when_caller_is_active() {
let panes = vec![make_pane(1, 10, 100, true)];
assert_eq!(find_active_tab_in_window(&panes, "100"), Some("10".into()));
}
}
pub fn launch_pane(path: &Path, command: &str, ai_tool_name: &str, horizontal: bool) -> Result<()> {
if !git::has_command("wezterm") {
return Err(CwError::Git(
"wezterm not installed. Install from https://wezterm.org/".to_string(),
));
}
let split_flag = if horizontal {
"--horizontal"
} else {
"--bottom"
};
let path_str = path.to_string_lossy().to_string();
let output = Command::new("wezterm")
.args(["cli", "split-pane", split_flag, "--cwd", &path_str])
.output()
.map_err(|e| CwError::Git(format!("wezterm split-pane failed: {}", e)))?;
let pane_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
send_text(&pane_id, command)?;
let pane_type = if horizontal { "horizontal" } else { "vertical" };
println!(
"{} {} running in WezTerm {} pane\n",
style("*").green().bold(),
ai_tool_name,
pane_type
);
Ok(())
}