use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use clap::{ArgAction, Args, CommandFactory, FromArgMatches, Parser};
use rmux_core::{
command_inventory::RMUX_EXTENSION_COMMANDS,
command_parser::{CommandEntry, ParsedCommands, COMMAND_TABLE},
tmux_precedence,
};
#[cfg(test)]
use rmux_core::command_parser::CommandParser as TmuxCommandParser;
#[path = "cli_args/buffer.rs"]
mod buffer;
pub(crate) use buffer::{
DeleteBufferArgs, ListBuffersArgs, LoadBufferArgs, PasteBufferArgs, SaveBufferArgs,
SetBufferArgs, ShowBufferArgs,
};
#[path = "cli_args/client.rs"]
mod client;
pub(crate) use client::{
DetachClientArgs, ListClientsArgs, RefreshClientArgs, SuspendClientArgs, SwitchClientArgs,
};
#[path = "cli_args/config.rs"]
mod config;
#[cfg(test)]
pub(crate) use config::build_scope;
pub(crate) use config::{
SetEnvironmentArgs, SetHookArgs, SetOptionArgs, SetOptionCommandKind, SetWindowOptionArgs,
ShowEnvironmentArgs, ShowHooksArgs, ShowOptionsArgs, ShowOptionsCommandKind,
ShowWindowOptionsArgs,
};
#[path = "cli_args/control.rs"]
mod control;
use control::initial_control_command_lines;
#[path = "cli_args/automation.rs"]
mod automation;
pub(crate) use automation::{
BroadcastKeysArgs, CollectPaneOutputArgs, ExpectPaneArgs, FindPanesArgs, FindSessionsArgs,
LocatorArgs, PaneSnapshotArgs, SendKeysWaitMode, SnapshotRegion, StreamPaneArgs, WaitPaneArgs,
WithSessionArgs,
};
#[path = "cli_args/keys.rs"]
mod keys;
pub(crate) use keys::{BindKeyArgs, ListKeysArgs, SendKeysArgs, SendPrefixArgs, UnbindKeyArgs};
#[path = "cli_args/history.rs"]
mod history;
pub(crate) use history::{CapturePaneArgs, ClearHistoryArgs};
#[path = "cli_args/inventory.rs"]
mod inventory;
pub(crate) use inventory::ListCommandsArgs;
#[path = "cli_args/mode_tree.rs"]
mod mode_tree;
pub(crate) use mode_tree::{ChooseBufferArgs, ChooseClientArgs, ChooseTreeArgs, CustomizeModeArgs};
#[path = "cli_args/message.rs"]
mod message;
pub(crate) use message::DisplayMessageArgs;
#[path = "cli_args/prompt.rs"]
mod prompt;
pub(crate) use prompt::{ConfirmBeforeArgs, PromptArgs, PromptHistoryArgs};
#[path = "cli_args/queue.rs"]
mod queue;
use queue::{command_from_parsed, parse_command_queue, parse_runtime_command_groups};
#[path = "cli_args/script.rs"]
mod script;
use script::parse_source_file_args;
pub(crate) use script::{IfShellArgs, RunShellArgs, SourceFileArgs, WaitForArgs};
#[path = "cli_args/overlay.rs"]
mod overlay;
pub(crate) use overlay::{DisplayMenuArgs, DisplayPopupArgs};
#[path = "cli_args/targets.rs"]
mod targets;
use targets::parse_session_name;
pub(crate) use targets::{parse_target_spec, TargetSpec};
#[path = "cli_args/pane.rs"]
mod pane;
use pane::{
parse_join_pane_args, parse_resize_pane_args, parse_select_layout_args, parse_select_pane_args,
parse_split_window_args,
};
pub(crate) use pane::{
BreakPaneArgs, ClockModeArgs, CopyModeArgs, DisplayPanesArgs, JoinPaneArgs, LastPaneArgs,
ListPanesArgs, PaneTargetArgs, PipePaneArgs, ResizePaneArgs, ResizePaneSize, RespawnPaneArgs,
SelectLayoutArgs, SelectLayoutMode, SelectPaneArgs, SplitWindowArgs, SwapPaneArgs,
};
#[path = "cli_args/session.rs"]
mod session;
pub(crate) use session::{
AlertSessionTargetArgs, AttachSessionArgs, ClientTargetArgs, KillSessionArgs, ListSessionsArgs,
NewSessionArgs, RenameSessionArgs, ServerAccessArgs, SessionTargetArgs, ShowMessagesArgs,
};
#[path = "cli_args/window.rs"]
mod window;
use window::{parse_rename_window_args, parse_select_window_args, parse_swap_window_args};
pub(crate) use window::{
FindWindowArgs, KillWindowArgs, LinkWindowArgs, ListWindowsArgs, MoveWindowArgs, NewWindowArgs,
RenameWindowArgs, ResizeWindowArgs, RespawnWindowArgs, RotateWindowArgs, SelectWindowArgs,
SwapWindowArgs, UnlinkWindowArgs, WindowTargetArgs,
};
#[path = "cli_args/web.rs"]
mod web;
pub(crate) use web::{WebShareArgs, WebShareTerminalThemeArg, WEB_SHARE_TUNNEL_PROVIDERS};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DocumentedCliAlias {
pub(crate) alias: &'static str,
pub(crate) expansion: &'static str,
}
const DOCUMENTED_CLI_ALIASES: &[DocumentedCliAlias] = &[
DocumentedCliAlias {
alias: "choose-session",
expansion: "choose-tree -s",
},
DocumentedCliAlias {
alias: "choose-window",
expansion: "choose-tree -w",
},
];
static IMPLEMENTED_COMMAND_SURFACE: OnceLock<Vec<&'static CommandEntry>> = OnceLock::new();
static IMPLEMENTED_COMMAND_HELP: OnceLock<String> = OnceLock::new();
pub(crate) fn parse<I, T>(args: I) -> Result<Cli, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let raw = parse_raw_cli(args)?;
if raw.control_mode != 0 {
return Cli::from_raw_control(raw);
}
let parsed_commands = parse_command_queue(&raw.command)?;
reject_parse_time_assignments_without_runtime_bridge(&parsed_commands)?;
Cli::from_raw(raw, parsed_commands, false)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RuntimeCommandGroup {
Canonical(String),
}
fn reject_parse_time_assignments_without_runtime_bridge(
parsed_commands: &ParsedCommands,
) -> Result<(), clap::Error> {
let Some(assignment) = parsed_commands.assignments().first() else {
return Ok(());
};
let token = format!("{}={}", assignment.name(), assignment.value());
let token = token
.chars()
.flat_map(char::escape_default)
.collect::<String>();
Err(clap::Error::raw(
clap::error::ErrorKind::InvalidSubcommand,
format!("unknown command: {token}"),
))
}
pub(crate) fn parse_with_runtime_command_groups<I, T>(
args: I,
runtime_groups: &[RuntimeCommandGroup],
) -> Result<Cli, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let raw = parse_raw_cli(args)?;
let parsed_commands = parse_runtime_command_groups(runtime_groups)?;
Cli::from_raw(raw, parsed_commands, true)
}
fn parse_raw_cli<I, T>(args: I) -> Result<RawCli, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let args = normalize_top_level_attached_short_values(args.into_iter().map(Into::into));
let mut command = RawCli::command();
command = command.after_help(implemented_command_help());
let matches = command.try_get_matches_from(args)?;
RawCli::from_arg_matches(&matches)
}
#[derive(Debug)]
pub(crate) struct TopLevelCommandScan {
pub(crate) assume_256_colors: bool,
pub(crate) control_mode: u8,
pub(crate) no_fork: bool,
pub(crate) shell_command: Option<String>,
pub(crate) config_files: Vec<PathBuf>,
pub(crate) login_shell: bool,
pub(crate) socket_name: Option<OsString>,
pub(crate) no_start_server: bool,
pub(crate) socket_path: Option<OsString>,
pub(crate) terminal_features: Vec<String>,
pub(crate) utf8: bool,
pub(crate) verbose: u8,
pub(crate) command: Vec<OsString>,
}
pub(crate) fn scan_top_level_command(
arguments: &[OsString],
) -> Result<TopLevelCommandScan, clap::Error> {
let args = std::iter::once(OsString::from("rmux"))
.chain(arguments.iter().cloned())
.collect::<Vec<_>>();
let args = normalize_top_level_attached_short_values(args);
let matches = RawCli::command().try_get_matches_from(args)?;
let raw = RawCli::from_arg_matches(&matches)?;
Ok(TopLevelCommandScan {
assume_256_colors: raw.assume_256_colors,
control_mode: raw.control_mode,
no_fork: raw.no_fork,
shell_command: raw.shell_command,
config_files: raw.config_files,
login_shell: raw.login_shell,
socket_name: raw.socket_name,
no_start_server: raw.no_start_server,
socket_path: raw.socket_path,
terminal_features: raw.terminal_features,
utf8: raw.utf8,
verbose: raw.verbose,
command: raw.command,
})
}
fn normalize_top_level_attached_short_values<I>(args: I) -> Vec<OsString>
where
I: IntoIterator<Item = OsString>,
{
let mut normalized = Vec::new();
let mut args = args.into_iter();
let Some(binary) = args.next() else {
return normalized;
};
normalized.push(binary);
let mut passthrough = false;
let mut preserve_hyphen_value = false;
for argument in args {
if passthrough {
normalized.push(argument);
continue;
}
if preserve_hyphen_value {
preserve_hyphen_value = false;
normalized.push(argument);
continue;
}
let Some(value) = argument.to_str() else {
normalized.push(argument);
continue;
};
if value == "--" {
passthrough = true;
normalized.push(argument);
continue;
}
if !value.starts_with('-') || value == "-" {
passthrough = true;
normalized.push(argument);
continue;
}
if matches!(value, "-L" | "-S" | "-T") {
preserve_hyphen_value = true;
normalized.push(argument);
continue;
}
if let Some((flag, attached)) = top_level_attached_short_value(value) {
normalized.push(OsString::from(format!("-{flag}")));
normalized.push(OsString::from(attached));
} else {
normalized.push(argument);
}
}
normalized
}
fn top_level_attached_short_value(argument: &str) -> Option<(char, &str)> {
let mut chars = argument.chars();
if chars.next()? != '-' || chars.as_str().starts_with('-') {
return None;
}
let flag = chars.next()?;
let value = chars.as_str();
(!value.is_empty() && matches!(flag, 'c' | 'f' | 'L' | 'S' | 'T')).then_some((flag, value))
}
fn build_implemented_command_help() -> String {
let mut help = String::from("Commands:\n");
for entry in implemented_command_surface() {
help.push_str(" ");
help.push_str(entry.name);
if let Some(alias) = entry.alias {
help.push_str(" (");
help.push_str(alias);
help.push(')');
}
help.push('\n');
}
help.push_str("\nBuilt-in command aliases:\n");
for alias in documented_cli_aliases() {
help.push_str(" ");
help.push_str(alias.alias);
help.push_str(" => ");
help.push_str(alias.expansion);
help.push('\n');
}
help.trim_end().to_owned()
}
pub(crate) fn implemented_command_surface() -> &'static [&'static CommandEntry] {
IMPLEMENTED_COMMAND_SURFACE
.get_or_init(|| {
COMMAND_TABLE
.iter()
.chain(RMUX_EXTENSION_COMMANDS.iter())
.collect()
})
.as_slice()
}
fn implemented_command_help() -> &'static str {
IMPLEMENTED_COMMAND_HELP
.get_or_init(build_implemented_command_help)
.as_str()
}
pub(crate) fn documented_cli_aliases() -> &'static [DocumentedCliAlias] {
DOCUMENTED_CLI_ALIASES
}
#[allow(dead_code)]
#[path = "cli_args/completion.rs"]
mod completion;
#[allow(unused_imports)]
pub(crate) use completion::completion_command;
#[derive(Debug)]
pub(crate) struct Cli {
pub(crate) assume_256_colors: bool,
pub(crate) control_mode: u8,
pub(crate) no_fork: bool,
pub(crate) shell_command: Option<String>,
config_files: Vec<PathBuf>,
pub(crate) login_shell: bool,
socket_name: Option<OsString>,
pub(crate) no_start_server: bool,
socket_path: Option<PathBuf>,
terminal_features: Vec<String>,
pub(crate) utf8: bool,
pub(crate) verbose: u8,
pub(crate) command: Option<Command>,
command_queue: Vec<Command>,
control_command_lines: Vec<String>,
}
#[derive(Debug, Parser)]
#[command(disable_help_subcommand = true, version)]
struct RawCli {
#[arg(
short = '2',
action = ArgAction::SetTrue,
overrides_with = "assume_256_colors"
)]
assume_256_colors: bool,
#[arg(short = 'C', action = ArgAction::Count)]
control_mode: u8,
#[arg(short = 'D', action = ArgAction::SetTrue, overrides_with = "no_fork")]
no_fork: bool,
#[arg(short = 'c', value_name = "shell-command")]
shell_command: Option<String>,
#[arg(short = 'f', value_name = "file")]
config_files: Vec<PathBuf>,
#[arg(
short = 'l',
action = ArgAction::SetTrue,
overrides_with = "login_shell"
)]
login_shell: bool,
#[arg(short = 'L', value_name = "socket-name", allow_hyphen_values = true)]
socket_name: Option<OsString>,
#[arg(
short = 'N',
action = ArgAction::SetTrue,
overrides_with = "no_start_server"
)]
no_start_server: bool,
#[arg(short = 'S', value_name = "socket-path", allow_hyphen_values = true)]
socket_path: Option<OsString>,
#[arg(short = 'T', value_name = "features", allow_hyphen_values = true)]
terminal_features: Vec<String>,
#[arg(short = 'u', action = ArgAction::SetTrue, overrides_with = "utf8")]
utf8: bool,
#[arg(short = 'v', action = ArgAction::Count)]
verbose: u8,
#[arg(
value_name = "command",
allow_hyphen_values = true,
trailing_var_arg = true
)]
command: Vec<OsString>,
}
impl Cli {
fn from_raw_control(raw: RawCli) -> Result<Self, clap::Error> {
let explicit_command_args = !raw.command.is_empty();
let control_command_lines = initial_control_command_lines(&raw.command)?;
let command_queue = explicit_command_args
.then_some(Command::Noop)
.into_iter()
.collect::<Vec<_>>();
let primary_command = command_queue.first().cloned();
Ok(Self::from_raw_queue(
raw,
primary_command,
command_queue,
control_command_lines,
))
}
fn from_raw(
raw: RawCli,
parsed_commands: ParsedCommands,
apply_runtime_assignments: bool,
) -> Result<Self, clap::Error> {
let explicit_command_args = !raw.command.is_empty();
let control_command_lines = if raw.control_mode == 0 {
Vec::new()
} else {
initial_control_command_lines(&raw.command)?
};
let runtime_assignments = apply_runtime_assignments
.then(|| parsed_commands.assignments_to_tmux_reparse_string())
.filter(|assignments| !assignments.is_empty());
let mut command_queue = parsed_commands
.into_commands()
.into_iter()
.map(command_from_parsed)
.collect::<Result<Vec<_>, _>>()?;
if command_queue.is_empty() && explicit_command_args {
command_queue.push(Command::Noop);
}
let primary_command = command_queue.first().cloned();
if raw.control_mode == 0 {
if let Some(assignments) = runtime_assignments {
command_queue.insert(0, Command::ApplyParseTimeAssignments(assignments));
}
}
Ok(Self::from_raw_queue(
raw,
primary_command,
command_queue,
control_command_lines,
))
}
fn from_raw_queue(
raw: RawCli,
primary_command: Option<Command>,
command_queue: Vec<Command>,
control_command_lines: Vec<String>,
) -> Self {
Self {
assume_256_colors: raw.assume_256_colors,
control_mode: raw.control_mode,
no_fork: raw.no_fork,
shell_command: raw.shell_command,
config_files: raw.config_files,
login_shell: raw.login_shell,
socket_name: raw.socket_name,
no_start_server: raw.no_start_server,
socket_path: raw.socket_path.map(PathBuf::from),
terminal_features: raw.terminal_features,
utf8: raw.utf8,
verbose: raw.verbose,
command: primary_command,
command_queue,
control_command_lines,
}
}
pub(crate) fn socket_name(&self) -> Option<&std::ffi::OsStr> {
self.socket_name.as_deref()
}
pub(crate) fn socket_path(&self) -> Option<&Path> {
self.socket_path.as_deref()
}
pub(crate) fn config_file_selection(&self) -> ConfigFileSelection<'_> {
match self.config_files.as_slice() {
[] => ConfigFileSelection::Default,
files => ConfigFileSelection::Custom(files),
}
}
pub(crate) fn terminal_features(&self) -> &[String] {
&self.terminal_features
}
pub(crate) fn into_command_queue(self) -> Vec<Command> {
self.command_queue
}
pub(crate) fn control_command_lines(&self) -> &[String] {
&self.control_command_lines
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConfigFileSelection<'a> {
Default,
Custom(&'a [PathBuf]),
}
fn parse_command_args<T>(
command_name: &'static str,
arguments: Vec<String>,
) -> Result<T, clap::Error>
where
T: Args + FromArgMatches,
{
parse_command_args_with_policy(
command_name,
arguments,
PositionalOptionPolicy::StopAtFirstPositional,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PositionalOptionPolicy {
StopAtFirstPositional,
InterspersedBeforeSeparator,
}
fn parse_command_args_with_policy<T>(
command_name: &'static str,
arguments: Vec<String>,
positional_option_policy: PositionalOptionPolicy,
) -> Result<T, clap::Error>
where
T: Args + FromArgMatches,
{
let command = T::augment_args(
clap::Command::new(command_name)
.no_binary_name(true)
.disable_help_flag(true),
)
.args_override_self(true)
.disable_help_subcommand(true)
.arg(
clap::Arg::new("help")
.long("help")
.action(ArgAction::Help)
.help("Print help"),
)
.mut_args(|argument| {
if argument_requires_value(&argument)
&& (argument.get_short().is_some() || argument.get_long().is_some())
{
argument.allow_hyphen_values(true)
} else {
argument
}
});
let arguments = normalize_attached_short_values(&command, arguments);
let arguments = tmux_precedence::normalize_tmux_precedence(command_name, arguments);
let arguments = match positional_option_policy {
PositionalOptionPolicy::StopAtFirstPositional => {
delimit_options_before_positionals(command_name, &command, arguments)?
}
PositionalOptionPolicy::InterspersedBeforeSeparator => arguments,
};
let matches = command.try_get_matches_from(arguments)?;
T::from_arg_matches(&matches)
}
fn delimit_options_before_positionals(
command_name: &'static str,
command: &clap::Command,
mut arguments: Vec<String>,
) -> Result<Vec<String>, clap::Error> {
let positionals_allow_hyphen = command
.get_positionals()
.any(|argument| argument.is_allow_hyphen_values_set());
let short_flags = command
.get_arguments()
.filter_map(clap::Arg::get_short)
.collect::<std::collections::BTreeSet<_>>();
let value_flags = command
.get_arguments()
.filter(|argument| argument_requires_value(argument))
.filter_map(clap::Arg::get_short)
.collect::<std::collections::BTreeSet<_>>();
let long_flags = command
.get_arguments()
.filter_map(clap::Arg::get_long)
.collect::<std::collections::BTreeSet<_>>();
let long_value_flags = command
.get_arguments()
.filter(|argument| argument_requires_value(argument))
.filter_map(clap::Arg::get_long)
.collect::<std::collections::BTreeSet<_>>();
let mut expected_value_flag = None::<String>;
let mut first_positional = None;
for (index, argument) in arguments.iter().enumerate() {
if expected_value_flag.take().is_some() {
continue;
}
if argument == "--" {
break;
}
if !argument.starts_with('-') || argument == "-" {
first_positional = Some(index);
break;
}
if let Some(long) = argument.strip_prefix("--") {
let name = long.split_once('=').map_or(long, |(name, _)| name);
if !long_flags.contains(name) {
if positionals_allow_hyphen {
return Err(unknown_flag_error(command_name, format!("--{name}")));
}
continue;
}
if long_value_flags.contains(name) && !long.contains('=') {
expected_value_flag = Some(format!("--{name}"));
}
continue;
}
let mut chars = argument[1..].chars().peekable();
while let Some(flag) = chars.next() {
if !short_flags.contains(&flag) {
if positionals_allow_hyphen {
return Err(unknown_flag_error(command_name, format!("-{flag}")));
}
continue;
}
if value_flags.contains(&flag) {
if chars.peek().is_none() {
expected_value_flag = Some(format!("-{flag}"));
}
break;
}
}
}
if let Some(flag) = expected_value_flag {
return Err(missing_value_error(command_name, flag));
}
if let Some(index) = first_positional {
let positional_limit = command
.get_positionals()
.map(|argument| {
argument
.get_num_args()
.map_or(1, |range| range.max_values())
})
.fold(0_usize, usize::saturating_add);
if positional_limit == 0 {
return Ok(arguments);
}
if arguments.len() - index > positional_limit {
return Err(clap::Error::raw(
clap::error::ErrorKind::TooManyValues,
format!(
"command {command_name}: too many arguments (need at most {positional_limit})"
),
));
}
arguments.insert(index, "--".to_owned());
}
Ok(arguments)
}
fn argument_requires_value(argument: &clap::Arg) -> bool {
matches!(argument.get_action(), ArgAction::Set | ArgAction::Append)
&& argument
.get_num_args()
.is_none_or(|range| range.min_values() > 0)
}
fn unknown_flag_error(command_name: &'static str, flag: String) -> clap::Error {
clap::Error::raw(
clap::error::ErrorKind::UnknownArgument,
format!("command {command_name}: unknown flag {flag}"),
)
}
fn missing_value_error(command_name: &'static str, flag: String) -> clap::Error {
clap::Error::raw(
clap::error::ErrorKind::ValueValidation,
format!("command {command_name}: {flag} expects an argument"),
)
}
fn normalize_attached_short_values(command: &clap::Command, arguments: Vec<String>) -> Vec<String> {
let boolean_flags = command
.get_arguments()
.filter(|argument| {
matches!(
argument.get_action(),
ArgAction::SetTrue | ArgAction::SetFalse | ArgAction::Count
)
})
.filter_map(clap::Arg::get_short)
.collect::<std::collections::BTreeSet<_>>();
let value_flags = command
.get_arguments()
.filter(|argument| argument_requires_value(argument))
.filter_map(clap::Arg::get_short)
.collect::<std::collections::BTreeSet<_>>();
let repeat_last_wins_flags = if command.get_name() == "new-window" {
command
.get_arguments()
.filter(|argument| matches!(argument.get_action(), ArgAction::Set))
.filter_map(clap::Arg::get_short)
.filter(|flag| *flag == 't')
.collect::<std::collections::BTreeSet<_>>()
} else {
std::collections::BTreeSet::new()
};
if value_flags.is_empty() {
return arguments;
}
let mut normalized_options = Vec::with_capacity(arguments.len());
let mut trailing_values = Vec::new();
let mut passthrough = false;
let mut expect_next_value = false;
let mut arguments = arguments.into_iter();
while let Some(argument) = arguments.next() {
if passthrough {
trailing_values.push(argument);
continue;
}
if expect_next_value {
normalized_options.push(argument);
expect_next_value = false;
continue;
}
if argument == "--" {
passthrough = true;
trailing_values.push(argument);
continue;
}
if is_positional_start(&argument) {
trailing_values.push(argument);
trailing_values.extend(arguments);
break;
}
if let Some((parts, needs_next_value)) =
normalize_compact_short_value_token(&argument, &boolean_flags, &value_flags)
{
normalized_options.extend(parts);
expect_next_value = needs_next_value;
} else if exact_short_flag(&argument, &value_flags).is_some() {
expect_next_value = true;
normalized_options.push(argument);
} else {
normalized_options.push(argument);
}
}
let mut normalized =
collapse_repeated_short_values(normalized_options, &repeat_last_wins_flags);
normalized.extend(trailing_values);
normalized
}
fn is_positional_start(argument: &str) -> bool {
!argument.starts_with('-') || argument == "-"
}
fn normalize_compact_short_value_token(
argument: &str,
boolean_flags: &std::collections::BTreeSet<char>,
value_flags: &std::collections::BTreeSet<char>,
) -> Option<(Vec<String>, bool)> {
let flags = argument.strip_prefix('-')?;
if flags.is_empty() || flags.starts_with('-') {
return None;
}
let mut chars = flags.char_indices().peekable();
let first = chars.next()?;
chars.peek()?;
let mut parts = Vec::new();
for (index, flag) in std::iter::once(first).chain(chars.by_ref()) {
if boolean_flags.contains(&flag) {
parts.push(format!("-{flag}"));
continue;
}
if value_flags.contains(&flag) {
parts.push(format!("-{flag}"));
let value_start = index + flag.len_utf8();
if value_start < flags.len() {
parts.push(flags[value_start..].to_owned());
return Some((parts, false));
}
return Some((parts, true));
}
return None;
}
Some((parts, false))
}
fn collapse_repeated_short_values(
arguments: Vec<String>,
repeat_last_wins_flags: &std::collections::BTreeSet<char>,
) -> Vec<String> {
if repeat_last_wins_flags.is_empty() {
return arguments;
}
let mut occurrences = std::collections::BTreeMap::<char, Vec<(usize, usize)>>::new();
let mut index = 0;
while index + 1 < arguments.len() {
if let Some(flag) = exact_short_flag(&arguments[index], repeat_last_wins_flags) {
occurrences
.entry(flag)
.or_default()
.push((index, index + 1));
index += 2;
} else {
index += 1;
}
}
let mut drop_indexes = std::collections::BTreeSet::new();
for positions in occurrences.values() {
for (flag_index, value_index) in positions
.iter()
.take(positions.len().saturating_sub(1))
.copied()
{
drop_indexes.insert(flag_index);
drop_indexes.insert(value_index);
}
}
arguments
.into_iter()
.enumerate()
.filter_map(|(index, argument)| (!drop_indexes.contains(&index)).then_some(argument))
.collect()
}
fn exact_short_flag(argument: &str, flags: &std::collections::BTreeSet<char>) -> Option<char> {
let mut chars = argument.chars();
if chars.next()? != '-' || chars.as_str().starts_with('-') {
return None;
}
let flag = chars.next()?;
(chars.next().is_none() && flags.contains(&flag)).then_some(flag)
}
#[derive(Debug, Clone)]
pub(crate) enum Command {
Noop,
ApplyParseTimeAssignments(String),
NewSession(NewSessionArgs),
StartServer(StartServerArgs),
KillServer,
HasSession(SessionTargetArgs),
KillSession(KillSessionArgs),
RenameSession(RenameSessionArgs),
ServerAccess(ServerAccessArgs),
LockServer,
LockSession(SessionTargetArgs),
LockClient(ClientTargetArgs),
NewWindow(NewWindowArgs),
KillWindow(KillWindowArgs),
SelectWindow(SelectWindowArgs),
RenameWindow(RenameWindowArgs),
NextWindow(AlertSessionTargetArgs),
PreviousWindow(AlertSessionTargetArgs),
LastWindow(SessionTargetArgs),
ListSessions(ListSessionsArgs),
ListWindows(ListWindowsArgs),
MoveWindow(MoveWindowArgs),
SwapWindow(SwapWindowArgs),
RotateWindow(RotateWindowArgs),
ResizeWindow(ResizeWindowArgs),
RespawnWindow(RespawnWindowArgs),
SplitWindow(SplitWindowArgs),
SwapPane(SwapPaneArgs),
LastPane(LastPaneArgs),
JoinPane(JoinPaneArgs),
MovePane(JoinPaneArgs),
BreakPane(BreakPaneArgs),
PipePane(PipePaneArgs),
RespawnPane(RespawnPaneArgs),
KillPane(PaneTargetArgs),
SelectLayout(SelectLayoutArgs),
NextLayout(WindowTargetArgs),
PreviousLayout(WindowTargetArgs),
ResizePane(ResizePaneArgs),
DisplayPanes(DisplayPanesArgs),
ListPanes(ListPanesArgs),
SelectPane(SelectPaneArgs),
CopyMode(CopyModeArgs),
ClockMode(ClockModeArgs),
WaitPane(WaitPaneArgs),
PaneSnapshot(PaneSnapshotArgs),
StreamPane(StreamPaneArgs),
CollectPaneOutput(CollectPaneOutputArgs),
Locator(LocatorArgs),
ExpectPane(ExpectPaneArgs),
FindPanes(FindPanesArgs),
FindSessions(FindSessionsArgs),
BroadcastKeys(BroadcastKeysArgs),
WithSession(WithSessionArgs),
SendKeys(SendKeysArgs),
BindKey(BindKeyArgs),
UnbindKey(UnbindKeyArgs),
ListCommands(ListCommandsArgs),
ListKeys(ListKeysArgs),
SendPrefix(SendPrefixArgs),
Prompt(PromptArgs),
ConfirmBefore(ConfirmBeforeArgs),
FindWindow(FindWindowArgs),
LinkWindow(LinkWindowArgs),
UnlinkWindow(UnlinkWindowArgs),
ChooseTree(ChooseTreeArgs),
ChooseBuffer(ChooseBufferArgs),
ChooseClient(ChooseClientArgs),
CustomizeMode(CustomizeModeArgs),
AttachSession(AttachSessionArgs),
RefreshClient(RefreshClientArgs),
ListClients(ListClientsArgs),
SwitchClient(SwitchClientArgs),
DetachClient(DetachClientArgs),
SuspendClient(SuspendClientArgs),
SetOption(SetOptionArgs),
SetWindowOption(SetOptionArgs),
SetEnvironment(SetEnvironmentArgs),
ShowOptions(ShowOptionsArgs),
ShowWindowOptions(ShowOptionsArgs),
ShowEnvironment(ShowEnvironmentArgs),
SetHook(SetHookArgs),
ShowHooks(ShowHooksArgs),
SetBuffer(SetBufferArgs),
ShowBuffer(ShowBufferArgs),
PasteBuffer(PasteBufferArgs),
ListBuffers(ListBuffersArgs),
DeleteBuffer(DeleteBufferArgs),
LoadBuffer(LoadBufferArgs),
SaveBuffer(SaveBufferArgs),
CapturePane(CapturePaneArgs),
ClearHistory(ClearHistoryArgs),
DisplayMessage(DisplayMessageArgs),
ShowMessages(ShowMessagesArgs),
RunShell(RunShellArgs),
SourceFile(SourceFileArgs),
IfShell(IfShellArgs),
WaitFor(WaitForArgs),
WebShare(WebShareArgs),
DisplayMenu(DisplayMenuArgs),
DisplayPopup(DisplayPopupArgs),
ClearPromptHistory(PromptHistoryArgs),
ShowPromptHistory(PromptHistoryArgs),
Unsupported(UnsupportedCommandArgs),
}
#[derive(Debug, Clone)]
pub(crate) struct UnsupportedCommandArgs {
pub(crate) name: String,
pub(crate) arguments: Vec<String>,
}
#[derive(Debug, Clone, Default, Args)]
pub(crate) struct StartServerArgs {
#[arg(long = "web-port", value_name = "port", value_parser = clap::value_parser!(u16).range(1..))]
pub(crate) web_port: Option<u16>,
#[arg(long = "frontend-url", alias = "web-frontend", value_name = "url")]
pub(crate) web_frontend: Option<String>,
}
trait QueuedCommand {
fn set_queue_command(&mut self, queue_command: String);
}
#[cfg(test)]
#[path = "cli_args_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "cli_args_config_tests.rs"]
mod config_tests;
#[cfg(test)]
#[path = "cli_args_layout_tests.rs"]
mod layout_tests;
#[cfg(test)]
#[path = "cli_args_zoom_tests.rs"]
mod zoom_tests;