rmux-server 0.2.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::LifecycleEvent;
use rmux_proto::{
    ErrorResponse, HookName, OptionName, PaneTarget, PaneTargetRef, ResizePaneResponse, Response,
    RmuxError, ScopeSelector, SelectPaneResponse, SetOptionMode, Target, WindowTarget,
};

use super::super::{prepare_lifecycle_event, RequestHandler};
use super::{encode_tokens_for_target, prepare_pane_input_write, write_bytes_to_target};
use crate::hook_runtime::PendingInlineHookFormat;
use crate::pane_terminals::{session_not_found, HandlerState};

impl RequestHandler {
    pub(in crate::handler) async fn handle_pane_input_ref(
        &self,
        request: rmux_proto::PaneInputRequest,
    ) -> Response {
        let key_count = request.keys.len();
        let prepared = {
            let state = self.state.lock().await;
            let target = match resolve_pane_target_ref(&state, &request.target) {
                Ok(target) => target,
                Err(error) => return Response::Error(ErrorResponse { error }),
            };
            let bytes = if request.literal {
                request
                    .keys
                    .iter()
                    .flat_map(|key| key.as_bytes().iter().copied())
                    .collect::<Vec<_>>()
            } else {
                match encode_tokens_for_target(&state, &target, &request.keys) {
                    Ok(bytes) => bytes,
                    Err(error) => return Response::Error(ErrorResponse { error }),
                }
            };
            let write = match prepare_pane_input_write(&state, &target, &bytes) {
                Ok(write) => write,
                Err(error) => return Response::Error(ErrorResponse { error }),
            };
            (write, bytes)
        };

        write_bytes_to_target(prepared.0, prepared.1, key_count).await
    }

    pub(in crate::handler) async fn handle_pane_resize_ref(
        &self,
        request: rmux_proto::PaneResizeRequest,
    ) -> Response {
        let session_name = request.target.session_name().clone();
        let adjustment = request.adjustment;
        let (response, window_index) = {
            let mut state = self.state.lock().await;
            let target = match resolve_pane_target_ref(&state, &request.target) {
                Ok(target) => target,
                Err(error) => return Response::Error(ErrorResponse { error }),
            };
            let window_index = target.window_index();
            let pane_index = target.pane_index();
            let response_target = target.clone();
            let response =
                match state.mutate_session_and_resize_terminals(&session_name, |session| {
                    session.resize_pane_in_window(window_index, pane_index, adjustment)?;
                    Ok(ResizePaneResponse {
                        target: response_target,
                        adjustment,
                    })
                }) {
                    Ok(response) => Response::ResizePane(response),
                    Err(error) => Response::Error(ErrorResponse { error }),
                };
            (response, window_index)
        };

        if matches!(response, Response::ResizePane(_))
            && !matches!(adjustment, rmux_proto::ResizePaneAdjustment::NoOp)
        {
            self.emit(LifecycleEvent::WindowLayoutChanged {
                target: WindowTarget::with_window(session_name.clone(), window_index),
            })
            .await;
            self.refresh_attached_session(&session_name).await;
        }

        response
    }

