rmux-server 0.8.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::collections::HashSet;
use std::sync::atomic::Ordering;

use rmux_core::LifecycleEvent;
use rmux_proto::{OptionName, RmuxError, SessionName, TerminalSize, WindowTarget};

use crate::pane_io::AttachControl;

use super::super::RequestHandler;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::handler) enum AttachedWindowSizePolicy {
    Latest,
    Largest,
    Smallest,
    Manual,
}

#[derive(Debug, Clone, Copy)]
struct AttachedSizeCandidate {
    size: TerminalSize,
    sequence: u64,
}

impl RequestHandler {
    pub(in crate::handler) async fn attached_window_size_policy_for_session(
        &self,
        session_name: &SessionName,
    ) -> Result<AttachedWindowSizePolicy, RmuxError> {
        let state = self.state.lock().await;
        let Some(session) = state.sessions.session(session_name) else {
            return Err(crate::pane_terminals::session_not_found(session_name));
        };
        let window_index = session.active_window_index();
        Ok(policy_from_option_value(state.options.resolve_for_window(
            session_name,
            window_index,
            OptionName::WindowSize,
        )))
    }

    pub(in crate::handler) async fn reconcile_attached_session_size(
        &self,
        session_name: &SessionName,
    ) -> Result<Option<WindowTarget>, RmuxError> {
        let (selected_size, current_size, active_window_index) = self
            .selected_attached_session_size(session_name, None)
            .await?;
        let Some(selected_size) = selected_size else {
            return Ok(None);
        };
        if selected_size == current_size {
            return Ok(None);
        }

        let mut state = self.state.lock().await;
        let Some(session) = state.sessions.session(session_name) else {
            return Ok(None);
        };
        if session.window().size() == selected_size {
            return Ok(None);
        }
        state.mutate_session_and_resize_terminals(session_name, |session| {
            session.resize_terminal(selected_size);
            Ok(())
        })?;
        Ok(Some(WindowTarget::with_window(
            session_name.clone(),
            active_window_index,
        )))
    }

    pub(in crate::handler) async fn reconcile_attached_session_size_and_emit(
        &self,
        session_name: &SessionName,
    ) -> Result<(), RmuxError> {
        if let Some(target) = self.reconcile_attached_session_size(session_name).await? {
            self.emit_without_attached_refresh(LifecycleEvent::WindowResized { target })
                .await;
        }
        Ok(())
    }

    pub(in crate::handler) async fn selected_attached_session_size_for_new_client(
        &self,
        session_name: &SessionName,
        client_size: TerminalSize,
        client_flags: super::ClientFlags,
    ) -> Result<Option<TerminalSize>, RmuxError> {
        if client_flags.contains(super::ClientFlags::IGNORESIZE) {
            let (selected_size, _current_size, _active_window_index) = self
                .selected_attached_session_size(session_name, None)
                .await?;
            return Ok(selected_size);
        }
        let (selected_size, _current_size, _active_window_index) = self
            .selected_attached_session_size(session_name, Some(client_size))
            .await?;
        Ok(selected_size)
    }

    async fn selected_attached_session_size(
        &self,
        session_name: &SessionName,
        incoming_client_size: Option<TerminalSize>,
    ) -> Result<(Option<TerminalSize>, TerminalSize, u32), RmuxError> {
        let (policy, linked_sessions, current_size, active_window_index) = {
            let state = self.state.lock().await;
            let Some(session) = state.sessions.session(session_name) else {
                return Err(crate::pane_terminals::session_not_found(session_name));
            };
            let active_window_index = session.active_window_index();
            let policy = policy_from_option_value(state.options.resolve_for_window(
                session_name,
                active_window_index,
                OptionName::WindowSize,
            ));
            if policy == AttachedWindowSizePolicy::Manual {
                return Ok((None, session.window().size(), active_window_index));
            }
            let aggressive_resize = state.options.resolve_for_window(
                session_name,
                active_window_index,
                OptionName::AggressiveResize,
            ) == Some("on");
            let linked_sessions = if aggressive_resize {
                state.window_linked_current_sessions_list(session_name, active_window_index)
            } else {
                state.window_linked_sessions_list(session_name, active_window_index)
            };
            (
                policy,
                linked_sessions.into_iter().collect::<HashSet<_>>(),
                session.window().size(),
                active_window_index,
            )
        };

        let candidates = {
            let active_attach = self.active_attach.lock().await;
            let mut candidates = active_attach
                .by_pid
                .values()
                .filter(|active| {
                    !active.suspended
                        && !active.closing.load(Ordering::Acquire)
                        && linked_sessions.contains(&active.session_name)
                        && !active.flags.contains(super::ClientFlags::IGNORESIZE)
                })
                .map(|active| AttachedSizeCandidate {
                    size: active.client_size,
                    sequence: active.size_sequence,
                })
                .collect::<Vec<_>>();
            if let Some(size) = incoming_client_size {
                candidates.push(AttachedSizeCandidate {
                    size,
                    sequence: active_attach.next_size_sequence,
                });
            }
            candidates
        };
        Ok((
            selected_attached_size(policy, &candidates),
            current_size,
            active_window_index,
        ))
    }

