rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::formats::FormatContext;
use rmux_proto::RmuxError;

use crate::format_runtime::{render_runtime_template, RuntimeFormatContext};
use crate::input_keys::MouseForwardEvent;

use super::super::prompt_support::PromptInputEvent;
use super::super::RequestHandler;
use super::mode_tree_model::{
    ModeTreeBuild, ModeTreeClientState, ModeTreeDeferredAction, ModeTreeKind,
    ModeTreePromptCallback, SearchDirection,
};
use super::mode_tree_render::{mode_tree_list_rows, sanitize_overlay_text};
use super::mode_tree_selection::{
    collapse_or_parent, current_tree_kill_prompt, cycle_sort, expand_or_child, move_selection,
    repeat_search, select_edge, tag_all, tagged_tree_kill_prompt, toggle_tag,
};

impl RequestHandler {
    pub(in crate::handler) async fn handle_mode_tree_key_event(
        &self,
        attach_pid: u32,
        event: PromptInputEvent,
    ) -> Result<bool, RmuxError> {
        let mut mode = {
            let active_attach = self.active_attach.lock().await;
            let active = active_attach
                .by_pid
                .get(&attach_pid)
                .ok_or_else(|| RmuxError::Server("attached client disappeared".to_owned()))?;
            let Some(mode) = active.mode_tree.clone() else {
                return Ok(false);
            };
            mode
        };
        let build = self.build_mode_tree(&mut mode, attach_pid).await?;
        if build.visible.is_empty() {
            if matches!(
                event,
                PromptInputEvent::Escape | PromptInputEvent::Char('q')
            ) {
                self.dismiss_mode_tree_with_refresh(attach_pid).await?;
                return Ok(true);
            }
            return Ok(false);
        }

        match event {
            PromptInputEvent::Up | PromptInputEvent::Ctrl('p') | PromptInputEvent::Char('k') => {
                move_selection(&mut mode, &build, -1, true);
            }
            PromptInputEvent::Down | PromptInputEvent::Ctrl('n') | PromptInputEvent::Char('j') => {
                move_selection(&mut mode, &build, 1, true);
            }
            PromptInputEvent::Home => select_edge(&mut mode, &build, false),
            PromptInputEvent::End => select_edge(&mut mode, &build, true),
            PromptInputEvent::KeyName(name) if name == "PageUp" => {
                move_selection(&mut mode, &build, -10, false);
            }
            PromptInputEvent::KeyName(name) if name == "PageDown" => {
                move_selection(&mut mode, &build, 10, false);
            }
            PromptInputEvent::Left => collapse_or_parent(&mut mode, &build),
            PromptInputEvent::Right => expand_or_child(&mut mode, &build),
            PromptInputEvent::Enter => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.accept_mode_tree_selection(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Escape | PromptInputEvent::Char('q') => {
                self.dismiss_mode_tree_with_refresh(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Char('t') => toggle_tag(&mut mode, &build),
            PromptInputEvent::Ctrl('t') => tag_all(&mut mode, &build),
            PromptInputEvent::Ctrl('s') => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.start_mode_tree_prompt(
                    attach_pid,
                    ModeTreePromptCallback::Search(SearchDirection::Forward),
                )
                .await?;
                return Ok(true);
            }
            PromptInputEvent::Ctrl('r') => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.start_mode_tree_prompt(
                    attach_pid,
                    ModeTreePromptCallback::Search(SearchDirection::Backward),
                )
                .await?;
                return Ok(true);
            }
            PromptInputEvent::Char('n') => repeat_search(&mut mode, &build, false),
            PromptInputEvent::Char('N') => repeat_search(&mut mode, &build, true),
            PromptInputEvent::Char('f') => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.start_mode_tree_prompt(attach_pid, ModeTreePromptCallback::Filter)
                    .await?;
                return Ok(true);
            }
            PromptInputEvent::Char('o') | PromptInputEvent::Char('O') => cycle_sort(&mut mode),
            PromptInputEvent::Char('r') => mode.reversed = !mode.reversed,
            PromptInputEvent::Char('v') => {
                mode.preview_mode = mode.preview_mode.cycle();
                mode.preview_scroll = 0;
            }
            PromptInputEvent::Char(':') => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.start_mode_tree_prompt(attach_pid, ModeTreePromptCallback::Command)
                    .await?;
                return Ok(true);
            }
            PromptInputEvent::Char('p') | PromptInputEvent::Char('P')
                if matches!(mode.kind, ModeTreeKind::Buffer) =>
            {
                let delete_after = matches!(event, PromptInputEvent::Char('P'));
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.perform_buffer_paste(attach_pid, delete_after).await?;
                return Ok(true);
            }
            PromptInputEvent::Char('d') | PromptInputEvent::Char('D')
                if matches!(mode.kind, ModeTreeKind::Buffer) =>
            {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.perform_buffer_delete(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Char('d') | PromptInputEvent::Char('D')
                if matches!(mode.kind, ModeTreeKind::Client) =>
            {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.perform_client_detach(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Char('x') | PromptInputEvent::Char('X')
                if matches!(mode.kind, ModeTreeKind::Tree) =>
            {
                let prompt = match event {
                    PromptInputEvent::Char('x') => current_tree_kill_prompt(&mode, &build),
                    PromptInputEvent::Char('X') => tagged_tree_kill_prompt(&mode),
                    _ => None,
                };
                let Some(prompt) = prompt else {
                    return Ok(false);
                };
                let action = match event {
                    PromptInputEvent::Char('x') => ModeTreeDeferredAction::KillCurrentTreeSelection,
                    PromptInputEvent::Char('X') => ModeTreeDeferredAction::KillTaggedTreeSelections,
                    _ => unreachable!("tree kill prompt only binds x/X"),
                };
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.confirm_mode_tree_action(attach_pid, prompt, action)
                    .await?;
                return Ok(true);
            }
            PromptInputEvent::Char('x') | PromptInputEvent::Char('X')
                if matches!(mode.kind, ModeTreeKind::Buffer) =>
            {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.confirm_mode_tree_action(
                    attach_pid,
                    "delete selected buffers?".to_owned(),
                    ModeTreeDeferredAction::DeleteBuffers,
                )
                .await?;
                return Ok(true);
            }
            PromptInputEvent::Char('x') | PromptInputEvent::Char('X')
                if matches!(mode.kind, ModeTreeKind::Client) =>
            {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.confirm_mode_tree_action(
                    attach_pid,
                    "detach selected clients?".to_owned(),
                    ModeTreeDeferredAction::DetachClients,
                )
                .await?;
                return Ok(true);
            }
            PromptInputEvent::Char('s') if matches!(mode.kind, ModeTreeKind::Customize) => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.start_customize_set_prompt(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Char('u') if matches!(mode.kind, ModeTreeKind::Customize) => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.perform_customize_unset(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Ctrl('x') if matches!(mode.kind, ModeTreeKind::Customize) => {
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.perform_customize_reset(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::KeyName(name) if name == "F1" => {
                self.show_mode_tree_help(attach_pid).await?;
                return Ok(true);
            }
            PromptInputEvent::Ctrl('h') => {
                self.show_mode_tree_help(attach_pid).await?;
                return Ok(true);
            }
            _ => {
                // Shortcut keys are checked last so navigation keys take priority
                // (matching tmux's mode_tree_key dispatch order).
                if let Some(shortcut_id) = shortcut_match(&mode, &build, &event) {
                    mode.selected_id = Some(shortcut_id);
                    self.store_mode_tree_state(attach_pid, mode).await?;
                    self.accept_mode_tree_selection(attach_pid).await?;
                    return Ok(true);
                }
                return Ok(false);
            }
        }

        self.store_mode_tree_state(attach_pid, mode).await?;
        self.refresh_mode_tree_overlay_if_active(attach_pid).await?;
        Ok(true)
    }

    pub(in crate::handler) async fn handle_mode_tree_mouse_event(
        &self,
        attach_pid: u32,
        event: MouseForwardEvent,
    ) -> Result<bool, RmuxError> {
        let mut mode = {
            let active_attach = self.active_attach.lock().await;
            let active = active_attach
                .by_pid
                .get(&attach_pid)
                .ok_or_else(|| RmuxError::Server("attached client disappeared".to_owned()))?;
            let Some(mode) = active.mode_tree.clone() else {
                return Ok(false);
            };
            mode
        };
        let build = self.build_mode_tree(&mut mode, attach_pid).await?;
        if build.visible.is_empty() {
            return Ok(false);
        }

        let rows = self.mode_tree_content_rows(&mode).await?;
        let list_rows = mode_tree_list_rows(rows, build.visible.len(), mode.preview_mode);
        let y = usize::from(event.y);
        if y < usize::from(list_rows) {
            let index = mode.scroll + y;
            if let Some(id) = build.visible.get(index) {
                let changed = mode.selected_id.as_ref() != Some(id);
                mode.selected_id = Some(id.clone());
                if changed {
                    mode.preview_scroll = 0;
                }
                self.store_mode_tree_state(attach_pid, mode).await?;
                self.refresh_mode_tree_overlay_if_active(attach_pid).await?;
                return Ok(true);
            }
        }
        Ok(false)
    }
}

fn shortcut_match(
    mode: &ModeTreeClientState,
    build: &ModeTreeBuild,
    event: &PromptInputEvent,
) -> Option<String> {
    let key = event_key_name(event);
    if key.is_empty() {
        return None;
    }
    for (index, id) in build.visible.iter().enumerate() {
        let rendered = render_runtime_template(
            &mode.key_format,
            &RuntimeFormatContext::new(FormatContext::new())
                .with_named_value("line", index.to_string()),
            false,
        );
        if !rendered.is_empty() && sanitize_overlay_text(&rendered) == key {
            return Some(id.clone());
        }
    }
    None
}

fn event_key_name(event: &PromptInputEvent) -> String {
    match event {
        PromptInputEvent::Char(ch) => ch.to_string(),
        PromptInputEvent::Ctrl(ch) => format!("C-{ch}"),
        PromptInputEvent::KeyName(name) => name.clone(),
        PromptInputEvent::Enter => "Enter".to_owned(),
        PromptInputEvent::Escape => "Escape".to_owned(),
        PromptInputEvent::Left => "Left".to_owned(),
        PromptInputEvent::Right => "Right".to_owned(),
        PromptInputEvent::Up => "Up".to_owned(),
        PromptInputEvent::Down => "Down".to_owned(),
        PromptInputEvent::Home => "Home".to_owned(),
        PromptInputEvent::End => "End".to_owned(),
        PromptInputEvent::Delete => "DC".to_owned(),
        PromptInputEvent::Backspace => "BSpace".to_owned(),
        PromptInputEvent::Tab => "Tab".to_owned(),
    }
}