rmux-server 0.1.1

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

use super::{session_not_found, HandlerState};

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

impl HandlerState {
    pub(crate) fn last_pane(
        &mut self,
        target: WindowTarget,
    ) -> Result<LastPaneResponse, RmuxError> {
        let session = self
            .sessions
            .session_mut(target.session_name())
            .ok_or_else(|| session_not_found(target.session_name()))?;
        let pane_index = session.last_pane_in_window(target.window_index())?;

        Ok(LastPaneResponse {
            target: PaneTarget::with_window(
                target.session_name().clone(),
                target.window_index(),
                pane_index,
            ),
        })
    }

    pub(crate) fn swap_pane(
        &mut self,
        request: SwapPaneRequest,
    ) -> Result<SwapPaneResponse, RmuxError> {
        let (source, target) = resolve_swap_targets(&self.sessions, &request)?;
        if source.session_name() == target.session_name() {
            let session_name = source.session_name().clone();
            self.mutate_session_and_resize_terminals(&session_name, |session| {
                session.swap_panes(
                    SessionPaneTarget::from(&source),
                    SessionPaneTarget::from(&target),
                    PaneSwapOptions::new(request.detached, request.preserve_zoom),
                )?;
                Ok(SwapPaneResponse {
                    source: source.clone(),
                    target: target.clone(),
                })
            })
        } 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> {
        if request.source == request.target {
            return Err(RmuxError::Server(
                "source and target panes must be different".to_owned(),
            ));
        }
        if request.source.session_name() == request.target.session_name() {
            let session_name = request.source.session_name().clone();
            let target = request.target.clone();
            let moved_pane_id = self
                .sessions
                .session(&session_name)
                .ok_or_else(|| session_not_found(&session_name))
                .and_then(|session| pane_id_for_target(session, &request.source))?;
            let direction = join_pane_internal_direction(request.direction);
            return self.mutate_session_and_resize_terminals(&session_name, |session| {
                session.join_pane(
                    SessionPaneTarget::from(&request.source),
                    SessionPaneTarget::from(&request.target),
                    PaneJoinOptions::new(
                        direction,
                        request.detached,
                        request.before,
                        request.full_size,
                        request.size,
                    ),
                )?;
                let moved_index = pane_index_for_id(session, target.window_index(), moved_pane_id)
                    .ok_or_else(|| {
                        RmuxError::Server("moved pane disappeared after join-pane".to_owned())
                    })?;
                Ok(JoinPaneResponse {
                    target: PaneTarget::with_window(
                        session_name.clone(),
                        target.window_index(),
                        moved_index,
                    ),
                })
            });
        }

        self.join_pane_across_sessions(request)
    }

    pub(crate) fn move_pane(
        &mut self,
        request: MovePaneRequest,
    ) -> Result<MovePaneResponse, RmuxError> {
        let response = self.join_pane(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,
        request: BreakPaneRequest,
    ) -> Result<BreakPaneResponse, RmuxError> {
        let destination_session_name = request.target.as_ref().map_or_else(
            || request.source.session_name().clone(),
            |target| target.session_name().clone(),
        );

        if request.source.session_name() == &destination_session_name {
            let session_name = request.source.session_name().clone();
            let destination_index =
                self.mutate_session_and_resize_terminals(&session_name, |session| {
                    session.break_pane(
                        SessionPaneTarget::from(&request.source),
                        BreakPaneOptions::new(
                            request.target.as_ref().map(WindowTarget::window_index),
                            request.name.clone(),
                            request.detached,
                            request.after,
                            request.before,
                        ),
                    )
                })?;

            return Ok(BreakPaneResponse {
                target: PaneTarget::with_window(destination_session_name, destination_index, 0),
                output: None,
            });
        }

        self.break_pane_across_sessions(request, destination_session_name)
    }
}

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 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()))
}