rmux 0.1.1

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

use rmux_client::{connect_or_absent, ConnectResult};
use rmux_core::{
    formats::{render_template, FormatContext},
    key_code_lookup_bits, key_string_lookup_string, KeyBindingDisplay, KeyBindingSortOrder,
    KeyBindingStore, KEYC_NONE, KEYC_UNKNOWN, LIST_KEYS_TEMPLATE,
};
use rmux_proto::{
    BindKeyRequest, CommandOutput, ListKeysRequest, SendKeysExtRequest, UnbindKeyRequest,
};

use super::{
    expect_command_output, resolve_pane_target_spec, run_command, run_command_resolved,
    write_command_output, ExitFailure,
};
use crate::cli_args::{BindKeyArgs, ListKeysArgs, SendKeysArgs, SendPrefixArgs, UnbindKeyArgs};

pub(super) fn run_send_keys(args: SendKeysArgs, socket_path: &Path) -> Result<i32, ExitFailure> {
    if send_keys_uses_legacy_path(&args) {
        let target = args
            .target
            .clone()
            .expect("legacy send-keys path requires explicit target");
        return run_command_resolved(socket_path, "send-keys", move |connection| {
            let target = resolve_pane_target_spec(connection, &target)?;
            connection
                .send_keys(target, args.keys)
                .map_err(ExitFailure::from_client)
        });
    }

    run_command_resolved(socket_path, "send-keys", move |connection| {
        let target = args
            .target
            .as_ref()
            .map(|target| resolve_pane_target_spec(connection, target))
            .transpose()?;
        connection
            .send_keys_extended(SendKeysExtRequest {
                target,
                keys: args.keys,
                expand_formats: args.expand_formats,
                hex: args.hex,
                literal: args.literal,
                dispatch_key_table: args.key_table,
                copy_mode_command: args.copy_mode,
                forward_mouse_event: args.mouse,
                reset_terminal: args.reset_terminal,
                repeat_count: args.repeat_count,
            })
            .map_err(ExitFailure::from_client)
    })
}

fn send_keys_uses_legacy_path(args: &SendKeysArgs) -> bool {
    args.target.is_some()
        && !args.expand_formats
        && !args.hex
        && !args.literal
        && !args.key_table
        && !args.mouse
        && args.repeat_count.is_none()
        && !args.reset_terminal
        && !args.copy_mode
}

pub(super) fn run_bind_key(args: BindKeyArgs, socket_path: &Path) -> Result<i32, ExitFailure> {
    run_command(socket_path, "bind-key", move |connection| {
        connection.bind_key(BindKeyRequest {
            table_name: args.table_name(),
            key: args.key,
            note: args.note,
            repeat: args.repeat,
            command: (!args.command.is_empty()).then_some(args.command),
        })
    })
}

pub(super) fn run_unbind_key(args: UnbindKeyArgs, socket_path: &Path) -> Result<i32, ExitFailure> {
    run_command(socket_path, "unbind-key", move |connection| {
        connection.unbind_key(UnbindKeyRequest {
            table_name: args.table_name(),
            all: args.all,
            key: args.key,
            quiet: args.quiet,
        })
    })
}

pub(super) fn run_list_keys(args: ListKeysArgs, socket_path: &Path) -> Result<i32, ExitFailure> {
    let request = ListKeysRequest {
        table_name: args.table_name,
        first_only: args.first_only,
        notes: args.notes,
        include_unnoted: args.include_unnoted,
        reversed: args.reversed,
        format: args.format,
        sort_order: args.sort_order,
        prefix: args.prefix,
        key: args.key,
    };

    match connect_or_absent(socket_path).map_err(ExitFailure::from_client)? {
        ConnectResult::Connected(mut connection) => {
            let response = connection
                .list_keys(request)
                .map_err(ExitFailure::from_client)?;
            let output = expect_command_output(&response, "list-keys")?;
            write_command_output(output)?;
            Ok(0)
        }
        ConnectResult::Absent => run_default_list_keys(request),
    }
}

