rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::path::{Path, PathBuf};

use rmux_core::{SessionStore, TargetFindContext};
use rmux_proto::{DeleteBufferRequest, ListBuffersRequest, LoadBufferRequest, Request, RmuxError};

use super::tokens::{parse_compact_flag_cluster, CommandTokens, CompactFlag};
use super::values::{missing_argument, reject_unknown_option_before_positional, unsupported_flag};
use super::{implicit_pane_target, parse_pane_target};

pub(super) fn parse_set_buffer(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut name = None;
    let mut append = false;
    let mut new_name = None;
    let mut set_clipboard = false;
    let mut target_client = None;
    while let Some(token) = args.peek().map(str::to_owned) {
        match token.as_str() {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-a" => {
                let _ = args.optional();
                append = true;
            }
            "-b" => {
                let _ = args.optional();
                name = Some(args.required("-b buffer name")?);
            }
            "-n" => {
                let _ = args.optional();
                new_name = Some(args.required("-n buffer name")?);
            }
            "-t" => {
                let _ = args.optional();
                target_client = Some(args.required("-t target-client")?);
            }
            "-w" => {
                let _ = args.optional();
                set_clipboard = true;
            }
            _ => {
                let Some(cluster) = parse_compact_flag_cluster("set-buffer", &token, "aw", "bnt")?
                else {
                    reject_unknown_option_before_positional("set-buffer", &token)?;
                    break;
                };
                let _ = args.optional();
                for flag in cluster {
                    match flag {
                        CompactFlag::Bare('a') => append = true,
                        CompactFlag::Bare('w') => set_clipboard = true,
                        compact_flag @ CompactFlag::Value { flag: 'b', .. } => {
                            name = Some(compact_flag.value_or_next(&mut args, "-b buffer name")?);
                        }
                        compact_flag @ CompactFlag::Value { flag: 'n', .. } => {
                            new_name =
                                Some(compact_flag.value_or_next(&mut args, "-n buffer name")?);
                        }
                        compact_flag @ CompactFlag::Value { flag: 't', .. } => {
                            target_client =
                                Some(compact_flag.value_or_next(&mut args, "-t target-client")?);
                        }
                        _ => unreachable!("compact set-buffer flags are prevalidated"),
                    }
                }
            }
        }
    }
    let content_parts = args.remaining();
    if new_name.is_none() && content_parts.is_empty() {
        return Err(missing_argument("set-buffer", "content"));
    }
    let content = content_parts.join(" ");

    Ok(Request::SetBuffer(Box::new(rmux_proto::SetBufferRequest {
        name,
        content: content.into_bytes(),
        append,
        new_name,
        set_clipboard,
        target_client,
    })))
}

pub(super) fn parse_show_buffer(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let name = parse_optional_buffer_name("show-buffer", &mut args)?;
    args.no_extra("show-buffer")?;
    Ok(Request::ShowBuffer(rmux_proto::ShowBufferRequest { name }))
}

pub(super) fn parse_paste_buffer(
    mut args: CommandTokens,
    sessions: &SessionStore,
    find_context: &TargetFindContext,
) -> Result<Request, RmuxError> {
    let mut name = None;
    let mut target = None;
    let mut delete_after = false;
    let mut separator = None;
    let mut linefeed = false;
    let mut raw = false;
    let mut bracketed = false;

    while let Some(token) = args.optional() {
        match token.as_str() {
            "-b" => name = Some(args.required("-b buffer name")?),
            "-t" => {
                target = Some(parse_pane_target(
                    "paste-buffer",
                    args.required("-t target")?,
                )?)
            }
            "-d" => delete_after = true,
            "-p" => bracketed = true,
            "-r" => linefeed = true,
            "-S" => raw = true,
            "-s" => separator = Some(args.required("-s separator")?),
            flag if flag.starts_with('-') => return Err(unsupported_flag("paste-buffer", flag)),
            _ => {
                return Err(RmuxError::Server(format!(
                    "unexpected argument '{token}' for paste-buffer"
                )));
            }
        }
    }

    Ok(Request::PasteBuffer(Box::new(
        rmux_proto::PasteBufferRequest {
            name,
            target: target.unwrap_or(implicit_pane_target(
                sessions,
                find_context,
                "paste-buffer",
            )?),
            delete_after,
            separator,
            linefeed,
            raw,
            bracketed,
        },
    )))
}

pub(super) fn parse_list_buffers(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let mut format = None;
    let mut filter = None;
    let mut sort_order = None;
    let mut reversed = false;

    while let Some(token) = args.optional() {
        match token.as_str() {
            "-F" => format = Some(args.required("-F format")?),
            "-f" => filter = Some(args.required("-f filter")?),
            "-O" => sort_order = Some(args.required("-O order")?),
            "-r" => reversed = true,
            flag if flag.starts_with('-') => return Err(unsupported_flag("list-buffers", flag)),
            _ => {
                return Err(RmuxError::Server(format!(
                    "unexpected argument '{token}' for list-buffers"
                )));
            }
        }
    }

    Ok(Request::ListBuffers(ListBuffersRequest {
        format,
        filter,
        sort_order,
        reversed,
    }))
}

