//! Command-line interface for controlling a running local Galaxy app. mod commands; mod completions; mod mcp; mod output; mod selectors; use std::ffi::OsString; use std::process::ExitCode; use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum}; use clap_complete::aot::Shell; use commands::{ run_action_catalog_command, run_app_command, run_appearance_command, run_capability_command, run_file_command, run_input_command, run_instance_command, run_keybinding_command, run_pane_command, run_session_command, run_setting_command, run_surface_command, run_tab_command, run_terminal_command, run_theme_command, run_window_command, }; use completions::generate_completions_to_stdout; use mcp::run_mcp_server; use output::write_control_error; use crate::agent::OutputFormat; /// Hidden flag used by the channel-specific Galaxy app binary to enter `galaxyctrl` mode. pub const CONTROL_MODE_FLAG: &str = "--galaxyctrl"; fn normalized_control_command_name(invocation_name: Option<&str>) -> String { invocation_name .filter(|name| *name == "galaxyctrl" || name.starts_with("galaxyctrl-")) .unwrap_or("galaxyctrl") .to_owned() } /// Parsed top-level arguments for `galaxyctrl`. #[derive(Debug, Parser)] #[command( name = "galaxyctrl", display_name = "galaxyctrl", about = "Control a running local Galaxy app instance" )] pub struct ControlArgs { /// Set the output format. #[arg( long = "output-format", global = true, value_enum, default_value_t = OutputFormat::Pretty, env = "GALAXY_OUTPUT_FORMAT" )] pub output_format: OutputFormat, #[command(subcommand)] pub command: ControlCommand, } /// Commands that inspect the public action catalog. #[derive(Debug, Clone, Subcommand)] pub enum ActionCatalogCommand { /// List allowlisted catalog actions. List, /// Inspect a single allowlisted catalog action. Inspect { /// Canonical action name, such as `tab.create` or `surface.settings.open`. action: String, }, } impl ControlArgs { pub fn from_env() -> Self { let bin_name = crate::binary_name().unwrap_or_else(|| "galaxyctrl".to_owned()); Self::try_parse_from_args(std::env::args_os(), bin_name).unwrap_or_else(|err| err.exit()) } /// Parse Galaxy Control arguments only when the wrapper-injected mode flag is present. /// /// Startup calls this before the normal Galaxy parser. Arguments through /// `--galaxyctrl` are removed, and the remaining arguments are parsed as if /// the standalone command name were `galaxyctrl`. pub fn from_control_mode_env() -> Option { Self::try_parse_control_mode_from(std::env::args_os()) .map(|result| result.unwrap_or_else(|err| err.exit())) } /// Testable implementation of [`Self::from_control_mode_env`]. pub fn try_parse_control_mode_from(args: I) -> Option> where I: IntoIterator, T: Into, { let mut stripped_args = vec![OsString::from("galaxyctrl")]; let mut found_control_mode = false; for arg in args { let arg = arg.into(); if !found_control_mode { if arg.to_str() == Some(CONTROL_MODE_FLAG) { found_control_mode = true; } continue; } stripped_args.push(arg); } found_control_mode.then(|| Self::try_parse_from_args(stripped_args, "galaxyctrl")) } pub fn clap_command() -> clap::Command { let invocation_name = crate::binary_name(); let bin_name = normalized_control_command_name(invocation_name.as_deref()); Self::clap_command_for_bin_name(bin_name) } fn try_parse_from_args(args: I, bin_name: impl Into) -> Result where I: IntoIterator, T: Into + Clone, { let matches = Self::clap_command_for_bin_name(bin_name).try_get_matches_from(args)?; Self::from_arg_matches(&matches) } fn clap_command_for_bin_name(bin_name: impl Into) -> clap::Command { let bin_name = bin_name.into(); ::command() .version(crate::version_string()) .bin_name(bin_name.clone()) .after_help(color_print::cformat!( r#"Examples: $ {bin_name} instance list $ {bin_name} tab create $ {bin_name} action list $ {bin_name} action inspect surface.settings.open Learn more: * Use {bin_name} help to learn more about each command * Use {bin_name} action list to inspect allowlisted actions "# )) } } /// Top-level `galaxyctrl` command groups. #[derive(Debug, Clone, Subcommand)] pub enum ControlCommand { /// Serve the allowlisted Galaxy Control catalog over MCP stdio. Mcp(McpArgs), /// Inspect local Galaxy app instances. #[command(subcommand)] Instance(InstanceCommand), /// Inspect and control a selected local Galaxy app. #[command(subcommand)] App(AppCommand), /// Inspect local-control capabilities. #[command(subcommand)] Capability(CapabilityCommand), /// Inspect public action metadata and implementation status. #[command(subcommand)] Action(ActionCatalogCommand), /// Control local Galaxy windows. #[command(subcommand)] Window(WindowCommand), /// Control local Galaxy tabs. #[command(subcommand)] Tab(TabCommand), /// Control local Galaxy panes. #[command(subcommand)] Pane(PaneCommand), /// Control local Galaxy sessions. #[command(subcommand)] Session(SessionCommand), /// Edit terminal input without submitting it. #[command(subcommand)] Input(InputCommand), /// Inspect, execute, and interrupt commands in existing terminal sessions. #[command(subcommand)] Terminal(TerminalCommand), /// Inspect and change Galaxy themes. #[command(subcommand)] Theme(ThemeCommand), /// Inspect and change Galaxy appearance. #[command(subcommand)] Appearance(AppearanceCommand), /// Inspect and change allowlisted settings. #[command(subcommand)] Setting(SettingCommand), /// Inspect keybinding metadata. #[command(subcommand)] Keybinding(KeybindingCommand), /// Open files in Galaxy. #[command(subcommand)] File(FileCommand), /// Open or toggle local Galaxy surfaces. #[command(subcommand)] Surface(SurfaceCommand), /// Generate shell completions for your shell to stdout. /// /// For bash, add the following to ~/.bashrc: /// source <(path/to/galaxyctrl completions bash) /// /// For zsh, add the following to ~/.zshrc: /// source <(path/to/galaxyctrl completions zsh) /// /// For fish, add the following to ~/.config/fish/config.fish: /// path/to/galaxyctrl completions fish | source /// /// For Powershell, add the following to $PROFILE: /// path\to\galaxyctrl completions powershell | Out-String | Invoke-Expression /// /// If no shell is provided, this defaults to the shell that Galaxy was run from. #[command(verbatim_doc_comment)] Completions { /// Shell to generate completions for. #[arg(value_enum)] shell: Option, }, } /// Commands that inspect locally discoverable Galaxy instances. #[derive(Debug, Clone, Subcommand)] pub enum InstanceCommand { /// List locally discoverable Galaxy instances. List, /// Print app, protocol, active target, and action metadata for the selected instance. Inspect(TargetArgs), } /// Commands that inspect and control the selected Galaxy app instance. #[derive(Debug, Clone, Subcommand)] pub enum AppCommand { /// Check that the selected local Galaxy app responds. Ping(TargetArgs), /// Print protocol and build identity metadata for the selected local Galaxy app. Version(TargetArgs), /// Print the active window/tab/pane/session chain. Active(TargetArgs), /// Focus the selected local Galaxy app. Focus(TargetArgs), } /// Commands that inspect public local-control capabilities. #[derive(Debug, Clone, Subcommand)] pub enum CapabilityCommand { /// List allowlisted local-control capabilities. List, /// Inspect a single local-control capability by canonical action name. Inspect { /// Canonical action name, such as `tab.create` or `surface.settings.open`. action: String, }, } #[derive(Debug, Clone, Subcommand)] pub enum WindowCommand { /// List windows in the selected local Galaxy app. List(TargetArgs), /// Inspect one window in the selected local Galaxy app. Inspect(TargetArgs), /// Create a new window. Create(TabCreateArgs), /// Focus a window. Focus(TargetArgs), /// Close a window. Close(TargetArgs), } /// Commands that control tabs in the selected Galaxy app instance. #[derive(Debug, Clone, Subcommand)] pub enum TabCommand { /// List tabs in the selected local Galaxy app. List(TargetArgs), /// Inspect one tab in the selected local Galaxy app. Inspect(TargetArgs), /// Create a new terminal tab in the active window. Create(TabCreateArgs), /// Activate a tab. Activate(TabActivateArgs), /// Move the active tab. Move(TabMoveArgs), /// Close tabs. Close(TabCloseArgs), /// Rename a tab. Rename(RenameArgs), /// Reset a tab name. ResetName(TargetArgs), /// Set or clear a tab color. #[command(subcommand)] Color(TabColorCommand), } /// Commands that control tab colors. #[derive(Debug, Clone, Subcommand)] pub enum TabColorCommand { /// Set a tab color. Set(ColorSetArgs), /// Clear a tab color. Clear(TargetArgs), } /// Commands that control local Galaxy panes. #[derive(Debug, Clone, Subcommand)] pub enum PaneCommand { /// List panes in the selected local Galaxy app. List(TargetArgs), /// Inspect one pane in the selected local Galaxy app. Inspect(TargetArgs), /// Split the active pane. Split(PaneSplitArgs), /// Focus a pane. Focus(TargetArgs), /// Navigate between panes. Navigate(PaneNavigateArgs), /// Resize the active pane. Resize(PaneResizeArgs), /// Maximize the active pane. Maximize(TargetArgs), /// Unmaximize the active pane. Unmaximize(TargetArgs), /// Close the active pane. Close(TargetArgs), /// Rename a pane. Rename(RenameArgs), /// Reset a pane name. ResetName(TargetArgs), } /// Commands that control local Galaxy sessions. #[derive(Debug, Clone, Subcommand)] pub enum SessionCommand { /// List sessions in the selected local Galaxy app. List(TargetArgs), /// Inspect one session in the selected local Galaxy app. Inspect(TargetArgs), /// Activate a session. Activate(TargetArgs), /// Activate the previous session. Previous(TargetArgs), /// Activate the next session. Next(TargetArgs), /// Reopen the most recently closed session. ReopenClosed(TargetArgs), } #[derive(Debug, Clone, Subcommand)] pub enum InputCommand { /// Insert text into the input buffer without submitting it. Insert(TextTargetArgs), /// Replace the input buffer without submitting it. Replace(TextTargetArgs), } /// Commands that control the active command in an existing terminal session. #[derive(Debug, Clone, Subcommand)] pub enum TerminalCommand { /// Inspect the current active command block. Status(TargetArgs), /// Submit a command, but only when the target terminal is idle. Execute(TerminalExecuteArgs), /// Interrupt the expected active command block. Interrupt(TerminalInterruptArgs), } #[derive(Debug, Clone, Subcommand)] pub enum SurfaceCommand { /// List available and unavailable Galaxy surfaces. List(TargetArgs), /// Open settings surfaces. #[command(subcommand)] Settings(SurfaceSettingsCommand), /// Open the command palette. #[command(subcommand)] CommandPalette(SurfaceQueryCommand), /// Open command search. #[command(subcommand)] CommandSearch(SurfaceQueryCommand), /// Open the theme picker. #[command(subcommand)] ThemePicker(SurfaceOpenCommand), /// Open keybinding settings. #[command(subcommand)] Keybindings(SurfaceOpenCommand), /// Open or toggle code review. #[command(subcommand)] CodeReview(SurfaceOpenToggleCommand), /// Open the project explorer. #[command(subcommand)] ProjectExplorer(SurfaceOpenCommand), /// Open global search. #[command(subcommand)] GlobalSearch(SurfaceOpenCommand), /// Toggle the right panel. #[command(subcommand)] RightPanel(SurfaceToggleCommand), /// Open or toggle vertical tabs. #[command(subcommand)] VerticalTabs(SurfaceOpenToggleCommand), } #[derive(Debug, Clone, Subcommand)] pub enum SurfaceSettingsCommand { /// Open Settings, optionally scoped to a page or query. Open(PageQueryArgs), } #[derive(Debug, Clone, Subcommand)] pub enum SurfaceQueryCommand { /// Open the surface with an optional seeded query. Open(QueryArgs), } #[derive(Debug, Clone, Subcommand)] pub enum SurfaceOpenCommand { /// Open the surface. Open(TargetArgs), } #[derive(Debug, Clone, Subcommand)] pub enum SurfaceOpenToggleCommand { /// Open the surface. Open(TargetArgs), /// Toggle the surface. Toggle(TargetArgs), } #[derive(Debug, Clone, Subcommand)] pub enum SurfaceToggleCommand { /// Toggle the surface. Toggle(TargetArgs), } /// Commands that inspect and change Galaxy themes. #[derive(Debug, Clone, Subcommand)] pub enum ThemeCommand { /// List available themes. List(TargetArgs), /// Read current theme state. Get(TargetArgs), /// Set the current theme. Set(ThemeSetArgs), /// Set whether Galaxy follows the system theme. SystemSet(ThemeSystemSetArgs), /// Set the light theme used when following the system theme. LightSet(ThemeSetArgs), /// Set the dark theme used when following the system theme. DarkSet(ThemeSetArgs), } #[derive(Debug, Clone, Subcommand)] pub enum AppearanceCommand { /// Read appearance state. Get(TargetArgs), /// Increase terminal font size. FontSizeIncrease(TargetArgs), /// Decrease terminal font size. FontSizeDecrease(TargetArgs), /// Reset terminal font size. FontSizeReset(TargetArgs), /// Increase UI zoom. ZoomIncrease(TargetArgs), /// Decrease UI zoom. ZoomDecrease(TargetArgs), /// Reset UI zoom. ZoomReset(TargetArgs), } #[derive(Debug, Clone, Subcommand)] pub enum SettingCommand { /// List allowlisted settings. List(NamespaceTargetArgs), /// Read one allowlisted setting. Get(SettingGetArgs), /// Set one allowlisted setting. Set(SettingSetArgs), /// Toggle one allowlisted boolean setting. Toggle(SettingToggleArgs), } #[derive(Debug, Clone, Subcommand)] pub enum KeybindingCommand { /// List keybindings. List(TargetArgs), /// Read one keybinding by name. Get(KeybindingGetArgs), } #[derive(Debug, Clone, Subcommand)] pub enum FileCommand { /// Open a file in Galaxy. Open(FileOpenArgs), } /// Exact selectors for a target within the selected Galaxy instance. #[derive(Debug, Clone, Args, Default)] pub struct TargetArgs { /// Target a specific local Galaxy instance id from `galaxyctrl instance list`. #[arg(long = "instance", conflicts_with = "pid")] pub instance: Option, /// Target a specific local Galaxy process id. #[arg(long = "pid", conflicts_with = "instance")] pub pid: Option, /// Target the active window or an opaque window id. #[arg(long = "window", conflicts_with_all = ["window_index", "window_title"])] pub window: Option, /// Target a window by scoped index when the handler supports it. #[arg(long = "window-index", conflicts_with_all = ["window", "window_title"])] pub window_index: Option, /// Target a window by exact title when the handler supports it. #[arg(long = "window-title", conflicts_with_all = ["window", "window_index"])] pub window_title: Option, /// Target the active tab or an opaque tab id. #[arg(long = "tab", conflicts_with_all = ["tab_index", "tab_title"])] pub tab: Option, /// Target a tab by scoped index when the handler supports it. #[arg(long = "tab-index", conflicts_with_all = ["tab", "tab_title"])] pub tab_index: Option, /// Target a tab by exact title when the handler supports it. #[arg(long = "tab-title", conflicts_with_all = ["tab", "tab_index"])] pub tab_title: Option, /// Target the active pane or an opaque pane id. #[arg(long = "pane", conflicts_with = "pane_index")] pub pane: Option, /// Target a pane by scoped index when the handler supports it. #[arg(long = "pane-index", conflicts_with = "pane")] pub pane_index: Option, /// Target the active session or an opaque session id. #[arg(long = "session")] pub session: Option, } /// Options for serving Galaxy Control over MCP stdio. #[derive(Debug, Clone, Args)] pub struct McpArgs { #[command(flatten)] pub target: TargetArgs, /// Expose only pane-pinned terminal tools suitable for an external agent. #[arg(long = "agent-safe", hide = true)] pub agent_safe: bool, /// Permit an agent-safe MCP client to execute a command in the delegated terminal. #[arg(long = "allow-terminal-execute", hide = true, requires = "agent_safe")] pub allow_terminal_execute: bool, /// Permit an agent-safe MCP client to interrupt the delegated terminal. #[arg( long = "allow-terminal-interrupt", hide = true, requires = "agent_safe" )] pub allow_terminal_interrupt: bool, } #[derive(Debug, Clone, Args)] pub struct TabCreateArgs { #[arg(long = "type", value_enum)] pub tab_type: Option, #[arg(long = "shell")] pub shell: Option, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct TabActivateArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "previous", conflicts_with_all = ["next", "last"])] pub previous: bool, #[arg(long = "next", conflicts_with_all = ["previous", "last"])] pub next: bool, #[arg(long = "last", conflicts_with_all = ["previous", "next"])] pub last: bool, } #[derive(Debug, Clone, Args)] pub struct TabMoveArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "direction", value_enum)] pub direction: CliTabMoveDirection, } #[derive(Debug, Clone, Args)] pub struct TabCloseArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "active", conflicts_with_all = ["others", "right_of"])] pub active: bool, #[arg(long = "others", conflicts_with_all = ["active", "right_of"])] pub others: bool, #[arg(long = "right-of", conflicts_with_all = ["active", "others"])] pub right_of: bool, } #[derive(Debug, Clone, Args)] pub struct PaneSplitArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "direction", value_enum)] pub direction: CliCardinalDirection, } #[derive(Debug, Clone, Args)] pub struct PaneNavigateArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "direction", value_enum)] pub direction: CliDirection, } #[derive(Debug, Clone, Args)] pub struct PaneResizeArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "direction", value_enum)] pub direction: CliCardinalDirection, #[arg(long = "amount")] pub amount: Option, } #[derive(Debug, Clone, Args)] pub struct TextTargetArgs { pub text: String, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct TerminalExecuteArgs { /// Command text to submit to the target terminal. pub command: String, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct TerminalInterruptArgs { /// Exact active block ID returned by `terminal status`. #[arg(long = "block-id")] pub block_id: String, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct PageQueryArgs { #[arg(long = "page")] pub page: Option, #[arg(long = "query")] pub query: Option, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct QueryArgs { #[arg(long = "query")] pub query: Option, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct FileOpenArgs { pub path: String, #[arg(long = "line")] pub line: Option, #[arg(long = "column")] pub column: Option, #[arg(long = "new-tab")] pub new_tab: bool, #[command(flatten)] pub target: TargetArgs, } #[derive(Debug, Clone, Args)] pub struct RenameArgs { #[command(flatten)] pub target: TargetArgs, pub title: String, } #[derive(Debug, Clone, Args)] pub struct ColorSetArgs { #[command(flatten)] pub target: TargetArgs, pub color: String, } #[derive(Debug, Clone, Args)] pub struct ThemeSetArgs { #[command(flatten)] pub target: TargetArgs, pub name: String, } #[derive(Debug, Clone, Args)] pub struct ThemeSystemSetArgs { #[command(flatten)] pub target: TargetArgs, #[arg(action = clap::ArgAction::Set)] pub enabled: bool, } #[derive(Debug, Clone, Args)] pub struct SettingSetArgs { #[command(flatten)] pub target: TargetArgs, pub key: String, pub value: String, } #[derive(Debug, Clone, Args)] pub struct SettingToggleArgs { #[command(flatten)] pub target: TargetArgs, pub key: String, } #[derive(Debug, Clone, Args)] pub struct NamespaceTargetArgs { #[command(flatten)] pub target: TargetArgs, #[arg(long = "namespace")] pub namespace: Option, } #[derive(Debug, Clone, Args)] pub struct SettingGetArgs { #[command(flatten)] pub target: TargetArgs, /// Allowlisted setting key. pub key: String, } #[derive(Debug, Clone, Args)] pub struct KeybindingGetArgs { #[command(flatten)] pub target: TargetArgs, /// Keybinding action name. pub name: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum CliTabType { Terminal, Agent, Default, } impl From for local_control::protocol::TabType { fn from(value: CliTabType) -> Self { match value { CliTabType::Terminal => Self::Terminal, CliTabType::Agent => Self::Agent, CliTabType::Default => Self::Default, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum CliCardinalDirection { Left, Right, Up, Down, } impl From for local_control::protocol::Direction { fn from(value: CliCardinalDirection) -> Self { match value { CliCardinalDirection::Left => Self::Left, CliCardinalDirection::Right => Self::Right, CliCardinalDirection::Up => Self::Up, CliCardinalDirection::Down => Self::Down, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum CliDirection { Left, Right, Up, Down, Previous, Next, } impl From for local_control::protocol::Direction { fn from(value: CliDirection) -> Self { match value { CliDirection::Left => Self::Left, CliDirection::Right => Self::Right, CliDirection::Up => Self::Up, CliDirection::Down => Self::Down, CliDirection::Previous => Self::Previous, CliDirection::Next => Self::Next, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum CliTabMoveDirection { Left, Right, Previous, Next, } impl From for local_control::protocol::Direction { fn from(value: CliTabMoveDirection) -> Self { match value { CliTabMoveDirection::Left => Self::Left, CliTabMoveDirection::Right => Self::Right, CliTabMoveDirection::Previous => Self::Previous, CliTabMoveDirection::Next => Self::Next, } } } pub fn run(args: ControlArgs) -> ExitCode { ExitCode::from(run_exit_code(args)) } pub fn run_and_exit(args: ControlArgs) -> ! { std::process::exit(i32::from(run_exit_code(args))) } fn run_exit_code(args: ControlArgs) -> u8 { let output_format = args.output_format; match run_inner(args) { Ok(()) => 0, Err(error) => { if let Err(write_error) = write_control_error(&error, output_format) { eprintln!( "error: failed to render local-control error: {}", write_error.message ); } 1 } } } fn run_inner(args: ControlArgs) -> Result<(), local_control::protocol::ControlError> { let output_format = args.output_format; match args.command { ControlCommand::Mcp(args) => run_mcp_server(args), ControlCommand::Instance(command) => run_instance_command(command, output_format), ControlCommand::App(command) => run_app_command(command, output_format), ControlCommand::Capability(command) => run_capability_command(command, output_format), ControlCommand::Action(command) => run_action_catalog_command(command, output_format), ControlCommand::Window(command) => run_window_command(command, output_format), ControlCommand::Tab(command) => run_tab_command(command, output_format), ControlCommand::Pane(command) => run_pane_command(command, output_format), ControlCommand::Session(command) => run_session_command(command, output_format), ControlCommand::Input(command) => run_input_command(command, output_format), ControlCommand::Terminal(command) => run_terminal_command(command, output_format), ControlCommand::Theme(command) => run_theme_command(command, output_format), ControlCommand::Appearance(command) => run_appearance_command(command, output_format), ControlCommand::Setting(command) => run_setting_command(command, output_format), ControlCommand::Keybinding(command) => run_keybinding_command(command, output_format), ControlCommand::File(command) => run_file_command(command, output_format), ControlCommand::Surface(command) => run_surface_command(command, output_format), ControlCommand::Completions { shell } => generate_completions_to_stdout(shell), } } #[cfg(test)] pub(crate) use commands::render_human_readable_for_test; #[cfg(test)] pub(crate) use completions::generate_completion_string; #[cfg(test)] pub(crate) use output::ErrorSummary; #[cfg(test)] #[path = "../local_control_tests.rs"] mod tests;