rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::{PaneId, Session};
use rmux_proto::{
    BreakPaneRequest, BreakPaneResponse, JoinPaneRequest, JoinPaneResponse, LastPaneResponse,
    MovePaneRequest, MovePaneResponse, PaneTarget, RmuxError, SplitDirection, SwapPaneDirection,
    SwapPaneRequest, SwapPaneResponse, WindowTarget,
};

use super::{session_mutation::PaneTransferGeometryContext, session_not_found, HandlerState};

#[path = "pane_transfer/cross_session.rs"]
mod cross_session;
#[path = "pane_transfer/grouped.rs"]
mod grouped;
#[path = "pane_transfer/linked_last.rs"]
mod linked_last;
#[path = "pane_transfer/window_metadata.rs"]
mod window_metadata;

impl HandlerState {
    pub(crate) fn last_pane(
        &mut self,
        target: WindowTarget,
        preserve_zoom: bool,
        input_disabled: Option<bool>,
    ) -> Result<(LastPaneResponse, Vec<rmux_proto::SessionName>), RmuxError> {
        let session_name = target.session_name().clone();
        let window_index = target.window_index();
        let (pane_index, synchronized_sessions) = self
            .mutate_session_and_resize_window_terminal_with_family(
                &session_name,
                window_index,
                |session| session.last_pane_in_window_with_zoom(window_index, preserve_zoom),
            )?;
        let response_target = PaneTarget::with_window(session_name, window_index, pane_index);
        if let Some(disabled) = input_disabled {
            self.set_pane_input_disabled(&response_target, disabled)?;
        }

        Ok((
            LastPaneResponse {
                target: response_target,
            },
            synchronized_sessions,
        ))
    }

    pub(crate) fn swap_pane(
        &mut self,
        request: SwapPaneRequest,
    ) -> Result<SwapPaneResponse, RmuxError> {
        let (source, target) = resolve_swap_targets(&self.sessions, &request)?;
        if pane_targets_share_window_identity(&self.sessions, &source, &target) {
            let normalized_target = PaneTarget::with_window(
                source.session_name().clone(),
                source.window_index(),
                target.pane_index(),
            );
            self.swap_pane_within_group(
                source.clone(),
                normalized_target,
                request.detached,
                request.preserve_zoom,
            )?;
            return Ok(SwapPaneResponse { source, target });
        }
        if sessions_share_grouped_window_state(
            &self.sessions,
            source.session_name(),
            target.session_name(),
        ) {
            self.swap_pane_within_group(source, target, request.detached, request.preserve_zoom)
        } else {
            self.swap_pane_across_sessions(source, target, request.detached, request.preserve_zoom)
        }
    }

    pub(crate) fn join_pane(
        &mut self,
        request: JoinPaneRequest,
    ) -> Result<JoinPaneResponse, RmuxError> {
        let geometry_context = PaneTransferGeometryContext::new(&request.source, &request.target);
        self.mutate_join_or_move_and_record_window_geometry_changes(geometry_context, |state| {
            state.join_pane_unrecorded(request)
        })
    }

    fn join_pane_unrecorded(
        &mut self,
        request: JoinPaneRequest,
    ) -> Result<JoinPaneResponse, RmuxError> {
        if request.source == request.target {
            return Err(RmuxError::Server(
                "source and target panes must be different".to_owned(),
            ));
        }
        if pane_targets_share_window_identity(&self.sessions, &request.source, &request.target) {
            let mut normalized_request = request;
            normalized_request.source = PaneTarget::with_window(
                normalized_request.target.session_name().clone(),
                normalized_request.target.window_index(),
                normalized_request.source.pane_index(),
            );
            return self.join_pane_within_group(normalized_request);
        }
        if sessions_share_grouped_window_state(
            &self.sessions,
            request.source.session_name(),
            request.target.session_name(),
        ) {
            return self.join_pane_within_group(request);
        }

        self.join_pane_across_sessions(request)
    }

    pub(crate) fn move_pane(
        &mut self,
        request: MovePaneRequest,
    ) -> Result<MovePaneResponse, RmuxError> {
        let geometry_context = PaneTransferGeometryContext::new(&request.source, &request.target);
        self.mutate_join_or_move_and_record_window_geometry_changes(geometry_context, |state| {
            let response = state.join_pane_unrecorded(JoinPaneRequest {
                source: request.source,
                target: request.target,
                direction: request.direction,
                detached: request.detached,
                before: request.before,
                full_size: request.full_size,
                size: request.size,
            })?;
            Ok(MovePaneResponse {
                target: response.target,
            })
        })
    }

