rmux 0.10.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use clap::{ArgAction, ArgGroup, Args};
use rmux_proto::HookName;
#[cfg(test)]
use rmux_proto::{ScopeSelector, SessionName};

use super::{parse_target_spec, TargetSpec};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SetOptionCommandKind {
    SetOption,
    SetWindowOption,
}

impl SetOptionCommandKind {
    pub(crate) const fn command_name(self) -> &'static str {
        match self {
            Self::SetOption => "set-option",
            Self::SetWindowOption => "set-window-option",
        }
    }
}

#[derive(Debug, Clone, Args)]
pub(crate) struct SetOptionArgs {
    #[arg(short = 'g', action = ArgAction::SetTrue)]
    pub(crate) global: bool,
    #[arg(short = 's', action = ArgAction::SetTrue)]
    pub(crate) server: bool,
    #[arg(short = 'w', action = ArgAction::SetTrue)]
    pub(crate) window: bool,
    #[arg(short = 'p', action = ArgAction::SetTrue)]
    pub(crate) pane: bool,
    #[arg(short = 'a', action = ArgAction::SetTrue)]
    pub(crate) append: bool,
    #[arg(short = 'F', action = ArgAction::SetTrue)]
    pub(crate) format: bool,
    #[arg(short = 'o', action = ArgAction::SetTrue)]
    pub(crate) only_if_unset: bool,
    #[arg(short = 'q', action = ArgAction::SetTrue)]
    pub(crate) quiet: bool,
    #[arg(short = 'u', action = ArgAction::SetTrue)]
    pub(crate) unset: bool,
    #[arg(short = 'U', action = ArgAction::SetTrue)]
    pub(crate) unset_pane_overrides: bool,
    #[arg(short = 't', value_parser = parse_target_spec, allow_hyphen_values = true)]
    pub(crate) target: Option<TargetSpec>,
    pub(crate) option: String,
    #[arg(allow_hyphen_values = true)]
    pub(crate) value: Option<String>,
}

impl SetOptionArgs {
    pub(crate) fn validate(self, kind: SetOptionCommandKind) -> Result<Self, clap::Error> {
        if matches!(kind, SetOptionCommandKind::SetOption)
            && [self.server, self.window, self.pane]
                .into_iter()
                .filter(|flag| *flag)
                .count()
                > 1
        {
            return Err(clap::Error::raw(
                clap::error::ErrorKind::ArgumentConflict,
                "set-option accepts at most one of -s, -w, or -p",
            ));
        }

        Ok(self)
    }
}

#[derive(Debug, Clone, Args)]
pub(crate) struct SetWindowOptionArgs {
    #[arg(short = 'g', action = ArgAction::SetTrue)]
    global: bool,
    #[arg(short = 'a', action = ArgAction::SetTrue)]
    append: bool,
    #[arg(short = 'F', action = ArgAction::SetTrue)]
    format: bool,
    #[arg(short = 'o', action = ArgAction::SetTrue)]
    only_if_unset: bool,
    #[arg(short = 'q', action = ArgAction::SetTrue)]
    quiet: bool,
    #[arg(short = 'u', action = ArgAction::SetTrue)]
    unset: bool,
    #[arg(short = 't', value_parser = parse_target_spec, allow_hyphen_values = true)]
    target: Option<TargetSpec>,
    option: String,
    #[arg(allow_hyphen_values = true)]
    value: Option<String>,
}

impl From<SetWindowOptionArgs> for SetOptionArgs {
    fn from(args: SetWindowOptionArgs) -> Self {
        Self {
            global: args.global,
            server: false,
            window: false,
            pane: false,
            append: args.append,
            format: args.format,
            only_if_unset: args.only_if_unset,
            quiet: args.quiet,
            unset: args.unset,
            unset_pane_overrides: false,
            target: args.target,
            option: args.option,
            value: args.value,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ShowOptionsCommandKind {
    ShowOptions,
    ShowWindowOptions,
}

impl ShowOptionsCommandKind {
    pub(crate) const fn command_name(self) -> &'static str {
        match self {
            Self::ShowOptions => "show-options",
            Self::ShowWindowOptions => "show-window-options",
        }
    }
}

#[derive(Debug, Clone, Args)]
#[command(
    disable_help_flag = true,
    group(
        ArgGroup::new("scope")
            .required(false)
            .multiple(false)
            .args(["global", "target"])
    )
)]
pub(crate) struct SetEnvironmentArgs {
    #[arg(short = 'g', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) global: bool,
    #[arg(short = 't', value_parser = parse_target_spec, group = "scope")]
    pub(crate) target: Option<TargetSpec>,
    #[arg(short = 'F', action = ArgAction::SetTrue)]
    pub(crate) format: bool,
    #[arg(short = 'h', action = ArgAction::SetTrue)]
    pub(crate) hidden: bool,
    #[arg(short = 'r', action = ArgAction::SetTrue)]
    pub(crate) clear: bool,
    #[arg(short = 'u', action = ArgAction::SetTrue)]
    pub(crate) unset: bool,
    pub(crate) name: String,
    #[arg(allow_hyphen_values = true)]
    pub(crate) value: Option<String>,
}