    pub(in crate::handler) async fn prune_stale_attached_clients_for_session(
        &self,
        session_name: &SessionName,
    ) -> Vec<u32> {
        let stale_pids = {
            let active_attach = self.active_attach.lock().await;
            active_attach
                .by_pid
                .iter()
                .filter_map(|(pid, active)| {
                    (&active.session_name == session_name
                        && (active.control_tx.is_closed()
                            || active.control_backlog.load(Ordering::Acquire)
                                >= super::ATTACH_CONTROL_BACKLOG_LIMIT))
                        .then_some(*pid)
                })
                .collect::<Vec<_>>()
        };
        self.remove_attached_clients_for_session(session_name, stale_pids)
            .await
    }

    pub(in crate::handler) async fn remove_attached_clients_for_session(
        &self,
        session_name: &SessionName,
        attach_pids: Vec<u32>,
    ) -> Vec<u32> {
        if attach_pids.is_empty() {
            return Vec::new();
        }
        let (removed, key_tables, overlays) = {
            let mut active_attach = self.active_attach.lock().await;
            let mut removed = Vec::new();
            let mut key_tables = Vec::new();
            let mut overlays = Vec::new();
            for pid in attach_pids {
                let remove = active_attach
                    .by_pid
                    .get(&pid)
                    .is_some_and(|active| &active.session_name == session_name);
                if remove {
                    let mut active = active_attach
                        .by_pid
                        .remove(&pid)
                        .expect("attached client checked above");
                    let _ = active.control_tx.send(AttachControl::Detach);
                    active.closing.store(true, Ordering::SeqCst);
                    removed.push(pid);
                    if let Some(table_name) = active.key_table_name.take() {
                        key_tables.push(table_name);
                    }
                    overlays.push(active.overlay.take());
                }
            }
            (removed, key_tables, overlays)
        };

        for overlay in overlays {
            super::terminate_overlay_job(overlay);
        }
        if !key_tables.is_empty() {
            let mut state = self.state.lock().await;
            for table_name in key_tables {
                state.key_bindings.unref_table(&table_name);
            }
        }
        for pid in &removed {
            self.emit_without_attached_refresh(LifecycleEvent::ClientDetached {
                session_name: session_name.clone(),
                client_name: Some(pid.to_string()),
            })
            .await;
        }
        removed
    }
}

fn policy_from_option_value(value: Option<&str>) -> AttachedWindowSizePolicy {
    match value {
        Some("largest") => AttachedWindowSizePolicy::Largest,
        Some("smallest") => AttachedWindowSizePolicy::Smallest,
        Some("manual") => AttachedWindowSizePolicy::Manual,
        Some("latest") | None => AttachedWindowSizePolicy::Latest,
        Some(_) => AttachedWindowSizePolicy::Latest,
    }
}

fn selected_attached_size(
    policy: AttachedWindowSizePolicy,
    candidates: &[AttachedSizeCandidate],
) -> Option<TerminalSize> {
    match policy {
        AttachedWindowSizePolicy::Manual => None,
        AttachedWindowSizePolicy::Latest => candidates
            .iter()
            .max_by_key(|candidate| candidate.sequence)
            .map(|candidate| candidate.size),
        AttachedWindowSizePolicy::Largest => candidates
            .iter()
            .map(|candidate| candidate.size)
            .reduce(|selected, size| TerminalSize {
                cols: selected.cols.max(size.cols),
                rows: selected.rows.max(size.rows),
            }),
        AttachedWindowSizePolicy::Smallest => candidates
            .iter()
            .map(|candidate| candidate.size)
            .reduce(|selected, size| TerminalSize {
                cols: selected.cols.min(size.cols),
                rows: selected.rows.min(size.rows),
            }),
    }
}