#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
use anyhow::{bail, Context, Result};
use std::path::Path;
use std::process::Command;
pub trait TmuxLauncher {
fn detect(&self) -> Result<()>;
fn create_window(&self, path: &Path, branch: &str) -> Result<()>;
fn create_pane(&self, path: &Path) -> Result<()>;
}
pub struct RealTmuxLauncher;
impl TmuxLauncher for RealTmuxLauncher {
fn detect(&self) -> Result<()> {
if std::env::var_os("TMUX").is_none() {
bail!(
"tmux integration requires running inside a tmux session; \
try again from a tmux pane or use --no-tmux to disable"
);
}
let status = Command::new("tmux")
.arg("-V")
.output()
.context("Failed to execute tmux command")?;
if !status.status.success() {
bail!("tmux binary not found or not executable");
}
Ok(())
}
fn create_window(&self, path: &Path, branch: &str) -> Result<()> {
self.detect()?;
let name = sanitize_window_name(branch);
let output = Command::new("tmux")
.arg("new-window")
.arg("-n")
.arg(&name)
.arg("-c")
.arg(path)
.output()
.context("Failed to execute tmux new-window command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("tmux new-window command failed: {}", stderr.trim());
}
Ok(())
}
fn create_pane(&self, path: &Path) -> Result<()> {
self.detect()?;
let output = Command::new("tmux")
.arg("split-window")
.arg("-h")
.arg("-c")
.arg(path)
.output()
.context("Failed to execute tmux split-window command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("tmux split-window command failed: {}", stderr.trim());
}
Ok(())
}
}
pub fn sanitize_window_name(branch: &str) -> String {
if branch.is_empty() {
return "worktree".to_string();
}
let sanitized = branch.replace(['/', ' '], "·");
if sanitized.len() > 50 {
sanitized.chars().take(50).collect()
} else {
sanitized
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_window_name_simple() {
assert_eq!(sanitize_window_name("feature"), "feature");
}
#[test]
fn test_sanitize_window_name_with_slash() {
assert_eq!(sanitize_window_name("feature/login"), "feature·login");
}
#[test]
fn test_sanitize_window_name_with_space() {
assert_eq!(sanitize_window_name("feature bug fix"), "feature·bug·fix");
}
#[test]
fn test_sanitize_window_name_mixed() {
assert_eq!(sanitize_window_name("feat/bug fix"), "feat·bug·fix");
}
#[test]
fn test_sanitize_window_name_long() {
let long_name = "a".repeat(100);
let result = sanitize_window_name(&long_name);
assert_eq!(result.len(), 50);
}
#[test]
fn test_sanitize_window_name_empty() {
assert_eq!(sanitize_window_name(""), "worktree");
}
#[test]
fn test_sanitize_window_name_only_special_chars() {
assert_eq!(sanitize_window_name("///"), "···");
}
}