use crate::app::tmux_handler::tmux_state::TmuxTransport;
use par_term_mux::MuxSessionClient;
use std::io;
use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
const DAEMON_UNRESPONSIVE_AFTER: Duration = Duration::from_millis(800);
const SEND_LOCK_WAIT: Duration = Duration::from_millis(250);
const OUTBOX_CAPACITY: usize = 512;
pub(crate) struct MuxTransport {
client: Arc<Mutex<MuxSessionClient>>,
outbox: SyncSender<String>,
health: Arc<MuxHealth>,
}
impl MuxTransport {
pub(crate) fn new(client: MuxSessionClient) -> Self {
let client = Arc::new(Mutex::new(client));
let health = Arc::new(MuxHealth::default());
let (outbox, inbox) = sync_channel::<String>(OUTBOX_CAPACITY);
spawn_send_worker(Arc::clone(&client), inbox, Arc::clone(&health));
Self {
client,
outbox,
health,
}
}
#[cfg(test)]
pub(crate) fn connect_or_spawn_at(path: &std::path::Path) -> io::Result<Self> {
Ok(Self::new(MuxSessionClient::connect_or_spawn_at(path)?))
}
pub(crate) fn client(&self) -> MutexGuard<'_, MuxSessionClient> {
self.client
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn lock_for_send(&self) -> io::Result<MutexGuard<'_, MuxSessionClient>> {
let deadline = Instant::now() + SEND_LOCK_WAIT;
loop {
match self.client.try_lock() {
Ok(guard) => return Ok(guard),
Err(std::sync::TryLockError::Poisoned(poisoned)) => {
return Ok(poisoned.into_inner());
}
Err(std::sync::TryLockError::WouldBlock) => {
if Instant::now() >= deadline {
return Err(io::Error::other(
"mux send worker still holds the client — daemon unresponsive?",
));
}
std::thread::sleep(Duration::from_millis(5));
}
}
}
}
}
fn spawn_send_worker(
client: Arc<Mutex<MuxSessionClient>>,
inbox: Receiver<String>,
health: Arc<MuxHealth>,
) {
std::thread::spawn(move || {
while let Ok(command) = inbox.recv() {
let mut client = client.lock().unwrap_or_else(|p| p.into_inner());
health.send_started();
let result = client.send(&command);
health.send_finished(&result);
}
});
}
#[derive(Default)]
pub(crate) struct MuxHealth {
state: Mutex<MuxHealthState>,
}
#[derive(Default)]
struct MuxHealthState {
started: Option<Instant>,
timed_out: bool,
toast_shown: bool,
dropped: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DaemonHealthSignal {
Unresponsive,
Recovered,
}
impl MuxHealth {
pub(crate) fn send_started(&self) {
let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
state.started = Some(Instant::now());
}
pub(crate) fn send_finished(&self, result: &io::Result<Vec<String>>) {
let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
state.started = None;
state.timed_out = matches!(
result,
Err(e) if e.kind() == io::ErrorKind::TimedOut
);
}
pub(crate) fn count_dropped(&self) {
let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
state.dropped += 1;
}
pub(crate) fn poll_event(&self, threshold: Duration) -> Option<DaemonHealthSignal> {
let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
let hung = state.timed_out
|| state
.started
.is_some_and(|started| started.elapsed() >= threshold);
match (hung, state.toast_shown) {
(true, false) => {
state.toast_shown = true;
Some(DaemonHealthSignal::Unresponsive)
}
(false, true) => {
state.toast_shown = false;
Some(DaemonHealthSignal::Recovered)
}
_ => None,
}
}
}
impl TmuxTransport for MuxTransport {
fn drain(
&self,
) -> (
Vec<par_term_emu_core_rust::tmux_control::TmuxNotification>,
bool,
) {
let Ok(mut client) = self.client.try_lock() else {
return (Vec::new(), false);
};
client.drain_core_notifications()
}
fn send_command(&self, command: &str) -> io::Result<Vec<String>> {
let mut client = self.lock_for_send()?;
client.send(command)
}
fn send_command_no_wait(&self, command: &str) -> io::Result<()> {
match self.outbox.try_send(command.to_string()) {
Ok(()) => Ok(()),
Err(TrySendError::Full(_)) => {
self.health.count_dropped();
Ok(())
}
Err(TrySendError::Disconnected(_)) => self.send_command(command).map(|_| ()),
}
}
fn daemon_health_event(&self) -> Option<String> {
let dropped = self
.health
.state
.lock()
.unwrap_or_else(|p| p.into_inner())
.dropped;
self.health
.poll_event(DAEMON_UNRESPONSIVE_AFTER)
.map(|signal| match signal {
DaemonHealthSignal::Unresponsive => {
if dropped > 0 {
format!(
"par-mux daemon not responding — input dropped ({dropped} commands)"
)
} else {
"par-mux daemon not responding — input queued until it recovers".into()
}
}
DaemonHealthSignal::Recovered => "par-mux daemon responding again".into(),
})
}
}