pub(super) fn parse_delete_buffer(mut args: CommandTokens) -> Result<Request, RmuxError> {
    let name = parse_optional_buffer_name("delete-buffer", &mut args)?;
    args.no_extra("delete-buffer")?;
    Ok(Request::DeleteBuffer(DeleteBufferRequest { name }))
}

pub(super) fn parse_load_buffer(
    mut args: CommandTokens,
    caller_cwd: Option<&Path>,
) -> Result<Request, RmuxError> {
    let mut name = None;
    let mut set_clipboard = false;
    let mut target_client = None;
    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-b" => {
                let _ = args.optional();
                name = Some(args.required("-b buffer name")?);
            }
            "-w" => {
                let _ = args.optional();
                set_clipboard = true;
            }
            "-t" => {
                let _ = args.optional();
                target_client = Some(args.required("-t target-client")?);
            }
            flag if flag.starts_with('-') => return Err(unsupported_flag("load-buffer", flag)),
            _ => break,
        }
    }
    let path = args.required("load-buffer path")?;
    args.no_extra("load-buffer")?;
    Ok(Request::LoadBuffer(Box::new(LoadBufferRequest {
        path,
        cwd: caller_cwd.map(PathBuf::from),
        name,
        set_clipboard,
        target_client,
    })))
}

pub(super) fn parse_save_buffer(
    mut args: CommandTokens,
    caller_cwd: Option<&Path>,
) -> Result<Request, RmuxError> {
    let mut name = None;
    let mut append = false;
    while let Some(token) = args.peek() {
        match token {
            "--" => {
                let _ = args.optional();
                break;
            }
            "-b" => {
                let _ = args.optional();
                name = Some(args.required("-b buffer name")?);
            }
            "-a" => {
                let _ = args.optional();
                append = true;
            }
            flag if flag.starts_with('-') => return Err(unsupported_flag("save-buffer", flag)),
            _ => break,
        }
    }
    let path = args.required("save-buffer path")?;
    args.no_extra("save-buffer")?;
    Ok(Request::SaveBuffer(rmux_proto::SaveBufferRequest {
        path,
        cwd: caller_cwd.map(PathBuf::from),
        name,
        append,
    }))
}

fn parse_optional_buffer_name(
    command: &str,
    args: &mut CommandTokens,
) -> Result<Option<String>, RmuxError> {
    let mut name = None;
    while args.peek_is_flag() {
        match args.required("buffer flag")?.as_str() {
            "-b" => name = Some(args.required("-b buffer name")?),
            flag => return Err(unsupported_flag(command, flag)),
        }
    }
    Ok(name)
}

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

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

    #[test]
    fn parse_set_buffer_accepts_bare_cluster_before_value_flags() {
        let request = parse_set_buffer(CommandTokens::new(vec![
            token("-aw"),
            token("-baudit"),
            token("tail"),
        ]))
        .expect("clustered set-buffer flags parse");

        let Request::SetBuffer(request) = request else {
            panic!("expected set-buffer request");
        };
        assert!(request.append);
        assert!(request.set_clipboard);
        assert_eq!(request.name.as_deref(), Some("audit"));
        assert_eq!(request.content, b"tail");
    }

    #[test]
    fn parse_set_buffer_stops_parsing_flags_at_separator() {
        let request = parse_set_buffer(CommandTokens::new(vec![token("--"), token("-aw")]))
            .expect("separator preserves flag-looking buffer content");

        let Request::SetBuffer(request) = request else {
            panic!("expected set-buffer request");
        };
        assert!(!request.append);
        assert!(!request.set_clipboard);
        assert_eq!(request.content, b"-aw");
    }

    #[test]
    fn parse_set_buffer_reports_the_first_unknown_cluster_flag_like_tmux_3_7b() {
        for (flag_token, expected) in [
            ("-value", "command set-buffer: unknown flag -v"),
            ("-avalue", "command set-buffer: unknown flag -v"),
            ("-wvalue", "command set-buffer: unknown flag -v"),
            ("-zfoo", "command set-buffer: unknown flag -z"),
            ("-az", "command set-buffer: unknown flag -z"),
            ("-waq", "command set-buffer: unknown flag -q"),
            ("--foo", "command set-buffer: invalid flag --"),
        ] {
            let error = parse_set_buffer(CommandTokens::new(vec![token(flag_token)]))
                .expect_err("unknown set-buffer flag must be rejected");
            assert_eq!(error, RmuxError::Server(expected.to_owned()));
        }
    }
}