rmux-server 0.9.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_proto::{
    CapturePaneRequest, ErrorResponse, PaneId, ResizePaneRequest, Response, RmuxError,
    SplitWindowTarget, Target,
};

use super::{
    pane_support::{SplitWindowParts, SplitWindowResponseMode},
    RequestHandler,
};

#[cfg(test)]
#[derive(Debug, Default)]
pub(in crate::handler) struct SplitTargetResolutionPause {
    pub(in crate::handler) reached: tokio::sync::Notify,
    pub(in crate::handler) release: tokio::sync::Notify,
}

#[cfg(test)]
static SPLIT_TARGET_RESOLUTION_PAUSE: std::sync::Mutex<
    Option<(
        rmux_proto::SessionName,
        PaneId,
        std::sync::Arc<SplitTargetResolutionPause>,
    )>,
> = std::sync::Mutex::new(None);

#[cfg(test)]
pub(in crate::handler) fn install_split_target_resolution_pause(
    session_name: rmux_proto::SessionName,
    pane_id: PaneId,
) -> std::sync::Arc<SplitTargetResolutionPause> {
    let pause = std::sync::Arc::new(SplitTargetResolutionPause::default());
    *SPLIT_TARGET_RESOLUTION_PAUSE
        .lock()
        .expect("split target resolution pause lock") =
        Some((session_name, pane_id, pause.clone()));
    pause
}

#[cfg(test)]
async fn pause_after_split_target_resolution(
    target: &SplitWindowTarget,
    expected_pane_id: Option<PaneId>,
) {
    let target_session = match target {
        SplitWindowTarget::Session(session_name) => session_name,
        SplitWindowTarget::Pane(target) => target.session_name(),
    };
    let pause = {
        let mut installed = SPLIT_TARGET_RESOLUTION_PAUSE
            .lock()
            .expect("split target resolution pause lock");
        let matches_pane = installed
            .as_ref()
            .is_some_and(|(session_name, pane_id, _)| {
                session_name == target_session && Some(*pane_id) == expected_pane_id
            });
        matches_pane.then(|| {
            installed
                .take()
                .expect("matching split target resolution pause remains installed")
                .2
        })
    };
    let Some(pause) = pause else {
        return;
    };
    pause.reached.notify_one();
    pause.release.notified().await;
}

#[cfg(not(test))]
async fn pause_after_split_target_resolution(
    _target: &SplitWindowTarget,
    _expected_pane_id: Option<PaneId>,
) {
}

fn stable_pane_id_target(target: Option<&str>) -> Option<PaneId> {
    target?
        .strip_prefix('%')?
        .parse::<u32>()
        .ok()
        .map(PaneId::new)
}

impl RequestHandler {
    pub(in crate::handler) async fn handle_split_window_target_action(
        &self,
        requester_pid: u32,
        request: rmux_proto::SplitWindowTargetActionRequest,
    ) -> Response {
        self.handle_split_window_target_action_with_mode(
            requester_pid,
            request,
            SplitWindowResponseMode::Legacy,
        )
        .await
    }

    pub(in crate::handler) async fn handle_split_window_identity(
        &self,
        requester_pid: u32,
        request: rmux_proto::SplitWindowIdentityRequest,
    ) -> Response {
        self.handle_split_window_target_action_with_mode(
            requester_pid,
            request.action,
            SplitWindowResponseMode::StableIdentity,
        )
        .await
    }

    async fn handle_split_window_target_action_with_mode(
        &self,
        requester_pid: u32,
        request: rmux_proto::SplitWindowTargetActionRequest,
        response_mode: SplitWindowResponseMode,
    ) -> Response {
        let expected_pane_id = stable_pane_id_target(request.target.as_deref());
        let target = match self
            .resolve_target_for_requester(
                requester_pid,
                rmux_proto::ResolveTargetRequest {
                    target: request.target,
                    target_type: rmux_proto::ResolveTargetType::Pane,
                    window_index: false,
                    prefer_unattached: false,
                },
            )
            .await
        {
            Ok(Target::Pane(target)) => SplitWindowTarget::Pane(target),
            Ok(_) => {
                return Response::Error(ErrorResponse {
                    error: RmuxError::InvalidTarget {
                        value: "split-window".to_owned(),
                        reason: "resolved target is not a pane".to_owned(),
                    },
                })
            }
            Err(error) => return Response::Error(ErrorResponse { error }),
        };
        pause_after_split_target_resolution(&target, expected_pane_id).await;

        self.handle_split_window_parts(
            requester_pid,
            SplitWindowParts {
                target,
                expected_pane_id,
                direction: request.direction,
                before: request.before,
                environment_overrides: request.environment,
                command: request.command,
                process_command: request.process_command,
                start_directory: request.start_directory,
                keep_alive_on_exit: request.keep_alive_on_exit,
                detached: request.detached,
                size: request.size,
                preserve_zoom: request.preserve_zoom,
                full_size: request.full_size,
                stdin_payload: request.stdin_payload,
                response_mode,
            },
        )
        .await
    }

    pub(in crate::handler) async fn handle_resize_pane_target_action(
        &self,
        requester_pid: u32,
        request: rmux_proto::ResizePaneTargetActionRequest,
    ) -> Response {
        let target = match self
            .resolve_target_for_requester(
                requester_pid,
                rmux_proto::ResolveTargetRequest {
                    target: request.target,
                    target_type: rmux_proto::ResolveTargetType::Pane,
                    window_index: false,
                    prefer_unattached: false,
                },
            )
            .await
        {
            Ok(Target::Pane(target)) => target,
            Ok(_) => {
                return Response::Error(ErrorResponse {
                    error: RmuxError::InvalidTarget {
                        value: "resize-pane".to_owned(),
                        reason: "resolved target is not a pane".to_owned(),
                    },
                })
            }
            Err(error) => return Response::Error(ErrorResponse { error }),
        };

        self.handle_resize_pane(ResizePaneRequest {
            target,
            adjustment: request.adjustment,
        })
        .await
    }

    pub(in crate::handler) async fn handle_capture_pane_target_action(
        &self,
        requester_pid: u32,
        request: rmux_proto::CapturePaneTargetActionRequest,
    ) -> Response {
        let target = match self
            .resolve_target_for_requester(
                requester_pid,
                rmux_proto::ResolveTargetRequest {
                    target: request.target,
                    target_type: rmux_proto::ResolveTargetType::Pane,
                    window_index: false,
                    prefer_unattached: false,
                },
            )
            .await
        {
            Ok(Target::Pane(target)) => target,
            Ok(_) => {
                return Response::Error(ErrorResponse {
                    error: RmuxError::InvalidTarget {
                        value: "capture-pane".to_owned(),
                        reason: "resolved target is not a pane".to_owned(),
                    },
                })
            }
            Err(error) => return Response::Error(ErrorResponse { error }),
        };

        self.handle_capture_pane(CapturePaneRequest {
            target,
            start: request.start,
            end: request.end,
            print: request.print,
            buffer_name: request.buffer_name,
            alternate: request.alternate,
            escape_ansi: request.escape_ansi,
            escape_sequences: request.escape_sequences,
            include_format: request.include_format,
            hyperlinks: request.hyperlinks,
            line_numbers: request.line_numbers,
            join_wrapped: request.join_wrapped,
            use_mode_screen: request.use_mode_screen,
            preserve_trailing_spaces: request.preserve_trailing_spaces,
            do_not_trim_spaces: request.do_not_trim_spaces,
            pending_input: request.pending_input,
            quiet: request.quiet,
            start_is_absolute: request.start_is_absolute,
            end_is_absolute: request.end_is_absolute,
        })
        .await
    }
}