rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::{
    text_width as tmux_text_width, truncate_to_width as tmux_truncate_to_width, OptionStore,
    Session, Utf8Config,
};
use rmux_proto::OptionName;

use super::super::{apply_style_overlay, RenderedPrompt};
use super::resolved_status_style;
use super::runs::{
    push_spaces, push_status_run, sanitize_status_text, status_runs_width, StatusRun, StatusStyle,
};

pub(super) struct PromptStatusLayout {
    pub(super) runs: Vec<StatusRun>,
    pub(super) cursor_x: u16,
}

pub(in crate::renderer) fn prompt_status_runs(
    session: &Session,
    options: &OptionStore,
    columns: u16,
    prompt: &RenderedPrompt,
) -> Vec<StatusRun> {
    prompt_status_layout(session, options, columns, prompt).runs
}

pub(super) fn prompt_status_layout(
    session: &Session,
    options: &OptionStore,
    columns: u16,
    prompt: &RenderedPrompt,
) -> PromptStatusLayout {
    let width = usize::from(columns);
    let utf8_config = Utf8Config::from_options(options);
    let style = prompt_style(session, options, prompt.command_prompt);
    let prompt_text =
        sanitize_status_text(tmux_truncate_to_width(&prompt.prompt, width, &utf8_config));
    let prompt_width = tmux_text_width(&prompt_text, &utf8_config);
    let available = width.saturating_sub(prompt_width);
    let input = prompt_visible_input(&prompt.input, prompt.cursor, available, &utf8_config);
    let input_text = sanitize_status_text(input.text);

    let mut runs = Vec::new();
    push_status_run(&mut runs, prompt_text, style.clone());
    push_status_run(&mut runs, input_text, style.clone());
    let rendered = status_runs_width(&runs, &utf8_config);
    push_spaces(&mut runs, width.saturating_sub(rendered), style);

    let cursor_x = prompt_width.saturating_add(input.cursor_x);
    PromptStatusLayout {
        runs,
        cursor_x: u16::try_from(cursor_x.min(width.saturating_sub(1))).unwrap_or(u16::MAX),
    }
}

fn prompt_style(session: &Session, options: &OptionStore, command_prompt: bool) -> StatusStyle {
    let style_option = if command_prompt {
        OptionName::MessageCommandStyle
    } else {
        OptionName::MessageStyle
    };
    apply_style_overlay(
        &resolved_status_style(options, session.name()),
        options.resolve(Some(session.name()), style_option),
    )
}

struct PromptVisibleInput {
    text: String,
    cursor_x: usize,
}

fn prompt_visible_input(
    input: &str,
    cursor: usize,
    width: usize,
    utf8_config: &Utf8Config,
) -> PromptVisibleInput {
    if width == 0 {
        return PromptVisibleInput {
            text: String::new(),
            cursor_x: 0,
        };
    }

    let input_width = width.saturating_sub(1);
    let cursor = cursor.min(input.chars().count());
    let cursor_byte = byte_index_for_char(input, cursor);
    let mut start_byte = 0;
    while tmux_text_width(&input[start_byte..cursor_byte], utf8_config) > input_width {
        let Some((offset, _)) = input[start_byte..].char_indices().nth(1) else {
            break;
        };
        start_byte += offset;
    }

    let cursor_x = tmux_text_width(&input[start_byte..cursor_byte], utf8_config).min(input_width);
    let text = tmux_truncate_to_width(&input[start_byte..], input_width, utf8_config);
    PromptVisibleInput { text, cursor_x }
}

fn byte_index_for_char(value: &str, char_index: usize) -> usize {
    value
        .char_indices()
        .nth(char_index)
        .map_or(value.len(), |(index, _)| index)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prompt_visible_input_reserves_a_cursor_cell_at_the_tail() {
        let visible = prompt_visible_input("0123456789", 10, 6, &Utf8Config::default());

        assert_eq!(visible.text, "56789");
        assert_eq!(visible.cursor_x, 5);
    }

    #[test]
    fn prompt_visible_input_keeps_cursor_column_for_middle_edits() {
        let visible = prompt_visible_input("abcdef", 3, 8, &Utf8Config::default());

        assert_eq!(visible.text, "abcdef");
        assert_eq!(visible.cursor_x, 3);
    }
}