rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
//! Handler-state side of `split-window`.
//!
//! Pulled out of `pane_lifecycle.rs` so the parent file stays compact and so
//! the split flow — which is dense with rollback paths — owns its own
//! module. Keeps the same `impl HandlerState` so callers see no API change.

use std::path::Path;

use rmux_proto::{PaneTarget, RmuxError, SplitDirection, SplitWindowResponse, SplitWindowTarget};

use crate::pane_io::{PaneAlertCallback, PaneExitCallback};
use crate::pane_terminal_lookup::missing_pane_terminal;
use crate::pane_terminal_process::open_pane_terminal;
use crate::terminal::TerminalProfile;

use super::super::lifecycle_state::terminal_size_from_geometry;
use super::super::{
    pane_terminal_geometry_for_session, session_not_found, HandlerState, PaneLifecycleSpawn,
    PaneOutputSpawn,
};
#[cfg(windows)]
use super::clone_terminal_for_exit_watcher;
use super::clone_terminal_for_output_reader;
use super::preview::{preview_split, split_window_internal_direction, split_window_session_name};

impl HandlerState {
    /// Splits the addressed pane, spawning a new pane terminal.
    ///
    /// `before` controls whether the new pane is inserted before (tmux `-b`)
    /// or after the target on the chosen axis.
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn split_window(
        &mut self,
        target: SplitWindowTarget,
        direction: SplitDirection,
        before: bool,
        socket_path: &Path,
        environment_overrides: Option<&[String]>,
        command: Option<&[String]>,
        pane_alert_callback: Option<PaneAlertCallback>,
        pane_exit_callback: Option<PaneExitCallback>,
    ) -> Result<SplitWindowResponse, RmuxError> {
        let session_name = split_window_session_name(&target).clone();
        let internal_direction = split_window_internal_direction(direction);
        let previous_session = self
            .sessions
            .session(&session_name)
            .cloned()
            .ok_or_else(|| session_not_found(&session_name))?;
        let (window_index, new_pane_index, _preview_pane_geometry) =
            preview_split(&self.sessions, &target, internal_direction, before)?;
        let runtime_session_name =
            self.runtime_session_name_for_window(&session_name, window_index);
        let new_pane_id = self.sessions.allocate_pane_id();
        let (session_id, window_id, new_pane_id, new_pane_geometry, requested_cwd) = self
            .commit_split_into_session(
                &session_name,
                &target,
                window_index,
                new_pane_id,
                new_pane_index,
                internal_direction,
                before,
            )?;

        let profile = TerminalProfile::for_session(
            &self.environment,
            &self.options,
            &session_name,
            session_id.as_u32(),
            socket_path,
            true,
            environment_overrides,
            Some(new_pane_id),
            requested_cwd.as_deref(),
        )?;
        let runtime_window_name = profile.runtime_window_name(command);
        let lifecycle_cwd = profile.cwd().to_path_buf();
        let terminal = open_pane_terminal(
            new_pane_geometry,
            profile,
            runtime_window_name.clone(),
            command,
        )?;
        let pid = terminal.pid();
        let output_reader =
            match clone_terminal_for_output_reader(&terminal, &session_name, new_pane_id) {
                Ok(output_reader) => output_reader,
                Err(error) => {
                    self.replace_session(&session_name, previous_session)?;
                    return Err(error);
                }
            };
        #[cfg(windows)]
        let exit_watcher =
            match clone_terminal_for_exit_watcher(&terminal, &session_name, new_pane_id) {
                Ok(exit_watcher) => exit_watcher,
                Err(error) => {
                    self.replace_session(&session_name, previous_session)?;
                    return Err(error);
                }
            };

        if let Err(error) = self.terminals.insert_pane(
            runtime_session_name.clone(),
            new_pane_id,
            window_index,
            new_pane_index,
            terminal,
        ) {
            self.replace_session(&session_name, previous_session)?;
            return Err(error);
        }
        if let Err(error) = self.insert_pane_output(
            &runtime_session_name,
            new_pane_id,
            PaneOutputSpawn {
                geometry: new_pane_geometry,
                output_reader,
                #[cfg(windows)]
                exit_watcher: Some(exit_watcher),
                pane_alert_callback,
                pane_exit_callback,
            },
        ) {
            let _ = self
                .terminals
                .remove_pane(&runtime_session_name, new_pane_id);
            self.replace_session(&session_name, previous_session)?;
            return Err(error);
        }

        if let Err(error) = self.resize_terminals(&session_name) {
            let rollback_target =
                PaneTarget::with_window(session_name.clone(), window_index, new_pane_index);
            self.remove_pane_output(&runtime_session_name, new_pane_id);
            if self
                .terminals
                .remove_pane(&runtime_session_name, new_pane_id)
                .is_none()
            {
                return Err(RmuxError::Server(format!(
                    "failed to roll back session {session_name} after {error}: missing pane terminal for {rollback_target}"
                )));
            }

            self.restore_session_after_resize_error(&session_name, previous_session, &error)?;
            return Err(error);
        }

        let sessions_to_synchronize = self
            .window_link_slots_for(&session_name, window_index)
            .into_iter()
            .map(|slot| slot.session_name)
            .collect::<Vec<_>>();
        self.synchronize_linked_window_from_slot(&session_name, window_index)?;
        for synchronized_session in sessions_to_synchronize {
            self.synchronize_session_group_from(&synchronized_session)?;
        }
        self.record_pane_lifecycle_spawn(PaneLifecycleSpawn {
            session_id,
            window_id,
            pane_id: new_pane_id,
            command: command.map(<[String]>::to_vec),
            working_directory: Some(lifecycle_cwd),
            private_environment: environment_overrides.map(<[String]>::to_vec),
            dimensions: terminal_size_from_geometry(new_pane_geometry),
            pid: Some(pid),
        });
        let output_sequence = self.pane_output_generation(&runtime_session_name, new_pane_id);
        self.update_pane_lifecycle_output_sequence(new_pane_id, output_sequence);
        self.sync_pane_lifecycle_dimensions_for_session(&session_name);

        Ok(SplitWindowResponse {
            pane: PaneTarget::with_window(session_name, window_index, new_pane_index),
        })
    }

