rmux 0.10.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use std::path::Path;

use rmux_client::{connect, Connection};
use rmux_proto::{CapturePaneRequest, CapturePaneTargetActionRequest, Response};

use crate::cli_args::{CapturePaneArgs, TargetSpec};

use super::{
    capture_target_action_needs_legacy_retry, cli_target_actions_enabled,
    resolve_pane_target_or_current, ExitFailure,
};

#[derive(Clone)]
pub(super) struct PendingCapturePaneRequest {
    target: Option<TargetSpec>,
    start: Option<i64>,
    end: Option<i64>,
    print: bool,
    buffer_name: Option<String>,
    alternate: bool,
    escape_ansi: bool,
    escape_sequences: bool,
    include_format: bool,
    hyperlinks: bool,
    line_numbers: bool,
    join_wrapped: bool,
    use_mode_screen: bool,
    preserve_trailing_spaces: bool,
    do_not_trim_spaces: bool,
    pending_input: bool,
    quiet: bool,
    start_is_absolute: bool,
    end_is_absolute: bool,
}

pub(super) fn capture_pane_request(
    args: CapturePaneArgs,
) -> Result<PendingCapturePaneRequest, ExitFailure> {
    let (start, start_is_absolute) = parse_capture_bound(args.start.as_deref(), "-S")?;
    let (end, end_is_absolute) = parse_capture_bound(args.end.as_deref(), "-E")?;

    Ok(PendingCapturePaneRequest {
        target: args.target,
        start,
        end,
        print: args.print,
        buffer_name: args.buffer_name,
        alternate: args.alternate,
        escape_ansi: args.escape_ansi,
        escape_sequences: args.escape_sequences,
        include_format: args.include_format,
        hyperlinks: args.hyperlinks,
        line_numbers: args.line_numbers,
        join_wrapped: args.join_wrapped,
        use_mode_screen: args.use_mode_screen,
        preserve_trailing_spaces: args.preserve_trailing_spaces,
        do_not_trim_spaces: args.do_not_trim_spaces,
        pending_input: args.pending_input,
        quiet: args.quiet,
        start_is_absolute,
        end_is_absolute,
    })
}

pub(super) fn build_capture_pane_request(
    connection: &mut Connection,
    request: PendingCapturePaneRequest,
) -> Result<CapturePaneRequest, ExitFailure> {
    Ok(CapturePaneRequest {
        target: resolve_pane_target_or_current(
            connection,
            request.target.as_ref(),
            "capture-pane",
        )?,
        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,
    })
}

pub(super) fn build_capture_pane_target_action_request(
    request: PendingCapturePaneRequest,
) -> CapturePaneTargetActionRequest {
    CapturePaneTargetActionRequest {
        target: request
            .target
            .as_ref()
            .map(|target| target.raw().to_owned()),
        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,
    }
}

pub(super) fn send_capture_pane_request(
    connection: &mut Connection,
    socket_path: &Path,
    request: PendingCapturePaneRequest,
) -> Result<Response, ExitFailure> {
    if !cli_target_actions_enabled() {
        let request = build_capture_pane_request(connection, request)?;
        return connection
            .capture_pane(request)
            .map_err(ExitFailure::from_client);
    }

    let legacy_request = request.clone();
    let response =
        connection.capture_pane_target_action(build_capture_pane_target_action_request(request));
    if !capture_target_action_needs_legacy_retry(&response) {
        return response.map_err(ExitFailure::from_client);
    }

    let mut legacy_connection = connect(socket_path)
        .map_err(|error| ExitFailure::from_client_connect(socket_path, error))?;
    let request = build_capture_pane_request(&mut legacy_connection, legacy_request)?;
    legacy_connection
        .capture_pane(request)
        .map_err(ExitFailure::from_client)
}

fn parse_capture_bound(
    value: Option<&str>,
    flag: &str,
) -> Result<(Option<i64>, bool), ExitFailure> {
    match value {
        None => Ok((None, false)),
        Some("-") => Ok((None, true)),
        Some(value) => value
            .parse::<i64>()
            .map(|value| (Some(value), false))
            .map_err(|_| {
                ExitFailure::new(1, format!("command capture-pane: {flag} expects a number"))
            }),
    }
}