    pub(in crate::handler) async fn handle_pane_kill_ref(
        &self,
        request: rmux_proto::PaneKillRequest,
    ) -> Response {
        let session_name = request.target.session_name().clone();
        let (
            response,
            queued_pane_exited,
            queued_session_closed,
            session_destroyed,
            removed_subscription_keys,
            removed_pane_ids,
            layout_window,
        ) = {
            let mut state = self.state.lock().await;
            let target = match resolve_pane_target_ref(&state, &request.target) {
                Ok(target) => target,
                Err(error) => {
                    return Response::Error(ErrorResponse { error });
                }
            };
            let layout_window = target.window_index();
            let removed_subscription_keys = state
                .pane_output_subscription_keys_for_kill(&target, request.kill_all_except)
                .unwrap_or_default();
            match state.kill_pane_with_options(target.clone(), request.kill_all_except) {
                Ok(result) => {
                    let queued_pane = prepare_lifecycle_event(
                        &mut state,
                        &LifecycleEvent::PaneExited {
                            target: result.hook_context.target.clone(),
                            pane_id: Some(result.hook_context.pane_id),
                            window_id: Some(result.hook_context.window_id),
                            window_name: Some(result.hook_context.window_name.clone()),
                        },
                    );
                    let queued_session = if result.session_destroyed {
                        let _ = state.hooks.remove_session(&session_name);
                        result.removed_session_id.map(|session_id| {
                            prepare_lifecycle_event(
                                &mut state,
                                &LifecycleEvent::SessionClosed {
                                    session_name: session_name.clone(),
                                    session_id: Some(session_id),
                                },
                            )
                        })
                    } else if result.response.window_destroyed {
                        let _ = state.hooks.remove_window(&WindowTarget::with_window(
                            session_name.clone(),
                            layout_window,
                        ));
                        None
                    } else {
                        let _ = state.hooks.remove_pane(&target);
                        None
                    };
                    (
                        Response::KillPane(result.response),
                        Some(queued_pane),
                        queued_session,
                        result.session_destroyed,
                        removed_subscription_keys,
                        result.removed_pane_ids,
                        layout_window,
                    )
                }
                Err(error) => (
                    Response::Error(ErrorResponse { error }),
                    None,
                    None,
                    false,
                    Vec::new(),
                    Vec::new(),
                    layout_window,
                ),
            }
        };

        if !removed_pane_ids.is_empty() {
            self.forget_pane_snapshot_coalescers(&removed_pane_ids);
        }
        if let Some(event) = queued_pane_exited {
            self.emit_prepared(event);
        }
        if let Some(event) = queued_session_closed {
            self.emit_prepared(event);
        }
        if matches!(response, Response::KillPane(_)) {
            self.cleanup_pane_output_subscriptions(&removed_subscription_keys)
                .await;
            if session_destroyed {
                self.remove_session_leases(std::slice::from_ref(&session_name));
                self.exit_attached_session(&session_name).await;
                self.cancel_session_silence_timers(&session_name).await;
                self.refresh_control_session(&session_name).await;
                let _ = self.queue_shutdown_if_server_empty().await;
            } else {
                self.sync_session_silence_timers(&session_name).await;
                if let Response::KillPane(success) = &response {
                    if !success.window_destroyed {
                        self.emit(LifecycleEvent::WindowLayoutChanged {
                            target: WindowTarget::with_window(session_name.clone(), layout_window),
                        })
                        .await;
                    }
                }
                self.dismiss_mode_tree_for_session(&session_name).await;
                self.refresh_attached_session(&session_name).await;
            }
        }

        response
    }

    pub(in crate::handler) async fn handle_pane_respawn_ref(
        &self,
        request: rmux_proto::PaneRespawnRequest,
    ) -> Response {
        let session_name = request.target.session_name().clone();
        let socket_path = self.socket_path();
        let response = {
            let mut state = self.state.lock().await;
            let target = match resolve_pane_target_ref(&state, &request.target) {
                Ok(target) => target,
                Err(error) => return Response::Error(ErrorResponse { error }),
            };
            if let Some(keep_alive) = request.keep_alive_on_exit {
                if let Err(error) = state.options.set(
                    ScopeSelector::Pane(target.clone()),
                    OptionName::RemainOnExit,
                    if keep_alive { "on" } else { "off" }.to_owned(),
                    SetOptionMode::Replace,
                ) {
                    return Response::Error(ErrorResponse { error });
                }
            }
            let request = rmux_proto::RespawnPaneRequest {
                target,
                kill: request.kill,
                start_directory: request.start_directory,
                environment: request.environment,
                command: request.command,
                process_command: request.process_command,
            };
            match state.respawn_pane(
                request,
                &socket_path,
                Some(self.pane_alert_callback()),
                Some(self.pane_exit_callback()),
                |state, replaced| {
                    let queued = prepare_lifecycle_event(
                        state,
                        &LifecycleEvent::PaneExited {
                            target: replaced.target.clone(),
                            pane_id: Some(replaced.pane_id),
                            window_id: Some(replaced.window_id),
                            window_name: Some(replaced.window_name.clone()),
                        },
                    );
                    self.emit_prepared(queued);
                },
            ) {
                Ok(response) => Response::RespawnPane(response),
                Err(error) => Response::Error(ErrorResponse { error }),
            }
        };

        if matches!(response, Response::RespawnPane(_)) {
            self.refresh_attached_session(&session_name).await;
        }

        response
    }