    /// Applies the split to the real session store and returns ids + geometry
    /// for the freshly committed pane. Isolating this from the surrounding
    /// terminal-spawn logic keeps `split_window` readable.
    #[allow(clippy::too_many_arguments)]
    fn commit_split_into_session(
        &mut self,
        session_name: &rmux_proto::SessionName,
        target: &SplitWindowTarget,
        window_index: u32,
        new_pane_id: rmux_core::PaneId,
        expected_pane_index: u32,
        direction: SplitDirection,
        before: bool,
    ) -> Result<
        (
            rmux_core::SessionId,
            rmux_core::WindowId,
            rmux_core::PaneId,
            rmux_core::PaneGeometry,
            Option<std::path::PathBuf>,
        ),
        RmuxError,
    > {
        // Capture `cwd` as an owned `PathBuf` so the caller can keep it past
        // the `&mut SessionStore` borrow.
        let session = self
            .sessions
            .session_mut(session_name)
            .ok_or_else(|| session_not_found(session_name))?;
        let (target_window_index, target_pane_index) = match target {
            SplitWindowTarget::Session(_) => {
                (session.active_window_index(), session.active_pane_index())
            }
            SplitWindowTarget::Pane(pane) => (pane.window_index(), pane.pane_index()),
        };
        let committed_index = session.split_pane_in_window_with_id_and_direction_before(
            target_window_index,
            target_pane_index,
            new_pane_id,
            direction,
            before,
        )?;
        debug_assert_eq!(committed_index, expected_pane_index);
        let pane = session
            .window_at(window_index)
            .expect("split target window must exist")
            .pane(committed_index)
            .ok_or_else(|| missing_pane_terminal(session_name, window_index, committed_index))?;
        let pane_id = pane.id();
        let pane_geometry =
            pane_terminal_geometry_for_session(session, &self.options, pane.geometry());
        let window_id = session
            .window_at(window_index)
            .expect("split target window must exist")
            .id();
        Ok((
            session.id(),
            window_id,
            pane_id,
            pane_geometry,
            session.cwd().map(std::path::Path::to_path_buf),
        ))
    }
}