rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_proto::{Request, RmuxError, SendKeysRequest};

use super::parse_pane_target;
use super::tokens::CommandTokens;
use super::values::{missing_argument, parse_usize};

pub(super) fn parse_send_keys(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut target = None;
    let mut expand_formats = false;
    let mut hex = false;
    let mut literal = false;
    let mut dispatch_key_table = false;
    let mut copy_mode_command = false;
    let mut forward_mouse_event = false;
    let mut reset_terminal = false;
    let mut repeat_count = None;

    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-F" => {
                let _ = args.optional();
                expand_formats = true;
            }
            "-H" => {
                let _ = args.optional();
                hex = true;
            }
            "-l" => {
                let _ = args.optional();
                literal = true;
            }
            "-K" => {
                let _ = args.optional();
                dispatch_key_table = true;
            }
            "-M" => {
                let _ = args.optional();
                forward_mouse_event = true;
            }
            "-N" => {
                let _ = args.optional();
                repeat_count = Some(parse_usize("send-keys", "-N", &args.required("-N count")?)?);
            }
            value if value.starts_with("-N") && value.len() > 2 => {
                let count = value[2..].to_owned();
                let _ = args.optional();
                repeat_count = Some(parse_usize("send-keys", "-N", &count)?);
            }
            "-R" => {
                let _ = args.optional();
                reset_terminal = true;
            }
            "-X" => {
                let _ = args.optional();
                copy_mode_command = true;
            }
            "-t" => {
                let _ = args.optional();
                target = Some(parse_pane_target("send-keys", args.required("-t target")?)?);
            }
            _ => break,
        }
    }

    let keys = args.remaining();
    if target.is_some()
        && !expand_formats
        && !hex
        && !literal
        && !dispatch_key_table
        && !copy_mode_command
        && !forward_mouse_event
        && !reset_terminal
        && repeat_count.is_none()
    {
        return Ok(Request::SendKeys(SendKeysRequest {
            target: target.ok_or_else(|| missing_argument("send-keys", "-t target"))?,
            keys,
        }));
    }

    Ok(Request::SendKeysExt(rmux_proto::SendKeysExtRequest {
        target,
        keys,
        expand_formats,
        hex,
        literal,
        dispatch_key_table,
        copy_mode_command,
        forward_mouse_event,
        reset_terminal,
        repeat_count,
    }))
}

pub(super) fn parse_bind_key(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut table_name = None;
    let mut note = None;
    let mut repeat = false;

    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-n" => {
                let _ = args.optional();
                table_name = Some("root".to_owned());
            }
            "-r" => {
                let _ = args.optional();
                repeat = true;
            }
            "-N" => {
                let _ = args.optional();
                note = Some(args.required("-N note")?);
            }
            "-T" => {
                let _ = args.optional();
                table_name = Some(args.required("-T key-table")?);
            }
            _ => break,
        }
    }

    let key = args.required("key")?;
    Ok(Request::BindKey(rmux_proto::BindKeyRequest {
        table_name: table_name.unwrap_or_else(|| "prefix".to_owned()),
        key,
        note,
        repeat,
        command: (!args.is_empty()).then_some(args.remaining()),
    }))
}

pub(super) fn parse_unbind_key(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut table_name = None;
    let mut all = false;
    let mut quiet = false;

    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-a" => {
                let _ = args.optional();
                all = true;
            }
            "-n" => {
                let _ = args.optional();
                table_name = Some("root".to_owned());
            }
            "-q" => {
                let _ = args.optional();
                quiet = true;
            }
            "-T" => {
                let _ = args.optional();
                table_name = Some(args.required("-T key-table")?);
            }
            _ => break,
        }
    }

    let key = args.optional();
    args.no_extra("unbind-key")?;
    Ok(Request::UnbindKey(rmux_proto::UnbindKeyRequest {
        table_name: table_name.unwrap_or_else(|| "prefix".to_owned()),
        all,
        key,
        quiet,
    }))
}

pub(super) fn parse_list_keys(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut table_name = None;
    let mut first_only = false;
    let mut include_unnoted = false;
    let mut notes = false;
    let mut reversed = false;
    let mut format = None;
    let mut sort_order = None;
    let mut prefix = None;

    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-1" => {
                let _ = args.optional();
                first_only = true;
            }
            "-a" => {
                let _ = args.optional();
                include_unnoted = true;
            }
            "-N" => {
                let _ = args.optional();
                notes = true;
            }
            "-r" => {
                let _ = args.optional();
                reversed = true;
            }
            "-F" => {
                let _ = args.optional();
                format = Some(args.required("-F format")?);
            }
            "-O" => {
                let _ = args.optional();
                sort_order = Some(args.required("-O sort-order")?);
            }
            "-P" => {
                let _ = args.optional();
                prefix = Some(args.required("-P prefix")?);
            }
            "-T" => {
                let _ = args.optional();
                table_name = Some(args.required("-T key-table")?);
            }
            _ => break,
        }
    }

    let key = args.optional();
    args.no_extra("list-keys")?;
    Ok(Request::ListKeys(rmux_proto::ListKeysRequest {
        table_name,
        first_only,
        notes,
        include_unnoted,
        reversed,
        format,
        sort_order,
        prefix,
        key,
    }))
}

pub(super) fn parse_send_prefix(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut secondary = false;
    let mut target = None;

    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-2" => {
                let _ = args.optional();
                secondary = true;
            }
            "-t" => {
                let _ = args.optional();
                target = Some(parse_pane_target(
                    "send-prefix",
                    args.required("-t target")?,
                )?);
            }
            _ => break,
        }
    }
    args.no_extra("send-prefix")?;
    Ok(Request::SendPrefix(rmux_proto::SendPrefixRequest {
        target,
        secondary,
    }))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn token(value: &str) -> String {
        value.to_owned()
    }

    #[test]
    fn parse_send_keys_accepts_tmux_compact_repeat_count() {
        let request = parse_send_keys(CommandTokens::new(vec![
            token("-N5"),
            token("-X"),
            token("scroll-up"),
        ]))
        .expect("compact repeat send-keys parses");

        let Request::SendKeysExt(request) = request else {
            panic!("compact repeat must use extended send-keys request");
        };
        assert_eq!(request.repeat_count, Some(5));
        assert!(request.copy_mode_command);
        assert_eq!(request.keys, vec!["scroll-up"]);
    }
}