1use super::{Channel, IncomingMessage, OutgoingMessage};
12use crate::channels::transport::Transport;
13use crate::cmd::CommandErrorContext;
14use async_trait::async_trait;
15use std::fmt;
16use std::fmt::Debug;
17use std::fmt::Formatter;
18use std::sync::Arc;
19use tokio::process::Command;
20use tokio::sync::mpsc;
21use tokio::sync::mpsc::Receiver;
22
23pub struct TmuxChannel {
24 transport: Option<Arc<Transport>>,
26}
27
28impl Debug for TmuxChannel {
29 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30 f.debug_struct("TmuxChannel")
31 .field("has_transport", &self.transport.is_some())
32 .finish()
33 }
34}
35
36impl TmuxChannel {
37 pub fn with_transport(transport: Arc<Transport>) -> Self {
38 Self {
39 transport: Some(transport),
40 }
41 }
42}
43
44#[async_trait]
45impl Channel for TmuxChannel {
46 fn name(&self) -> &'static str {
47 "tmux"
48 }
49
50 async fn start(&self) -> anyhow::Result<Receiver<IncomingMessage>> {
51 let (_tx, rx) = mpsc::channel(64);
52 tracing::info!("tmux channel started");
53
54 if self.transport.is_none() {
57 tracing::warn!("tmux channel started without transport - output streaming disabled");
58 }
59
60 Ok(rx)
61 }
62
63 async fn send(&self, msg: &OutgoingMessage) -> anyhow::Result<()> {
64 let session = &msg.thread_id; send_keys(session, &msg.body).await
67 }
68
69 async fn health_check(&self) -> anyhow::Result<()> {
70 let output = Command::new("tmux")
72 .args(["list-sessions"])
73 .output_with_context()
74 .await?;
75 if !output.status.success() {
76 anyhow::bail!("tmux server not running");
77 }
78 Ok(())
79 }
80}
81
82pub async fn send_keys(session: &str, text: &str) -> anyhow::Result<()> {
84 let output = Command::new("tmux")
85 .args(["send-keys", "-t", session, text, "Enter"])
86 .output_with_context()
87 .await?;
88 if !output.status.success() {
89 let stderr = String::from_utf8_lossy(&output.stderr);
90 anyhow::bail!("tmux send-keys failed: {stderr}");
91 }
92 Ok(())
93}
94
95pub async fn capture_pane(session: &str) -> anyhow::Result<String> {
97 let output = Command::new("tmux")
98 .args(["capture-pane", "-t", session, "-p", "-S", "-"])
99 .output_with_context()
100 .await?;
101 if !output.status.success() {
102 let stderr = String::from_utf8_lossy(&output.stderr);
103 anyhow::bail!("tmux capture-pane failed: {stderr}");
104 }
105 Ok(String::from_utf8_lossy(&output.stdout).to_string())
106}
107
108pub async fn list_orch_sessions() -> anyhow::Result<Vec<String>> {
110 let output = Command::new("tmux")
111 .args(["list-sessions", "-F", "#{session_name}"])
112 .output_with_context()
113 .await?;
114 if !output.status.success() {
115 return Ok(vec![]);
116 }
117 Ok(String::from_utf8_lossy(&output.stdout)
118 .lines()
119 .filter(|s| s.starts_with("orch-"))
120 .map(String::from)
121 .collect())
122}
123
124pub async fn is_session_dead(session: &str) -> bool {
130 let output = Command::new("tmux")
131 .args(["has-session", "-t", session])
132 .output_with_context()
133 .await;
134
135 match output {
136 Ok(output) => !output.status.success(),
137 Err(_) => true,
138 }
139}
140
141