rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::{OptionStore, PaneGeometry};
use rmux_proto::{OptionName, SessionName};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PaneBorderStatusPosition {
    Top,
    Bottom,
}

impl PaneBorderStatusPosition {
    pub(crate) fn from_options(
        options: &OptionStore,
        session_name: &SessionName,
        window_index: u32,
    ) -> Option<Self> {
        match options.resolve_for_window(session_name, window_index, OptionName::PaneBorderStatus) {
            Some("top") => Some(Self::Top),
            Some("bottom") => Some(Self::Bottom),
            _ => None,
        }
    }
}

pub(crate) fn clip_pane_geometry(geometry: PaneGeometry, content_rows: u16) -> PaneGeometry {
    let y = geometry.y().min(content_rows);
    let rows = geometry.rows().min(content_rows.saturating_sub(y));
    PaneGeometry::new(geometry.x(), y, geometry.cols(), rows)
}

pub(crate) fn visible_pane_content_geometry(
    options: &OptionStore,
    session_name: &SessionName,
    window_index: u32,
    geometry: PaneGeometry,
    content_rows: u16,
) -> PaneGeometry {
    let geometry = clip_pane_geometry(geometry, content_rows);
    if geometry.rows() == 0 || content_rows == 0 {
        return geometry;
    }

    match PaneBorderStatusPosition::from_options(options, session_name, window_index) {
        Some(PaneBorderStatusPosition::Top) if geometry.y() == 0 => PaneGeometry::new(
            geometry.x(),
            geometry.y().saturating_add(1),
            geometry.cols(),
            geometry.rows().saturating_sub(1),
        ),
        Some(PaneBorderStatusPosition::Bottom)
            if geometry.y().saturating_add(geometry.rows()) >= content_rows =>
        {
            PaneGeometry::new(
                geometry.x(),
                geometry.y(),
                geometry.cols(),
                geometry.rows().saturating_sub(1),
            )
        }
        _ => geometry,
    }
}

pub(crate) fn pane_border_status_row(
    position: PaneBorderStatusPosition,
    geometry: PaneGeometry,
    content_rows: u16,
) -> Option<u16> {
    let geometry = clip_pane_geometry(geometry, content_rows);
    if geometry.rows() == 0 || content_rows == 0 {
        return None;
    }

    let row = match position {
        PaneBorderStatusPosition::Top => {
            if geometry.y() == 0 {
                0
            } else {
                geometry.y().saturating_sub(1)
            }
        }
        PaneBorderStatusPosition::Bottom => {
            let row = geometry.y().saturating_add(geometry.rows());
            if row >= content_rows {
                content_rows.saturating_sub(1)
            } else {
                row
            }
        }
    };
    (row < content_rows).then_some(row)
}

#[cfg(test)]
mod tests {
    use rmux_core::OptionStore;
    use rmux_proto::{ScopeSelector, SessionName, SetOptionMode, WindowTarget};

    use super::*;

    fn session_name(value: &str) -> SessionName {
        SessionName::new(value).expect("valid session name")
    }

    fn options_with_border_status(value: &str) -> (OptionStore, SessionName) {
        let session = session_name("alpha");
        let mut options = OptionStore::new();
        options
            .set(
                ScopeSelector::Window(WindowTarget::with_window(session.clone(), 0)),
                OptionName::PaneBorderStatus,
                value.to_owned(),
                SetOptionMode::Replace,
            )
            .expect("pane-border-status set succeeds");
        (options, session)
    }

    #[test]
    fn top_status_reserves_only_the_top_edge_row() {
        let (options, session) = options_with_border_status("top");

        assert_eq!(
            visible_pane_content_geometry(
                &options,
                &session,
                0,
                PaneGeometry::new(0, 0, 80, 12),
                24,
            ),
            PaneGeometry::new(0, 1, 80, 11)
        );
        assert_eq!(
            visible_pane_content_geometry(
                &options,
                &session,
                0,
                PaneGeometry::new(0, 13, 80, 11),
                24,
            ),
            PaneGeometry::new(0, 13, 80, 11)
        );
        assert_eq!(
            pane_border_status_row(
                PaneBorderStatusPosition::Top,
                PaneGeometry::new(0, 0, 80, 12),
                24,
            ),
            Some(0)
        );
        assert_eq!(
            pane_border_status_row(
                PaneBorderStatusPosition::Top,
                PaneGeometry::new(0, 13, 80, 11),
                24,
            ),
            Some(12)
        );
    }

    #[test]
    fn bottom_status_reserves_only_the_bottom_edge_row() {
        let (options, session) = options_with_border_status("bottom");

        assert_eq!(
            visible_pane_content_geometry(
                &options,
                &session,
                0,
                PaneGeometry::new(0, 0, 80, 12),
                24,
            ),
            PaneGeometry::new(0, 0, 80, 12)
        );
        assert_eq!(
            visible_pane_content_geometry(
                &options,
                &session,
                0,
                PaneGeometry::new(0, 13, 80, 11),
                24,
            ),
            PaneGeometry::new(0, 13, 80, 10)
        );
        assert_eq!(
            pane_border_status_row(
                PaneBorderStatusPosition::Bottom,
                PaneGeometry::new(0, 0, 80, 12),
                24,
            ),
            Some(12)
        );
        assert_eq!(
            pane_border_status_row(
                PaneBorderStatusPosition::Bottom,
                PaneGeometry::new(0, 13, 80, 11),
                24,
            ),
            Some(23)
        );
    }
}