rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::formats::FormatContext;
use rmux_core::{OptionStore, Pane, Session, Style, Utf8Config};
use rmux_proto::OptionName;

use crate::format_runtime::{render_runtime_template, RuntimeFormatContext};
use crate::pane_terminals::HandlerState;
use crate::pane_visible_geometry::{
    pane_border_status_row as content_pane_border_status_row, PaneBorderStatusPosition,
};

use super::super::{
    format_draw_content_width, format_draw_line, render_formatted_line, StatusGeometry,
};
use super::{resolve_border_style_for_pane, PaneBorderLineStyle};

pub(in crate::renderer) fn render_pane_border_status_lines(
    session: &Session,
    options: &OptionStore,
    geometry: StatusGeometry,
    state: Option<&HandlerState>,
) -> Vec<u8> {
    if session.window().pane_count() == 0 {
        return Vec::new();
    }

    let Some(position) = PaneBorderStatusPosition::from_options(
        options,
        session.name(),
        session.active_window_index(),
    ) else {
        return Vec::new();
    };

    let mut frame = Vec::new();
    for pane in session.window().panes() {
        if session.window().is_zoomed() && pane.index() != session.active_pane_index() {
            continue;
        }
        render_pane_border_status_line(
            &mut frame, session, options, geometry, state, pane, position,
        );
    }
    frame
}

fn render_pane_border_status_line(
    frame: &mut Vec<u8>,
    session: &Session,
    options: &OptionStore,
    geometry: StatusGeometry,
    state: Option<&HandlerState>,
    pane: &Pane,
    position: PaneBorderStatusPosition,
) {
    let pane_geometry = pane.geometry();
    if pane_geometry.cols() == 0 || pane_geometry.rows() == 0 {
        return;
    }

    let Some(row) = pane_border_status_row(position, pane, geometry) else {
        return;
    };

    let left_padding = if pane_geometry.cols() > 2 { 2 } else { 0 };
    let width = usize::from(pane_geometry.cols().saturating_sub(left_padding));
    if width == 0 {
        return;
    }

    let window_index = session.active_window_index();
    let base = resolve_border_style_for_pane(
        session,
        options,
        window_index,
        pane,
        pane.index() == session.active_pane_index(),
    );
    let line_style = PaneBorderLineStyle::from_option(options.resolve_for_window(
        session.name(),
        window_index,
        OptionName::PaneBorderLines,
    ));
    let expanded = expanded_pane_border_format(session, options, state, pane, window_index);
    let utf8 = Utf8Config::from_options(options);
    let expanded = with_border_fill(expanded, width, line_style, &base, &utf8);
    let line = format_draw_line(&expanded, &base, width, &utf8);

    render_formatted_line(
        frame,
        pane_geometry.x().saturating_add(left_padding),
        row,
        &line,
    );
}

fn pane_border_status_row(
    position: PaneBorderStatusPosition,
    pane: &Pane,
    geometry: StatusGeometry,
) -> Option<u16> {
    let content_row =
        content_pane_border_status_row(position, pane.geometry(), geometry.content_rows)?;
    let row = content_row.saturating_add(geometry.content_y_offset);
    (row < geometry.terminal_size.rows).then_some(row)
}

fn expanded_pane_border_format(
    session: &Session,
    options: &OptionStore,
    state: Option<&HandlerState>,
    pane: &Pane,
    window_index: u32,
) -> String {
    let context = FormatContext::from_session(session)
        .with_window(window_index, session.window(), true, false)
        .with_window_pane(session.window(), pane);
    let mut runtime = RuntimeFormatContext::new(context)
        .with_options(options)
        .with_session(session)
        .with_window(window_index, session.window())
        .with_pane(pane);
    if let Some(state) = state {
        runtime = runtime.with_state(state);
    }
    let template = options
        .resolve_for_pane(
            session.name(),
            window_index,
            pane.index(),
            OptionName::PaneBorderFormat,
        )
        .unwrap_or("");
    render_runtime_template(template, &runtime, true)
}

fn with_border_fill(
    mut expanded: String,
    width: usize,
    line_style: PaneBorderLineStyle,
    base: &Style,
    utf8: &Utf8Config,
) -> String {
    let content_width = format_draw_content_width(&expanded, base, utf8);
    let fill_width = width.saturating_sub(content_width);
    if fill_width == 0 {
        return expanded;
    }
    expanded.extend(std::iter::repeat_n(line_style.map_glyph(''), fill_width));
    expanded
}

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

    use super::*;

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

    fn two_pane_session() -> Session {
        let mut session = Session::new(session_name("alpha"), TerminalSize { cols: 40, rows: 10 });
        session
            .split_active_pane_with_direction(SplitDirection::Horizontal)
            .expect("split succeeds");
        session
    }

    #[test]
    fn pane_border_status_top_renders_edge_and_border_lines() {
        let mut session = two_pane_session();
        session.select_pane(1).expect("select bottom pane");
        let mut options = OptionStore::new();
        options
            .set(
                ScopeSelector::Window(rmux_proto::WindowTarget::with_window(
                    session.name().clone(),
                    session.active_window_index(),
                )),
                OptionName::PaneBorderStatus,
                "top".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("pane border status set succeeds");
        options
            .set(
                ScopeSelector::Window(rmux_proto::WindowTarget::with_window(
                    session.name().clone(),
                    session.active_window_index(),
                )),
                OptionName::PaneBorderFormat,
                "pane=#{pane_index},active=#{pane_active}".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("pane border format set succeeds");

        let frame = String::from_utf8(render_pane_border_status_lines(
            &session,
            &options,
            StatusGeometry::without_status(session.window().size(), session.window().size()),
            None,
        ))
        .expect("frame is utf8");

        assert!(frame.contains("pane=1"), "{frame:?}");
        assert!(frame.contains("pane=0"), "{frame:?}");
        assert!(frame.contains("active=1"), "{frame:?}");
    }

    #[test]
    fn pane_border_status_edge_rows_are_reserved_inside_content() {
        let session = two_pane_session();
        let top_pane = session.window().pane(0).expect("top pane exists");
        let bottom_pane = session.window().pane(1).expect("bottom pane exists");
        let geometry =
            StatusGeometry::without_status(session.window().size(), session.window().size());

        assert_eq!(
            pane_border_status_row(PaneBorderStatusPosition::Top, top_pane, geometry),
            Some(0)
        );
        assert_eq!(
            pane_border_status_row(PaneBorderStatusPosition::Bottom, bottom_pane, geometry),
            Some(9)
        );
    }

    #[test]
    fn pane_border_status_uses_configured_border_line_fill() {
        let session = two_pane_session();
        let mut options = OptionStore::new();
        let scope = ScopeSelector::Window(rmux_proto::WindowTarget::with_window(
            session.name().clone(),
            session.active_window_index(),
        ));
        options
            .set(
                scope.clone(),
                OptionName::PaneBorderStatus,
                "top".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("pane border status set succeeds");
        options
            .set(
                scope.clone(),
                OptionName::PaneBorderFormat,
                "p#{pane_index}".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("pane border format set succeeds");
        options
            .set(
                scope,
                OptionName::PaneBorderLines,
                "heavy".to_owned(),
                SetOptionMode::Replace,
            )
            .expect("pane border lines set succeeds");

        let frame = String::from_utf8(render_pane_border_status_lines(
            &session,
            &options,
            StatusGeometry::without_status(session.window().size(), session.window().size()),
            None,
        ))
        .expect("frame is utf8");

        assert!(frame.contains(''), "{frame:?}");
        assert!(!frame.contains(''), "{frame:?}");
    }
}