rmux-server 0.10.0

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

use crate::status_lines::status_line_count;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::renderer) enum StatusOverlayRow {
    Status(u16),
    Content(u16),
}

impl StatusOverlayRow {
    pub(in crate::renderer) const fn y(self) -> u16 {
        match self {
            Self::Status(y) | Self::Content(y) => y,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct StatusGeometry {
    pub(in crate::renderer) terminal_size: TerminalSize,
    content_cols: u16,
    pub(in crate::renderer) content_rows: u16,
    pub(in crate::renderer) content_y_offset: u16,
    pub(in crate::renderer) status_y: Option<u16>,
    pub(in crate::renderer) status_lines: u16,
}

impl StatusGeometry {
    pub(crate) fn for_session(session: &Session, options: &OptionStore) -> Self {
        let terminal_size = session.terminal_size();
        let layout_size = session.window().size();
        let status = options.resolve(Some(session.name()), OptionName::Status);
        if terminal_size.cols == 0 || terminal_size.rows == 0 || matches!(status, Some("off")) {
            return Self::without_status(terminal_size, layout_size);
        }
        let status_lines = status_line_count(status, terminal_size.rows);
        let available_content_rows = terminal_size.rows.saturating_sub(status_lines);
        let content_cols = layout_size.cols.min(terminal_size.cols);
        let content_rows = layout_size.rows.min(available_content_rows);

        match options.resolve(Some(session.name()), OptionName::StatusPosition) {
            Some("top") => Self {
                terminal_size,
                content_cols,
                content_rows,
                content_y_offset: status_lines,
                status_y: Some(0),
                status_lines,
            },
            _ => Self {
                terminal_size,
                content_cols,
                content_rows,
                content_y_offset: 0,
                status_y: Some(terminal_size.rows.saturating_sub(status_lines)),
                status_lines,
            },
        }
    }

    pub(in crate::renderer) const fn without_status(
        terminal_size: TerminalSize,
        layout_size: TerminalSize,
    ) -> Self {
        Self {
            terminal_size,
            content_cols: if layout_size.cols < terminal_size.cols {
                layout_size.cols
            } else {
                terminal_size.cols
            },
            content_rows: if layout_size.rows < terminal_size.rows {
                layout_size.rows
            } else {
                terminal_size.rows
            },
            content_y_offset: 0,
            status_y: None,
            status_lines: 0,
        }
    }

    pub(in crate::renderer) const fn content_size(self) -> TerminalSize {
        TerminalSize {
            cols: self.content_cols,
            rows: self.content_rows,
        }
    }

    pub(crate) const fn content_y_offset(self) -> u16 {
        self.content_y_offset
    }

    pub(crate) const fn content_rows(self) -> u16 {
        self.content_rows
    }

    pub(in crate::renderer) const fn status_line_y(self, line: u16) -> Option<u16> {
        let status_y = match self.status_y {
            Some(status_y) => status_y,
            None => return None,
        };
        if line >= self.status_lines {
            return None;
        }
        Some(status_y.saturating_add(line))
    }

    pub(in crate::renderer) const fn message_overlay_row(
        self,
        status_line: u16,
    ) -> Option<StatusOverlayRow> {
        if self.terminal_size.cols == 0 || self.terminal_size.rows == 0 {
            return None;
        }
        match self.status_y {
            Some(_) => match self.status_line_y(status_line) {
                Some(y) => Some(StatusOverlayRow::Status(y)),
                None => None,
            },
            None => Some(StatusOverlayRow::Content(
                self.terminal_size.rows.saturating_sub(1),
            )),
        }
    }
}

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

    fn session() -> Session {
        Session::new(
            SessionName::new("overlay-row").expect("valid session name"),
            TerminalSize { cols: 20, rows: 6 },
        )
    }

    #[test]
    fn message_overlay_row_models_status_and_content_backing() {
        let session = session();
        let mut options = OptionStore::new();
        let status_geometry = StatusGeometry::for_session(&session, &options);
        assert_eq!(
            status_geometry.message_overlay_row(0),
            Some(StatusOverlayRow::Status(5))
        );

        options
            .set(
                ScopeSelector::Global,
                OptionName::Status,
                "off".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("status off");
        let content_geometry = StatusGeometry::for_session(&session, &options);
        assert_eq!(
            content_geometry.message_overlay_row(0),
            Some(StatusOverlayRow::Content(5))
        );
    }

    #[test]
    fn message_overlay_row_rejects_degenerate_terminal_sizes() {
        for size in [
            TerminalSize { cols: 0, rows: 6 },
            TerminalSize { cols: 20, rows: 0 },
        ] {
            let session = Session::new(
                SessionName::new("degenerate-overlay").expect("valid session name"),
                size,
            );
            let geometry = StatusGeometry::for_session(&session, &OptionStore::new());
            assert_eq!(geometry.message_overlay_row(0), None);
        }
    }
}