fn run_default_list_keys(request: ListKeysRequest) -> Result<i32, ExitFailure> {
    let sort_order = match request.sort_order.as_deref() {
        Some(value) => KeyBindingSortOrder::parse(value)
            .ok_or_else(|| ExitFailure::new(1, format!("invalid sort order: {value}")))?,
        None => KeyBindingSortOrder::default(),
    };
    let filter_key = match request.key.as_deref() {
        Some(key) => match key_string_lookup_string(key) {
            Some(key) if key != KEYC_NONE && key != KEYC_UNKNOWN => Some(key_code_lookup_bits(key)),
            _ => return Err(ExitFailure::new(1, format!("unknown key: {key}"))),
        },
        None => None,
    };
    let store = KeyBindingStore::default();
    let mut bindings = list_default_key_bindings(&store, &request, sort_order);
    if let Some(filter_key) = filter_key {
        bindings.retain(|binding| key_code_lookup_bits(binding.binding().key()) == filter_key);
        if bindings.is_empty() {
            let key = request.key.as_deref().unwrap_or_default();
            return Err(ExitFailure::new(1, format!("unknown key: {key}")));
        }
    }
    if request.notes && !request.include_unnoted {
        bindings.retain(|binding| binding.binding().note().is_some());
    }
    if request.first_only {
        bindings.truncate(1);
    }

    let render_metrics = ListKeysRenderMetrics::from_bindings(&bindings);
    let output = render_default_list_keys_output(&bindings, &request, render_metrics);
    write_command_output(&output)?;
    Ok(0)
}

fn list_default_key_bindings(
    store: &KeyBindingStore,
    request: &ListKeysRequest,
    sort_order: KeyBindingSortOrder,
) -> Vec<KeyBindingDisplay> {
    if request.notes && request.table_name.is_none() {
        store
            .list_bindings(None, sort_order, request.reversed)
            .into_iter()
            .filter(|binding| matches!(binding.table_name(), "prefix" | "root"))
            .collect()
    } else {
        store.list_bindings(request.table_name.as_deref(), sort_order, request.reversed)
    }
}

fn render_default_list_keys_output(
    bindings: &[KeyBindingDisplay],
    request: &ListKeysRequest,
    render_metrics: ListKeysRenderMetrics,
) -> CommandOutput {
    let template = request.format.as_deref().unwrap_or(LIST_KEYS_TEMPLATE);
    let lines = bindings
        .iter()
        .map(|binding| {
            if request.format.is_none() && request.key.is_some() && !request.notes {
                return render_default_key_filtered_binding_line(binding, render_metrics);
            }
            let key_has_repeat = if request.key.is_some() {
                binding.binding().repeat()
            } else {
                render_metrics.has_repeat
            };
            let context = FormatContext::new()
                .with_named_value("key_repeat", bool_format(binding.binding().repeat()))
                .with_named_value("key_note", binding.binding().note().unwrap_or_default())
                .with_named_value("key_prefix", request.prefix.clone().unwrap_or_default())
                .with_named_value("key_table", binding.table_name())
                .with_named_value("key_string", binding.key_string())
                .with_named_value("key_command", binding.command_string())
                .with_named_value("notes_only", bool_format(request.notes))
                .with_named_value("key_has_repeat", bool_format(key_has_repeat))
                .with_named_value(
                    "key_string_width",
                    render_metrics.key_string_width.to_string(),
                )
                .with_named_value(
                    "key_table_width",
                    render_metrics.key_table_width.to_string(),
                );
            render_template(template, &context)
        })
        .collect::<Vec<_>>();
    command_output_from_lines(&lines)
}

fn render_default_key_filtered_binding_line(
    binding: &KeyBindingDisplay,
    render_metrics: ListKeysRenderMetrics,
) -> String {
    let repeat = if binding.binding().repeat() {
        " -r"
    } else {
        ""
    };
    format!(
        "bind-key{repeat} -T {table:<table_width$} {key:<key_width$} {command}",
        table = binding.table_name(),
        table_width = render_metrics.key_table_width,
        key = binding.key_string(),
        key_width = render_metrics.key_string_width,
        command = binding.command_string()
    )
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ListKeysRenderMetrics {
    key_string_width: usize,
    key_table_width: usize,
    has_repeat: bool,
}

impl ListKeysRenderMetrics {
    fn from_bindings(bindings: &[KeyBindingDisplay]) -> Self {
        Self {
            key_string_width: KeyBindingStore::key_string_width(bindings),
            key_table_width: KeyBindingStore::key_table_width(bindings),
            has_repeat: KeyBindingStore::has_repeat(bindings),
        }
    }
}

fn command_output_from_lines(lines: &[String]) -> CommandOutput {
    if lines.is_empty() {
        CommandOutput::from_stdout(Vec::new())
    } else {
        CommandOutput::from_stdout(format!("{}\n", lines.join("\n")).into_bytes())
    }
}

fn bool_format(value: bool) -> &'static str {
    if value {
        "1"
    } else {
        "0"
    }
}

pub(super) fn run_send_prefix(
    args: SendPrefixArgs,
    socket_path: &Path,
) -> Result<i32, ExitFailure> {
    run_command_resolved(socket_path, "send-prefix", move |connection| {
        let target = args
            .target
            .as_ref()
            .map(|target| resolve_pane_target_spec(connection, target))
            .transpose()?;
        connection
            .send_prefix(target, args.secondary)
            .map_err(ExitFailure::from_client)
    })
}