#[derive(Debug, Clone, Args)]
#[command(group(
    ArgGroup::new("scope")
        .required(false)
        .multiple(false)
        .args(["server", "window", "pane"])
))]
pub(crate) struct ShowOptionsArgs {
    #[arg(short = 'A', action = ArgAction::SetTrue)]
    pub(crate) include_inherited: bool,
    #[arg(short = 'H', action = ArgAction::SetTrue)]
    pub(crate) include_hooks: bool,
    #[arg(short = 'g', action = ArgAction::SetTrue)]
    pub(crate) global: bool,
    #[arg(short = 's', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) server: bool,
    #[arg(short = 'w', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) window: bool,
    #[arg(short = 'p', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) pane: bool,
    #[arg(short = 'q', action = ArgAction::SetTrue)]
    pub(crate) quiet: bool,
    #[arg(short = 'v', action = ArgAction::SetTrue)]
    pub(crate) value_only: bool,
    #[arg(short = 't', value_parser = parse_target_spec, allow_hyphen_values = true)]
    pub(crate) target: Option<TargetSpec>,
    #[arg(allow_hyphen_values = true)]
    pub(crate) name: Option<String>,
}

#[derive(Debug, Clone, Args)]
pub(crate) struct ShowWindowOptionsArgs {
    #[arg(short = 'g', action = ArgAction::SetTrue)]
    global: bool,
    #[arg(short = 'v', action = ArgAction::SetTrue)]
    value_only: bool,
    #[arg(short = 't', value_parser = parse_target_spec, allow_hyphen_values = true)]
    target: Option<TargetSpec>,
    #[arg(allow_hyphen_values = true)]
    name: Option<String>,
}

impl From<ShowWindowOptionsArgs> for ShowOptionsArgs {
    fn from(args: ShowWindowOptionsArgs) -> Self {
        Self {
            include_inherited: false,
            include_hooks: false,
            global: args.global,
            server: false,
            window: false,
            pane: false,
            quiet: false,
            value_only: args.value_only,
            target: args.target,
            name: args.name,
        }
    }
}

#[derive(Debug, Clone, Args)]
#[command(
    disable_help_flag = true,
    group(
        ArgGroup::new("scope")
            .required(false)
            .multiple(false)
            .args(["global", "target"])
    )
)]
pub(crate) struct ShowEnvironmentArgs {
    #[arg(short = 'g', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) global: bool,
    #[arg(short = 't', value_parser = parse_target_spec, group = "scope")]
    pub(crate) target: Option<TargetSpec>,
    #[arg(short = 'h', action = ArgAction::SetTrue)]
    pub(crate) hidden: bool,
    #[arg(short = 's', action = ArgAction::SetTrue)]
    pub(crate) shell_format: bool,
    pub(crate) name: Option<String>,
}

#[derive(Debug, Clone, Args)]
#[command(group(
    ArgGroup::new("scope")
        .required(false)
        .multiple(true)
        .args(["global", "target"])
))]
pub(crate) struct SetHookArgs {
    #[arg(short = 'a', action = ArgAction::SetTrue)]
    pub(crate) append: bool,
    #[arg(short = 'g', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) global: bool,
    #[arg(short = 'p', action = ArgAction::SetTrue)]
    pub(crate) pane: bool,
    #[arg(short = 'R', action = ArgAction::SetTrue)]
    pub(crate) run_immediately: bool,
    #[arg(short = 't', value_parser = parse_target_spec, group = "scope")]
    pub(crate) target: Option<TargetSpec>,
    #[arg(short = 'u', action = ArgAction::SetTrue)]
    pub(crate) unset: bool,
    #[arg(short = 'w', action = ArgAction::SetTrue)]
    pub(crate) window: bool,
    #[arg(value_parser = parse_hook_spec)]
    pub(crate) hook: ParsedHookSpec,
    pub(crate) command: Option<String>,
}

#[derive(Debug, Clone, Args)]
#[command(group(
    ArgGroup::new("scope")
        .required(false)
        .multiple(true)
        .args(["global", "target"])
))]
pub(crate) struct ShowHooksArgs {
    #[arg(short = 'g', action = ArgAction::SetTrue, group = "scope")]
    pub(crate) global: bool,
    #[arg(short = 'p', action = ArgAction::SetTrue)]
    pub(crate) pane: bool,
    #[arg(short = 't', value_parser = parse_target_spec, group = "scope")]
    pub(crate) target: Option<TargetSpec>,
    #[arg(short = 'w', action = ArgAction::SetTrue)]
    pub(crate) window: bool,
    #[arg(value_parser = parse_hook_name)]
    pub(crate) hook: Option<HookName>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ParsedHookSpec {
    pub(crate) hook: HookName,
    pub(crate) index: Option<u32>,
}

#[cfg(test)]
pub(crate) fn build_scope(global: bool, target: Option<SessionName>) -> ScopeSelector {
    match (global, target) {
        (true, None) => ScopeSelector::Global,
        (false, Some(session_name)) => ScopeSelector::Session(session_name),
        _ => unreachable!("clap scope group should enforce valid combinations"),
    }
}

fn parse_hook_spec(value: &str) -> Result<ParsedHookSpec, String> {
    let (name, index) = if let Some(open_bracket) = value.find('[') {
        let Some(index_text) = value[open_bracket + 1..].strip_suffix(']') else {
            return Err(format!("unknown hook: {value}"));
        };
        let index = index_text
            .parse::<u32>()
            .map_err(|_| format!("invalid hook index: {value}"))?;
        (&value[..open_bracket], Some(index))
    } else {
        (value, None)
    };

    Ok(ParsedHookSpec {
        hook: parse_hook_name(name)?,
        index,
    })
}

fn parse_hook_name(value: &str) -> Result<HookName, String> {
    HookName::from_str(value).ok_or_else(|| format!("invalid option: {value}"))
}