    pub(in crate::handler) async fn handle_pane_snapshot_ref(
        &self,
        request: rmux_proto::PaneSnapshotRefRequest,
    ) -> Response {
        let state = self.state.lock().await;
        let target = match resolve_pane_target_ref(&state, &request.target) {
            Ok(target) => target,
            Err(error) => return Response::Error(ErrorResponse { error }),
        };
        self.handle_resolved_pane_snapshot(&state, &target)
    }

    pub(in crate::handler) async fn handle_pane_select_ref(
        &self,
        request: rmux_proto::PaneSelectRequest,
    ) -> Response {
        let session_name = request.target.session_name().clone();
        let title = request.title.clone();
        let (response, pane_changed, window_index) = {
            let mut state = self.state.lock().await;
            let target = match resolve_pane_target_ref(&state, &request.target) {
                Ok(target) => target,
                Err(error) => return Response::Error(ErrorResponse { error }),
            };
            let window_index = target.window_index();
            let pane_index = target.pane_index();
            let pane_changed = title.is_none()
                && state
                    .sessions
                    .session(&session_name)
                    .and_then(|session| session.window_at(window_index))
                    .is_some_and(|window| window.active_pane_index() != pane_index);
            match (|| -> Result<SelectPaneResponse, RmuxError> {
                let response_target = if let Some(title) = title.as_deref() {
                    state.set_pane_title(&target, title)?;
                    target.clone()
                } else {
                    let session = state
                        .sessions
                        .session_mut(&session_name)
                        .ok_or_else(|| session_not_found(&session_name))?;
                    session.select_pane_in_window(window_index, pane_index)?;
                    let active_pane_index = session
                        .window_at(window_index)
                        .expect("selected pane window must exist")
                        .active_pane_index();
                    PaneTarget::with_window(session_name.clone(), window_index, active_pane_index)
                };
                Ok(SelectPaneResponse {
                    target: response_target,
                })
            })() {
                Ok(response) => (Response::SelectPane(response), pane_changed, window_index),
                Err(error) => (
                    Response::Error(ErrorResponse { error }),
                    false,
                    window_index,
                ),
            }
        };

        if matches!(response, Response::SelectPane(_)) {
            if pane_changed {
                self.emit(LifecycleEvent::WindowPaneChanged {
                    target: WindowTarget::with_window(session_name.clone(), window_index),
                })
                .await;
            }
            if let Response::SelectPane(success) = &response {
                self.queue_inline_hook(
                    HookName::AfterSelectPane,
                    ScopeSelector::Session(session_name.clone()),
                    Some(Target::Pane(success.target.clone())),
                    PendingInlineHookFormat::AfterCommand,
                );
            }
            self.refresh_attached_session(&session_name).await;
        }

        response
    }
}

pub(crate) fn resolve_pane_target_ref(
    state: &HandlerState,
    target: &PaneTargetRef,
) -> Result<PaneTarget, RmuxError> {
    match target {
        PaneTargetRef::Slot(target) => Ok(target.clone()),
        PaneTargetRef::Id {
            session_name,
            pane_id,
        } => resolve_pane_id(state, session_name, *pane_id),
    }
}

fn resolve_pane_id(
    state: &HandlerState,
    session_name: &rmux_proto::SessionName,
    pane_id: rmux_proto::PaneId,
) -> Result<PaneTarget, RmuxError> {
    let session = state
        .sessions
        .session(session_name)
        .ok_or_else(|| session_not_found(session_name))?;
    let window_index = session
        .window_index_for_pane_id(pane_id)
        .ok_or_else(|| RmuxError::pane_not_found(session_name.clone(), pane_id))?;
    let pane_index = session
        .window_at(window_index)
        .and_then(|window| {
            window
                .panes()
                .iter()
                .find(|pane| pane.id() == pane_id)
                .map(|pane| pane.index())
        })
        .ok_or_else(|| RmuxError::pane_not_found(session_name.clone(), pane_id))?;
    Ok(PaneTarget::with_window(
        session_name.clone(),
        window_index,
        pane_index,
    ))
}