use std::io::Read;
use std::path::Path;
use rmux_client::connect;
use rmux_proto::{ClientTerminalContext, CopyModeRequest, ErrorResponse, LayoutName, Response};
use super::capture_pane::{build_capture_pane_request, capture_pane_request};
use super::client_commands::run_attach_session;
use super::command_inventory::run_list_commands;
use super::command_runner::{
finish_command_success, run_command, run_command_resolved, run_payload_command,
run_payload_command_resolved, run_queued_server_command,
};
use super::config_commands::{
run_set_environment, run_set_hook, run_set_option, run_show_environment, run_show_hooks,
run_show_options,
};
use super::key_commands::{
run_bind_key, run_list_keys, run_send_keys, run_send_prefix, run_unbind_key,
};
use super::pane_commands::{
run_break_pane, run_join_pane, run_last_pane, run_list_panes, run_move_pane, run_pipe_pane,
run_resize_pane, run_respawn_pane, run_select_pane, run_split_window, run_swap_pane,
};
use super::server_commands::{
run_kill_server, run_lock_client, run_lock_server, run_lock_session, run_server_access,
run_start_server,
};
use super::session_commands::{
run_has_session, run_kill_session, run_list_sessions, run_new_session, run_rename_session,
};
use super::target_resolution::{
display_panes_client_target_error, resolve_current_pane_target, resolve_pane_target_or_current,
resolve_pane_target_spec, resolve_select_layout_target_spec, resolve_session_target_or_current,
resolve_window_target_or_current,
};
use super::window_commands::{
run_kill_window, run_last_window, run_link_window, run_list_windows, run_move_window,
run_new_window, run_next_window, run_previous_window, run_rename_window, run_resize_window,
run_respawn_window, run_rotate_window, run_select_window, run_swap_window, run_unlink_window,
};
use super::{connect_with_startserver, shell_command_text, ExitFailure, StartupOptions};
use crate::cli_args::{Command, NewSessionArgs, SetOptionCommandKind, ShowOptionsCommandKind};
pub(super) fn default_client_command() -> Command {
Command::NewSession(NewSessionArgs {
attach_if_exists: false,
working_directory: None,
detach_other_clients: false,
detached: false,
session_name: None,
environment: Vec::new(),
flags: Vec::new(),
print_format: None,
window_name: None,
print_session_info: false,
group_target: None,
kill_other_clients: false,
cols: None,
rows: None,
command: Vec::new(),
})
}
pub(super) fn dispatch_command_queue(
commands: Vec<Command>,
socket_path: &Path,
startup: StartupOptions,
client_terminal: ClientTerminalContext,
) -> Result<i32, ExitFailure> {
let commands = if commands.is_empty() {
vec![default_client_command()]
} else {
commands
};
let mut exit_code = 0;
for command in commands {
exit_code = dispatch(
command,
socket_path,
startup.clone(),
client_terminal.clone(),
)?;
}
Ok(exit_code)
}
fn dispatch(
command: Command,
socket_path: &Path,
startup: StartupOptions,
client_terminal: ClientTerminalContext,
) -> Result<i32, ExitFailure> {
let command_startup = startup.for_command(command_has_start_server_flag(&command));
match command {
Command::NewSession(args) => {
run_new_session(args, socket_path, command_startup, client_terminal)
}
Command::StartServer => run_start_server(socket_path, command_startup),
Command::KillServer => run_kill_server(socket_path),
Command::HasSession(args) => run_has_session(args, socket_path),
Command::KillSession(args) => run_kill_session(args, socket_path),
Command::RenameSession(args) => run_rename_session(args, socket_path),
Command::ServerAccess(args) => run_server_access(args, socket_path),
Command::LockServer => run_lock_server(socket_path),
Command::LockSession(args) => run_lock_session(args, socket_path),
Command::LockClient(args) => run_lock_client(args, socket_path),
Command::NewWindow(args) => run_new_window(args, socket_path),
Command::KillWindow(args) => run_kill_window(args, socket_path),
Command::SelectWindow(args) => run_select_window(args, socket_path),
Command::RenameWindow(args) => run_rename_window(args, socket_path),
Command::NextWindow(args) => run_next_window(args, socket_path),
Command::PreviousWindow(args) => run_previous_window(args, socket_path),
Command::LastWindow(args) => run_last_window(args, socket_path),
Command::ListSessions(args) => run_list_sessions(args, socket_path),
Command::ListWindows(args) => run_list_windows(args, socket_path),
Command::LinkWindow(args) => run_link_window(args, socket_path),
Command::MoveWindow(args) => run_move_window(args, socket_path),
Command::SwapWindow(args) => run_swap_window(args, socket_path),
Command::RotateWindow(args) => run_rotate_window(args, socket_path),
Command::ResizeWindow(args) => run_resize_window(args, socket_path),
Command::RespawnWindow(args) => run_respawn_window(args, socket_path),
Command::SplitWindow(args) => run_split_window(args, socket_path),
Command::SwapPane(args) => run_swap_pane(args, socket_path),
Command::LastPane(args) => run_last_pane(args, socket_path),
Command::JoinPane(args) => run_join_pane(args, socket_path),
Command::MovePane(args) => run_move_pane(args, socket_path),
Command::BreakPane(args) => run_break_pane(args, socket_path),
Command::PipePane(args) => run_pipe_pane(args, socket_path),
Command::RespawnPane(args) => run_respawn_pane(args, socket_path),
Command::KillPane(args) => {
run_command_resolved(socket_path, "kill-pane", move |connection| {
let target = match args.target.as_ref() {
Some(target) => resolve_pane_target_spec(connection, target)?,
None => resolve_current_pane_target(connection, "kill-pane")?,
};
connection
.kill_pane_with_options(target, args.kill_all_except)
.map_err(ExitFailure::from_client)
})
}
Command::SelectLayout(args) => {
if args.layout.is_none() {
return run_select_layout_noop(args.target.as_ref(), socket_path);
}
run_command_resolved(socket_path, "select-layout", move |connection| {
let target = match args.target.as_ref() {
Some(target) => resolve_select_layout_target_spec(connection, target)?,
None => rmux_proto::SelectLayoutTarget::Window(
resolve_window_target_or_current(connection, None, "select-layout")?,
),
};
let layout = args.layout.as_ref().expect("handled no-op layout");
match layout.parse::<LayoutName>() {
Ok(parsed) if is_unsupported_named_layout(parsed) => {
Err(invalid_layout_failure(layout))
}
Ok(parsed) => connection
.select_layout(target, parsed)
.map_err(ExitFailure::from_client),
Err(_) if looks_like_custom_layout(layout) => connection
.select_custom_layout(target, layout.clone())
.map_err(ExitFailure::from_client),
Err(_) => Err(invalid_layout_failure(layout)),
}
})
}
Command::NextLayout(args) => {
run_command_resolved(socket_path, "next-layout", move |connection| {
let target = resolve_window_target_or_current(
connection,
args.target.as_ref(),
"next-layout",
)?;
connection
.next_layout(target)
.map_err(ExitFailure::from_client)
})
}
Command::PreviousLayout(args) => {
run_command_resolved(socket_path, "previous-layout", move |connection| {
let target = resolve_window_target_or_current(
connection,
args.target.as_ref(),
"previous-layout",
)?;
connection
.previous_layout(target)
.map_err(ExitFailure::from_client)
})
}
Command::ResizePane(args) => run_resize_pane(args, socket_path),
Command::DisplayPanes(args) => {
let template = args.template_command();
let raw_target = args.target.as_ref().map(|target| target.raw().to_owned());
run_command_resolved(socket_path, "display-panes", move |connection| {
let target = match resolve_session_target_or_current(
connection,
args.target.as_ref(),
"display-panes",
) {
Ok(target) => target,
Err(_error) if raw_target.is_some() => {
return Err(display_panes_client_target_error(
raw_target.as_deref().expect("target checked above"),
))
}
Err(error) => return Err(error),
};
let response = connection
.display_panes(
target,
args.duration_ms,
args.non_blocking,
args.no_command,
template,
)
.map_err(ExitFailure::from_client)?;
if raw_target.is_some()
&& matches!(
&response,
Response::Error(ErrorResponse { error })
if error.to_string() == "no current client"
)
{
return Err(display_panes_client_target_error(
raw_target.as_deref().expect("target checked above"),
));
}
Ok(response)
})
}
Command::ListPanes(args) => run_list_panes(args, socket_path),
Command::SelectPane(args) => run_select_pane(args, socket_path),
Command::CopyMode(args) => {
run_command_resolved(socket_path, "copy-mode", move |connection| {
let target = args
.target
.as_ref()
.map(|target| resolve_pane_target_spec(connection, target))
.transpose()?;
let source = args
.source
.as_ref()
.map(|target| resolve_pane_target_spec(connection, target))
.transpose()?;
connection
.copy_mode(CopyModeRequest {
target,
page_down: args.page_down,
exit_on_scroll: args.exit_on_scroll,
hide_position: args.hide_position,
mouse_drag_start: args.mouse_drag_start,
cancel_mode: args.cancel_mode,
scrollbar_scroll: args.scrollbar_scroll,
source,
page_up: args.page_up,
})
.map_err(ExitFailure::from_client)
})
}
Command::ClockMode(args) => {
run_command_resolved(socket_path, "clock-mode", move |connection| {
let target = args
.target
.as_ref()
.map(|target| resolve_pane_target_spec(connection, target))
.transpose()?;
connection
.clock_mode(target)
.map_err(ExitFailure::from_client)
})
}
Command::SendKeys(args) => run_send_keys(args, socket_path),
Command::BindKey(args) => run_bind_key(args, socket_path),
Command::UnbindKey(args) => run_unbind_key(args, socket_path),
Command::ListCommands(args) => run_list_commands(args),
Command::ListKeys(args) => run_list_keys(args, socket_path),
Command::SendPrefix(args) => run_send_prefix(args, socket_path),
Command::Prompt(args) => {
run_queued_server_command(socket_path, "command-prompt", args.queue_command)
}
Command::ConfirmBefore(args) => {
run_queued_server_command(socket_path, "confirm-before", args.queue_command)
}
Command::FindWindow(args) => {
run_queued_server_command(socket_path, "find-window", args.queue_command)
}
Command::UnlinkWindow(args) => run_unlink_window(args, socket_path),
Command::ChooseTree(args) => {
run_queued_server_command(socket_path, "choose-tree", args.queue_command)
}
Command::ChooseBuffer(args) => {
run_queued_server_command(socket_path, "choose-buffer", args.queue_command)
}
Command::ChooseClient(args) => {
run_queued_server_command(socket_path, "choose-client", args.queue_command)
}
Command::CustomizeMode(args) => {
run_queued_server_command(socket_path, "customize-mode", args.queue_command)
}
Command::AttachSession(args) => {
run_attach_session(args, socket_path, command_startup, client_terminal)
}
Command::RefreshClient(args) => super::run_refresh_client(args, socket_path),
Command::ListClients(args) => super::run_list_clients(args, socket_path),
Command::SwitchClient(args) => super::run_switch_client(args, socket_path),
Command::DetachClient(args) => super::run_detach_client(args, socket_path),
Command::SuspendClient(args) => super::run_suspend_client(args, socket_path),
Command::SetOption(args) => {
run_set_option(SetOptionCommandKind::SetOption, args, socket_path)
}
Command::SetWindowOption(args) => {
run_set_option(SetOptionCommandKind::SetWindowOption, args, socket_path)
}
Command::SetEnvironment(args) => run_set_environment(args, socket_path),
Command::ShowOptions(args) => {
run_show_options(ShowOptionsCommandKind::ShowOptions, args, socket_path)
}
Command::ShowWindowOptions(args) => {
run_show_options(ShowOptionsCommandKind::ShowWindowOptions, args, socket_path)
}
Command::ShowEnvironment(args) => run_show_environment(args, socket_path),
Command::SetHook(args) => run_set_hook(args, socket_path),
Command::ShowHooks(args) => run_show_hooks(args, socket_path),
Command::SetBuffer(args) => run_command(socket_path, "set-buffer", move |connection| {
connection.set_buffer(
args.name,
args.content.unwrap_or_default().into_bytes(),
args.append,
args.new_name,
args.set_clipboard,
)
}),
Command::ShowBuffer(args) => {
run_payload_command(socket_path, "show-buffer", move |connection| {
connection.show_buffer(args.name)
})
}
Command::PasteBuffer(args) => {
run_command_resolved(socket_path, "paste-buffer", move |connection| {
let target = resolve_pane_target_or_current(
connection,
args.target.as_ref(),
"paste-buffer",
)?;
connection
.paste_buffer(
args.name,
target,
args.delete_after,
args.separator,
args.linefeed,
args.raw,
args.bracketed,
)
.map_err(ExitFailure::from_client)
})
}
Command::ListBuffers(args) => {
run_payload_command(socket_path, "list-buffers", move |connection| {
connection.list_buffers(args.format, args.filter, args.sort_order, args.reversed)
})
}
Command::DeleteBuffer(args) => {
run_command(socket_path, "delete-buffer", move |connection| {
connection.delete_buffer(args.name)
})
}
Command::LoadBuffer(args) => run_command(socket_path, "load-buffer", move |connection| {
connection.load_buffer(args.path, args.name, args.set_clipboard)
}),
Command::SaveBuffer(args) => run_command(socket_path, "save-buffer", move |connection| {
connection.save_buffer(args.path, args.name, args.append)
}),
Command::CapturePane(args) if args.print => {
let args = capture_pane_request(args)?;
run_payload_command_resolved(socket_path, "capture-pane", move |connection| {
let request = build_capture_pane_request(connection, args)?;
connection
.capture_pane(request)
.map_err(ExitFailure::from_client)
})
}
Command::CapturePane(args) => {
let args = capture_pane_request(args)?;
run_command_resolved(socket_path, "capture-pane", move |connection| {
let request = build_capture_pane_request(connection, args)?;
connection
.capture_pane(request)
.map_err(ExitFailure::from_client)
})
}
Command::ClearHistory(args) => {
run_command_resolved(socket_path, "clear-history", move |connection| {
let target = resolve_pane_target_or_current(
connection,
args.target.as_ref(),
"clear-history",
)?;
connection
.clear_history(target, args.reset_hyperlinks)
.map_err(ExitFailure::from_client)
})
}
Command::DisplayMessage(args) => {
run_queued_server_command(socket_path, "display-message", args.queue_command)
}
Command::ShowMessages(args) => {
run_payload_command(socket_path, "show-messages", move |connection| {
connection.show_messages(args.jobs, args.terminals, args.target_client)
})
}
Command::RunShell(args) if args.background => {
let command = shell_command_text(args.command);
run_command_resolved(socket_path, "run-shell", move |connection| {
let target = args
.target
.as_ref()
.map(|target| resolve_pane_target_spec(connection, target))
.transpose()?;
connection
.run_shell(
command,
true,
args.as_commands,
args.show_stderr,
args.delay_seconds,
args.start_directory,
target,
)
.map_err(ExitFailure::from_client)
})
}
Command::RunShell(args) => {
let command = shell_command_text(args.command);
run_command_resolved(socket_path, "run-shell", move |connection| {
let target = args
.target
.as_ref()
.map(|target| resolve_pane_target_spec(connection, target))
.transpose()?;
connection
.run_shell(
command,
false,
args.as_commands,
args.show_stderr,
args.delay_seconds,
args.start_directory,
target,
)
.map_err(ExitFailure::from_client)
})
}
Command::SourceFile(args) => run_source_file(args, socket_path, command_startup),
Command::IfShell(args) => run_command(socket_path, "if-shell", move |connection| {
connection.if_shell(
args.condition,
args.format_mode,
args.then_command,
args.else_command,
args.target,
args.background,
)
}),
Command::WaitFor(args) => {
let mode = args.mode();
run_command(socket_path, "wait-for", move |connection| {
connection.wait_for(args.channel, mode)
})
}
Command::DisplayMenu(args) => {
run_queued_server_command(socket_path, "display-menu", args.queue_command)
}
Command::DisplayPopup(args) => {
run_queued_server_command(socket_path, "display-popup", args.queue_command)
}
Command::ClearPromptHistory(args) => {
run_queued_server_command(socket_path, "clear-prompt-history", args.queue_command)
}
Command::ShowPromptHistory(args) => {
run_queued_server_command(socket_path, "show-prompt-history", args.queue_command)
}
Command::Unsupported(args) => Err(ExitFailure::new(
1,
format!(
"command not implemented: {}{}",
args.name,
unsupported_argument_suffix(&args.arguments)
),
)),
}
}
fn run_select_layout_noop(
target: Option<&crate::cli_args::TargetSpec>,
socket_path: &Path,
) -> Result<i32, ExitFailure> {
let mut connection = connect(socket_path)
.map_err(|error| ExitFailure::from_client_connect(socket_path, error))?;
match target {
Some(target) => {
let _ = resolve_select_layout_target_spec(&mut connection, target)?;
}
None => {
let _ = resolve_window_target_or_current(&mut connection, None, "select-layout")?;
}
}
Ok(0)
}
fn is_unsupported_named_layout(layout: LayoutName) -> bool {
matches!(
layout,
LayoutName::MainHorizontalMirrored | LayoutName::MainVerticalMirrored
)
}
fn looks_like_custom_layout(layout: &str) -> bool {
layout.contains(',')
}
fn invalid_layout_failure(layout: &str) -> ExitFailure {
ExitFailure::new(1, format!("invalid layout: {layout}"))
}
pub(super) fn command_has_start_server_flag(command: &Command) -> bool {
matches!(
command,
Command::NewSession(_)
| Command::StartServer
| Command::AttachSession(_)
| Command::SourceFile(_)
)
}
fn unsupported_argument_suffix(arguments: &[String]) -> String {
if arguments.is_empty() {
String::new()
} else {
format!(" {}", arguments.join(" "))
}
}
fn run_source_file(
args: crate::cli_args::SourceFileArgs,
socket_path: &Path,
startup: StartupOptions,
) -> Result<i32, ExitFailure> {
let stdin = if args.paths.iter().any(|path| path == "-") {
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.map_err(|error| ExitFailure::new(1, format!("failed to read stdin: {error}")))?;
Some(buffer)
} else {
None
};
let mut connection = connect_with_startserver(socket_path, startup)?;
let target = args
.target
.as_ref()
.map(|target| resolve_pane_target_spec(&mut connection, target))
.transpose()?;
let response = connection
.source_file(
args.paths,
args.quiet,
args.parse_only,
args.verbose,
args.expand_paths,
target,
stdin,
)
.map_err(ExitFailure::from_client)?;
finish_command_success(response, "source-file")
}