    pub(crate) fn break_pane(
        &mut self,
        mut request: BreakPaneRequest,
    ) -> Result<BreakPaneResponse, RmuxError> {
        let explicit_name = request.name.is_some();
        let destination_session_name = request.target.as_ref().map_or_else(
            || request.source.session_name().clone(),
            |target| target.session_name().clone(),
        );
        if request.target.is_none() && !(request.after || request.before) {
            let destination_index = self.first_available_window_index(&destination_session_name)?;
            request.target = Some(WindowTarget::with_window(
                destination_session_name.clone(),
                destination_index,
            ));
        }
        let shares_grouped_window_state = sessions_share_grouped_window_state(
            &self.sessions,
            request.source.session_name(),
            &destination_session_name,
        );

        if request.source.session_name() != &destination_session_name
            && shares_grouped_window_state
            && pane_is_only_pane_in_window(&self.sessions, &request.source)
        {
            return Err(RmuxError::Server("sessions are grouped".to_owned()));
        }

        let creates_window = self
            .sessions
            .session(request.source.session_name())
            .and_then(|session| session.window_at(request.source.window_index()))
            .is_some_and(|window| window.pane_count() > 1);
        let initial_name = if !explicit_name
            && creates_window
            && !crate::automatic_rename::automatic_rename_enabled_for_new_window(
                &self.options,
                &destination_session_name,
            ) {
            self.pane_runtime_window_name_in_window(
                request.source.session_name(),
                request.source.window_index(),
                request.source.pane_index(),
            )?
            .filter(|name| !name.is_empty())
        } else {
            None
        };
        if let Some(name) = initial_name.as_ref() {
            // Feed the stored runtime name into the core creation transaction.
            // The original request remains implicit for marker semantics.
            request.name = Some(name.clone());
        }

        let response = if shares_grouped_window_state {
            self.break_pane_within_group(request, destination_session_name)
        } else {
            self.break_pane_across_sessions(request, destination_session_name)
        }?;
        if explicit_name {
            // `-n` pins the destination name. A moved window can carry the
            // source's auto-name marker, which would otherwise override the
            // explicit name on the next pane activity callback.
            self.clear_auto_named_window_family(
                response.target.session_name(),
                response.target.window_index(),
            );
        } else if initial_name.is_some() {
            self.mark_auto_named_window(
                response.target.session_name(),
                response.target.window_index(),
            );
        }
        Ok(response)
    }
}

fn pane_targets_share_window_identity(
    sessions: &rmux_core::SessionStore,
    first: &PaneTarget,
    second: &PaneTarget,
) -> bool {
    let first_window_id = sessions
        .session(first.session_name())
        .and_then(|session| session.window_at(first.window_index()))
        .map(rmux_core::Window::id);
    let second_window_id = sessions
        .session(second.session_name())
        .and_then(|session| session.window_at(second.window_index()))
        .map(rmux_core::Window::id);
    first_window_id.is_some() && first_window_id == second_window_id
}

fn pane_is_only_pane_in_window(sessions: &rmux_core::SessionStore, target: &PaneTarget) -> bool {
    let Some(session) = sessions.session(target.session_name()) else {
        return false;
    };
    let Some(window) = session.window_at(target.window_index()) else {
        return false;
    };

    window.panes().len() == 1
        && session
            .pane_id_in_window(target.window_index(), target.pane_index())
            .is_some()
}

fn sessions_share_grouped_window_state(
    sessions: &rmux_core::SessionStore,
    first: &rmux_proto::SessionName,
    second: &rmux_proto::SessionName,
) -> bool {
    if first == second {
        return true;
    }

    let Some(first_group) = sessions.session_group_name(first) else {
        return false;
    };
    sessions.session_group_name(second) == Some(first_group)
}

fn join_pane_internal_direction(direction: SplitDirection) -> SplitDirection {
    match direction {
        SplitDirection::Horizontal => SplitDirection::Vertical,
        SplitDirection::Vertical => SplitDirection::Horizontal,
    }
}

fn pane_id_for_target(session: &Session, target: &PaneTarget) -> Result<PaneId, RmuxError> {
    session
        .pane_id_in_window(target.window_index(), target.pane_index())
        .ok_or_else(|| {
            RmuxError::invalid_target(target.to_string(), "pane index does not exist in session")
        })
}

fn pane_index_for_id(session: &Session, window_index: u32, pane_id: PaneId) -> Option<u32> {
    session.window_at(window_index).and_then(|window| {
        window
            .panes()
            .iter()
            .find(|pane| pane.id() == pane_id)
            .map(|pane| pane.index())
    })
}

fn pane_target_for_id(
    state: &HandlerState,
    session_name: &rmux_proto::SessionName,
    pane_id: 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::Server("moved pane disappeared after pane transfer".to_owned())
    })?;
    let pane_index = pane_index_for_id(session, window_index, pane_id).ok_or_else(|| {
        RmuxError::Server("moved pane index disappeared after pane transfer".to_owned())
    })?;
    Ok(PaneTarget::with_window(
        session_name.clone(),
        window_index,
        pane_index,
    ))
}

fn resolve_swap_targets(
    sessions: &rmux_core::SessionStore,
    request: &SwapPaneRequest,
) -> Result<(PaneTarget, PaneTarget), RmuxError> {
    if let Some(direction) = request.direction {
        let anchor = &request.target;
        let session = sessions
            .session(anchor.session_name())
            .ok_or_else(|| session_not_found(anchor.session_name()))?;
        let window = session.window_at(anchor.window_index()).ok_or_else(|| {
            RmuxError::invalid_target(
                format!("{}:{}", anchor.session_name(), anchor.window_index()),
                "window index does not exist in session",
            )
        })?;
        let anchor_position = window
            .panes()
            .iter()
            .position(|pane| pane.index() == anchor.pane_index())
            .ok_or_else(|| {
                RmuxError::invalid_target(
                    anchor.to_string(),
                    "pane index does not exist in session",
                )
            })?;
        let pane_count = window.pane_count();
        let source_position = match direction {
            SwapPaneDirection::Down => (anchor_position + 1) % pane_count,
            SwapPaneDirection::Up => (anchor_position + pane_count - 1) % pane_count,
        };
        let source_pane_index = window
            .panes()
            .get(source_position)
            .expect("resolved pane position must exist")
            .index();

        return Ok((
            PaneTarget::with_window(
                anchor.session_name().clone(),
                anchor.window_index(),
                source_pane_index,
            ),
            anchor.clone(),
        ));
    }

    Ok((request.source.clone(), request.target.clone()))
}