rmux-server 0.9.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::{OptionStore, Pane, PaneGeometry, Session, Window};
use rmux_proto::{OptionName, ResizePaneAdjustment, TerminalSize};

use crate::pane_visible_geometry::{clip_pane_geometry, visible_pane_content_geometry};
use crate::status_lines::content_rows_for_status;

use super::RuntimeFormatContext;

impl RuntimeFormatContext<'_> {
    pub(super) fn visible_session_snapshot(&self) -> Option<Session> {
        let mut session = self.session?.clone();
        if !self.use_unclipped_geometry {
            let size =
                visible_session_size(self.option_store(), &session, self.session_attached_count());
            if size != session.window().size() {
                session.resize_terminal(size);
            }
        }
        Some(session)
    }

    pub(super) fn visible_window_snapshot(&self) -> Option<Window> {
        let session = self.visible_session_snapshot()?;
        let window_index = self
            .window_index
            .unwrap_or_else(|| session.active_window_index());
        session.window_at(window_index).cloned()
    }

    pub(super) fn layout_window_snapshot(&self) -> Option<Window> {
        let mut session = self.session?.clone();
        for window_index in session.windows().keys().copied().collect::<Vec<_>>() {
            let Some(active_pane_index) = session
                .window_at(window_index)
                .map(|window| (window.is_zoomed(), window.active_pane_index()))
                .and_then(|(zoomed, active_pane_index)| zoomed.then_some(active_pane_index))
            else {
                continue;
            };
            let _ = session.resize_pane_in_window(
                window_index,
                active_pane_index,
                ResizePaneAdjustment::Zoom,
            );
        }
        if !self.use_unclipped_geometry {
            let size =
                visible_session_size(self.option_store(), &session, self.session_attached_count());
            if size != session.window().size() {
                session.resize_terminal(size);
            }
        }
        let window_index = self
            .window_index
            .unwrap_or_else(|| session.active_window_index());
        session.window_at(window_index).cloned()
    }

    pub(super) fn visible_pane_snapshot(&self) -> Option<Pane> {
        let session = self.visible_session_snapshot()?;
        let window_index = self
            .window_index
            .unwrap_or_else(|| session.active_window_index());
        let window = session.window_at(window_index)?;
        let pane_index = self
            .pane
            .map(Pane::index)
            .unwrap_or_else(|| window.active_pane_index());
        window.pane(pane_index).cloned()
    }

    pub(super) fn visible_pane_geometry(&self) -> Option<PaneGeometry> {
        let session = self.session?;
        let window = self.visible_window_snapshot()?;
        let window_index = self
            .window_index
            .unwrap_or_else(|| session.active_window_index());
        let content_rows = window.size().rows;
        let pane_index = self
            .pane
            .map(Pane::index)
            .unwrap_or_else(|| window.active_pane_index());
        if window.is_zoomed() && pane_index == window.active_pane_index() {
            let size = window.size();
            let geometry = PaneGeometry::new(0, 0, size.cols, size.rows);
            return Some(match self.option_store() {
                Some(options) => visible_pane_content_geometry(
                    options,
                    session.name(),
                    window_index,
                    geometry,
                    content_rows,
                ),
                None => clip_pane_geometry(geometry, content_rows),
            });
        }
        self.visible_pane_snapshot().map(|pane| {
            let geometry = pane.geometry();
            match self.option_store() {
                Some(options) => visible_pane_content_geometry(
                    options,
                    session.name(),
                    window_index,
                    geometry,
                    content_rows,
                ),
                None => clip_pane_geometry(geometry, content_rows),
            }
        })
    }
}

fn visible_session_size(
    options: Option<&OptionStore>,
    session: &Session,
    attached_count: usize,
) -> TerminalSize {
    let size = session.window().size();
    if size.cols == 0 || size.rows == 0 {
        return size;
    }
    if attached_count == 0 {
        return size;
    }

    let Some(options) = options else {
        return size;
    };

    TerminalSize {
        cols: size.cols,
        rows: content_rows_for_status(
            options.resolve(Some(session.name()), OptionName::Status),
            size.rows,
        ),
    }
}

#[cfg(test)]
mod tests {
    use super::visible_session_size;
    use rmux_core::{OptionStore, Session};
    use rmux_proto::{OptionName, ScopeSelector, SessionName, SetOptionMode, TerminalSize};

    #[test]
    fn visible_session_size_uses_multi_line_status_rows() {
        let alpha = SessionName::new("alpha").expect("valid session name");
        let session = Session::new(alpha.clone(), TerminalSize { cols: 80, rows: 24 });
        let mut options = OptionStore::default();
        options
            .set(
                ScopeSelector::Session(alpha),
                OptionName::Status,
                "3".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("session status set succeeds");

        assert_eq!(
            visible_session_size(Some(&options), &session, 1),
            TerminalSize { cols: 80, rows: 21 }
        );
    }
}