use crate::data::{Direction, InputMode, Resize, UnblockCondition}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, input::{ layout::PluginUserConfiguration, options::{Options, PaneFrameStyle}, }, }; use clap::builder::styling::{AnsiColor, Color, Style, Styles}; use clap::{Args, Parser, Subcommand, ValueEnum}; use serde::{Deserialize, Serialize}; use std::net::IpAddr; use std::path::PathBuf; use url::Url; const fn ansi(color: AnsiColor) -> Style { Style::new().fg_color(Some(Color::Ansi(color))) } const CLI_STYLES: Styles = Styles::styled() .header(ansi(AnsiColor::Yellow)) .usage(ansi(AnsiColor::Yellow)) .literal(ansi(AnsiColor::Green)) .placeholder(Style::new()) .error(ansi(AnsiColor::Red)) .valid(ansi(AnsiColor::Green)) .invalid(ansi(AnsiColor::Yellow)); fn validate_session(name: &str) -> Result { #[cfg(unix)] { use crate::consts::ZELLIJ_SOCK_MAX_LENGTH; let mut socket_path = crate::consts::ZELLIJ_SOCK_DIR.clone(); socket_path.push(name); if socket_path.as_os_str().len() >= ZELLIJ_SOCK_MAX_LENGTH { // socket path must be less than 108 bytes let available_length = ZELLIJ_SOCK_MAX_LENGTH .saturating_sub(socket_path.as_os_str().len()) .saturating_sub(1); return Err(format!( "session name must be less than {} characters", available_length )); }; }; Ok(name.to_owned()) } #[derive(Parser, Default, Debug, Clone, Serialize, Deserialize)] #[clap( version, name = "zellij", about = "A terminal workspace with batteries included", styles = CLI_STYLES, args_override_self = true )] pub struct CliArgs { /// Maximum panes on screen, caution: opening more panes will close old ones #[clap(long, value_parser)] pub max_panes: Option, /// Change where zellij looks for plugins #[clap(long, value_parser, overrides_with = "data_dir")] pub data_dir: Option, /// Run server listening at the specified socket path #[clap(long, value_parser, hide = true, overrides_with = "server")] pub server: Option, /// Specify name of a new session #[clap(long, short, overrides_with = "session", value_parser = validate_session)] pub session: Option, /// Name of a predefined layout inside the layout directory or the path to a layout file /// if inside a session (or using the --session flag) will be added to the session as a new tab /// or tabs, otherwise will start a new session #[clap(short, long, value_parser, overrides_with = "layout")] pub layout: Option, /// Raw KDL layout string to use directly (instead of a file path) /// if inside a session (or using the --session flag) will be added to the session as a new tab /// or tabs, otherwise will start a new session #[clap(long, value_parser, conflicts_with_all = &["layout", "new_session_with_layout"])] pub layout_string: Option, /// Name of a predefined layout inside the layout directory or the path to a layout file /// Will always start a new session, even if inside an existing session #[clap(short, long, value_parser, overrides_with = "new_session_with_layout")] pub new_session_with_layout: Option, /// Change where zellij looks for the configuration file #[clap(short, long, overrides_with = "config", env = ZELLIJ_CONFIG_FILE_ENV, value_parser)] pub config: Option, /// Change where zellij looks for the configuration directory #[clap(long, overrides_with = "config_dir", env = ZELLIJ_CONFIG_DIR_ENV, value_parser)] pub config_dir: Option, #[clap(subcommand)] pub command: Option, /// Specify emitting additional debug information #[clap(short, long, value_parser)] pub debug: bool, } impl CliArgs { pub fn is_setup_clean(&self) -> bool { if let Some(Command::Setup(ref setup)) = &self.command { if setup.clean { return true; } } false } pub fn options(&self) -> Option { if let Some(Command::Options(options)) = &self.command { return Some(options.clone()); } None } } #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] pub enum Command { /// Change the behaviour of zellij #[clap(name = "options", value_parser)] Options(Options), /// Setup zellij and check its configuration #[clap(name = "setup", value_parser)] Setup(Setup), /// Run a web server to serve terminal sessions #[clap(name = "web", value_parser)] Web(WebCli), /// Send actions to a specific session #[clap(visible_alias = "ac")] #[clap(subcommand)] Action(Box), /// Explore existing zellij sessions #[clap(flatten)] Sessions(Sessions), /// Subscribe to pane render updates (viewport and scrollback) #[clap(override_usage( "zellij [--session ] subscribe [OPTIONS] --pane-id..." ))] Subscribe(SubscribeCli), } #[derive(Debug, Parser, Clone, Serialize, Deserialize)] pub struct SubscribeCli { /// Pane ID(s) to subscribe to (e.g. terminal_1, plugin_2, or bare number like 1) #[clap( short, long, required = true, num_args(1..) )] pub pane_id: Vec, /// Include scrollback lines in initial delivery. /// Bare --scrollback = all scrollback, --scrollback N = last N lines. #[clap( short, long, default_missing_value = "0", num_args(0..=1) )] pub scrollback: Option, /// Output format #[clap(short, long, default_value = "raw", value_enum)] pub format: SubscribeFormat, /// Preserve ANSI styling in the output #[clap(long)] pub ansi: bool, } #[derive(Debug, Clone, Serialize, Deserialize, ValueEnum)] pub enum SubscribeFormat { Raw, Json, } #[derive(Debug, Clone, Args, Serialize, Deserialize)] pub struct WebCli { /// Start the server (default unless other arguments are specified) #[clap(long, value_parser, display_order = 1)] pub start: bool, /// Stop the server #[clap(long, value_parser, exclusive(true), display_order = 2)] pub stop: bool, /// Get the server status #[clap(long, value_parser, conflicts_with("start"), display_order = 3)] pub status: bool, /// Timeout in seconds for the status check (default: 30) #[clap(long, value_parser, requires = "status", display_order = 4)] pub timeout: Option, /// Run the server in the background #[clap( short, long, value_parser, conflicts_with_all(&["stop", "status", "create_token", "revoke_token", "revoke_all_tokens"]), display_order = 5 )] pub daemonize: bool, /// Timeout in seconds waiting for the server to start (default: 10). /// Only used on Windows where the daemonized server is polled via TCP. /// On Unix, startup signaling uses pipes and this option is ignored. #[clap(long, value_parser, display_order = 6)] pub server_startup_timeout: Option, /// Create a login token for the web interface, will only be displayed once and cannot later be /// retrieved. Returns the token name and the token. #[clap(long, value_parser, exclusive(true), display_order = 7)] pub create_token: bool, /// Optional name for the token #[clap(long, value_parser, value_name = "TOKEN_NAME", display_order = 8)] pub token_name: Option, /// Create a read-only login token (can only attach to existing sessions as watcher) #[clap(long, value_parser, exclusive(true), display_order = 9)] pub create_read_only_token: bool, /// Revoke a login token by its name #[clap( long, value_parser, exclusive(true), value_name = "TOKEN NAME", display_order = 10 )] pub revoke_token: Option, /// Revoke all login tokens #[clap(long, value_parser, exclusive(true), display_order = 11)] pub revoke_all_tokens: bool, /// List token names and their creation dates (cannot show actual tokens) #[clap(long, value_parser, exclusive(true), display_order = 12)] pub list_tokens: bool, /// The ip address to listen on locally for connections (defaults to 127.0.0.1) #[clap( long, value_parser, conflicts_with_all(&["stop", "create_token", "revoke_token", "revoke_all_tokens"]), display_order = 13 )] pub ip: Option, /// The port to listen on locally for connections (defaults to 8082) #[clap( long, value_parser, conflicts_with_all(&["stop", "create_token", "revoke_token", "revoke_all_tokens"]), display_order = 14 )] pub port: Option, /// The path to the SSL certificate (required if not listening on 127.0.0.1) #[clap( long, value_parser, conflicts_with_all(&["stop", "status", "create_token", "revoke_token", "revoke_all_tokens"]), display_order = 15 )] pub cert: Option, /// The path to the SSL key (required if not listening on 127.0.0.1) #[clap( long, value_parser, conflicts_with_all(&["stop", "status", "create_token", "revoke_token", "revoke_all_tokens"]), display_order = 16 )] pub key: Option, } impl WebCli { pub fn get_start(&self) -> bool { self.start || !(self.stop || self.status || self.create_token || self.create_read_only_token || self.revoke_token.is_some() || self.revoke_all_tokens || self.list_tokens) } } #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] pub enum SessionCommand { /// Change the behaviour of zellij #[clap(name = "options")] Options(Options), } #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] pub enum Sessions { /// List active sessions #[clap(visible_alias = "ls")] ListSessions { /// Do not add colors and formatting to the list (useful for parsing) #[clap(short, long)] no_formatting: bool, /// Print just the session name #[clap(short, long)] short: bool, /// List the sessions in reverse order (default is ascending order) #[clap(short, long)] reverse: bool, }, /// List existing plugin aliases #[clap(visible_alias = "la")] ListAliases, /// Attach to a session #[clap(visible_alias = "a")] Attach { /// Name of the session to attach to. #[clap(value_parser)] session_name: Option, /// Create a session if one does not exist. #[clap(short, long, value_parser)] create: bool, /// Create a detached session in the background if one does not exist #[clap(short('b'), long, value_parser)] create_background: bool, /// Number of the session index in the active sessions ordered creation date. #[clap(long, value_parser)] index: Option, /// Change the behaviour of zellij #[clap(subcommand, name = "options")] options: Option>, /// If resurrecting a dead session, immediately run all its commands on startup #[clap(short, long)] force_run_commands: bool, /// Authentication token for remote sessions #[clap(short('t'), long, value_parser)] token: Option, /// Save session for automatic re-authentication (4 weeks) #[clap(short('r'), long, value_parser)] remember: bool, /// Delete saved session before connecting #[clap(long, value_parser)] forget: bool, /// Path to a custom CA certificate (PEM format) for verifying the remote server #[clap(long, value_name = "FILE", value_parser)] ca_cert: Option, /// Skip TLS certificate validation (DANGEROUS — development only) #[clap(long, value_parser)] insecure: bool, }, /// Watch a session (read-only) #[clap(visible_alias = "w")] Watch { /// Name of the session to watch #[clap(value_parser)] session_name: Option, }, /// Kill a specific session #[clap(visible_alias = "k")] KillSession { /// Name of target session #[clap(value_parser)] target_session: Option, }, /// Delete a specific session #[clap(visible_alias = "d")] DeleteSession { /// Name of target session #[clap(value_parser)] target_session: Option, /// Kill the session if it's running before deleting it #[clap(short, long)] force: bool, }, /// Kill all sessions #[clap(visible_alias = "ka")] KillAllSessions { /// Automatic yes to prompts #[clap(short, long, value_parser)] yes: bool, }, /// Delete all sessions #[clap(visible_alias = "da")] DeleteAllSessions { /// Automatic yes to prompts #[clap(short, long, value_parser)] yes: bool, /// Kill the sessions if they're running before deleting them #[clap(short, long)] force: bool, }, /// Run a command in a new pane /// Returns: Created pane ID (format: terminal_) #[clap(visible_alias = "r")] Run { /// Command to run #[clap(last(true), required(true))] command: Vec, /// Direction to open the new pane in #[clap(short, long, value_parser, conflicts_with("floating"))] direction: Option, /// Change the working directory of the new pane #[clap(long, value_parser)] cwd: Option, /// Open the new pane in floating mode #[clap(short, long)] floating: bool, /// Open the new pane in place of the current pane, temporarily suspending it #[clap(short, long, conflicts_with("floating"), conflicts_with("direction"))] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, /// Name of the new pane #[clap(short, long, value_parser)] name: Option, /// Close the pane immediately when its command exits #[clap(short, long)] close_on_exit: bool, /// Start the command suspended, only running after you first presses ENTER #[clap(short, long)] start_suspended: bool, /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] x: Option, /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] y: Option, /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] width: Option, /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] height: Option, /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, #[clap(long, conflicts_with("floating"), conflicts_with("direction"))] stacked: bool, /// Block until the command has finished and its pane has been closed #[clap(long)] blocking: bool, /// Block until the command exits successfully (exit status 0) OR its pane has been closed #[clap( long, conflicts_with("blocking"), conflicts_with("block_until_exit_failure"), conflicts_with("block_until_exit") )] block_until_exit_success: bool, /// Block until the command exits with failure (non-zero exit status) OR its pane has been /// closed #[clap( long, conflicts_with("blocking"), conflicts_with("block_until_exit_success"), conflicts_with("block_until_exit") )] block_until_exit_failure: bool, /// Block until the command exits (regardless of exit status) OR its pane has been closed #[clap( long, conflicts_with("blocking"), conflicts_with("block_until_exit_success"), conflicts_with("block_until_exit_failure") )] block_until_exit: bool, /// if set, will open the pane near the current one rather than following the user's focus #[clap(long)] near_current_pane: bool, /// Opaque agent-facing hint for what this pane is for, reported by /// `list-panes --json` (eg. dev-server). Zellij never interprets it. /// Not supported for in-place panes, which temporarily replace another /// pane rather than being a pane of their own. #[clap(long, value_parser, conflicts_with("in_place"))] purpose: Option, #[clap( long, help = "if set, will open the pane without changing the focus of any client, placing it relative to the pane the command was issued from" )] no_focus: bool, /// start this pane without a border (warning: will make it impossible to move with the /// mouse) #[clap(short, long, value_parser)] borderless: Option, /// Target a specific tab by ID #[clap( long, value_parser, conflicts_with("near_current_pane"), conflicts_with("in_place") )] tab_id: Option, }, /// Load a plugin /// Returns: Created pane ID (format: plugin_) #[clap(visible_alias = "p")] Plugin { /// Plugin URL, can either start with http(s), file: or zellij: #[clap(last(true), required(true))] url: String, /// Plugin configuration #[clap(short, long, value_parser)] configuration: Option, /// Open the new pane in floating mode #[clap(short, long)] floating: bool, /// Open the new pane in place of the current pane, temporarily suspending it #[clap(short, long, conflicts_with("floating"))] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, /// Skip the memory and HD cache and force recompile of the plugin (good for development) #[clap(short, long)] skip_plugin_cache: bool, /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] x: Option, /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] y: Option, /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] width: Option, /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] height: Option, /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, #[clap( long, help = "if set, will open the plugin pane without changing the focus of any client, placing it relative to the pane the command was issued from" )] no_focus: bool, /// start this pane without a border (warning: will make it impossible to move with the /// mouse) #[clap(short, long, value_parser)] borderless: Option, /// Target a specific tab by ID #[clap(long, value_parser, conflicts_with("in_place"))] tab_id: Option, }, /// Edit file with default $EDITOR / $VISUAL /// Returns: Created pane ID (format: terminal_) #[clap(visible_alias = "e")] Edit { file: PathBuf, /// Open the file in the specified line number #[clap(short, long, value_parser)] line_number: Option, /// Direction to open the new pane in #[clap(short, long, value_parser, conflicts_with("floating"))] direction: Option, /// Open the new pane in place of the current pane, temporarily suspending it #[clap(short, long, conflicts_with("floating"), conflicts_with("direction"))] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, /// Open the new pane in floating mode #[clap(short, long)] floating: bool, /// Change the working directory of the editor #[clap(long, value_parser)] cwd: Option, /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] x: Option, /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] y: Option, /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] width: Option, /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] height: Option, /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, /// if set, will open the pane near the current one rather than following the user's focus #[clap(long)] near_current_pane: bool, #[clap( long, help = "if set, will open the pane without changing the focus of any client, placing it relative to the pane the command was issued from" )] no_focus: bool, /// start this pane without a border (warning: will make it impossible to move with the /// mouse) #[clap(short, long, value_parser)] borderless: Option, /// Target a specific tab by ID #[clap( long, value_parser, conflicts_with("near_current_pane"), conflicts_with("in_place") )] tab_id: Option, }, /// Send data to one or more plugins, launch them if they are not running. #[clap(override_usage( r#" zellij pipe [OPTIONS] [--] * Send data to a specific plugin: zellij pipe --plugin file:/path/to/my/plugin.wasm --name my_pipe_name -- my_arbitrary_data * To all running plugins (that are listening): zellij pipe --name my_pipe_name -- my_arbitrary_data * Pipe data into this command's STDIN and get output from the plugin on this command's STDOUT tail -f /tmp/my-live-logfile | zellij pipe --name logs --plugin https://example.com/my-plugin.wasm | wc -l "#))] Pipe { /// The name of the pipe #[clap(short, long, value_parser, display_order(1))] name: Option, /// The data to send down this pipe (if blank, will listen to STDIN) payload: Option, #[clap(short, long, value_parser, display_order(2))] /// The args of the pipe args: Option, // TODO: we might want to not re-use // PluginUserConfiguration /// The plugin url (eg. file:/tmp/my-plugin.wasm) to direct this pipe to, if not specified, /// will be sent to all plugins, if specified and is not running, the plugin will be launched #[clap(short, long, value_parser, display_order(3))] plugin: Option, /// The plugin configuration (note: the same plugin with different configuration is /// considered a different plugin for the purposes of determining the pipe destination) #[clap(short('c'), long, value_parser, display_order(4))] plugin_configuration: Option, }, } #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] pub enum CliAction { /// Write bytes to the terminal. Write { bytes: Vec, /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Write characters to the terminal. WriteChars { chars: String, /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Paste text to the terminal (using bracketed paste mode). Paste { chars: String, /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Send one or more keys to the terminal (e.g., "Ctrl a", "F1", "Alt Shift b") SendKeys { /// Keys to send as space-separated strings #[clap(value_parser, required = true)] keys: Vec, /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3) #[clap(short, long, value_parser)] pane_id: Option, }, /// [increase|decrease] the focused panes area at the [left|down|up|right] border. Resize { resize: Resize, direction: Option, /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Change focus to the next pane FocusNextPane, /// Change focus to the previous pane FocusPreviousPane, /// Focus a specific pane by its ID FocusPaneId { /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 pane_id: String, }, /// Change focus to the last focused frame FocusLastPane, /// Move the focused pane in the specified direction. [right|left|up|down] MoveFocus { direction: Direction, }, /// Move focus to the pane or tab (if on screen edge) in the specified direction /// [right|left|up|down] MoveFocusOrTab { direction: Direction, }, /// Change the location of the focused pane in the specified direction or rotate forwrads /// [right|left|up|down] MovePane { direction: Option, /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Rotate the location of the previous pane backwards MovePaneBackwards { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Clear all buffers for a focused pane Clear { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Dumps the viewport and optionally scrollback of a pane to a file or STDOUT DumpScreen { /// File path to dump the pane content to. If omitted, prints to STDOUT. #[clap(long, value_parser)] path: Option, /// Dump the pane with full scrollback #[clap(short, long)] full: bool, /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3). If not specified, dumps the focused pane. #[clap(short, long, value_parser)] pane_id: Option, /// Preserve ANSI styling in the dump output #[clap(short, long)] ansi: bool, }, /// Dump current layout to stdout DumpLayout, /// Save the current session state to disk immediately SaveSession, /// Open the pane scrollback in your default editor EditScrollback { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, /// Preserve ANSI styling in the scrollback dump #[clap(short, long)] ansi: bool, }, /// Scroll up in the focused pane ScrollUp { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll down in focus pane. ScrollDown { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll down to bottom in focus pane. ScrollToBottom { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll up to top in focus pane. ScrollToTop { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll up one page in focus pane. PageScrollUp { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll down one page in focus pane. PageScrollDown { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll up half page in focus pane. HalfPageScrollUp { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Scroll down half page in focus pane. HalfPageScrollDown { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Toggle between fullscreen focus pane and normal layout. ToggleFullscreen { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, #[clap( about = "Toggle between fullscreen over the entire display (including the UI bars) and normal layout" )] ToggleNoUiFullscreen { #[clap( short, long, value_parser, help = "Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)" )] pane_id: Option, }, /// Toggle frames around panes in the UI TogglePaneFrames, SetPaneFrameStyle { #[clap(value_enum, value_parser)] style: PaneFrameStyle, }, /// Toggle between sending text commands to all panes on the current tab and normal mode. ToggleActiveSyncTab { /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, /// Open a new pane in the specified direction [right|down] /// If no direction is specified, will try to use the biggest available space. /// Returns: Created pane ID (format: terminal_ or plugin_) NewPane { /// Direction to open the new pane in #[clap(short, long, value_parser, conflicts_with("floating"))] direction: Option, #[clap(last(true))] command: Vec, #[clap(short, long, conflicts_with("command"), conflicts_with("direction"))] plugin: Option, /// Change the working directory of the new pane #[clap(long, value_parser)] cwd: Option, /// Open the new pane in floating mode #[clap(short, long)] floating: bool, /// Open the new pane in place of the current pane, temporarily suspending it #[clap(short, long, conflicts_with("floating"), conflicts_with("direction"))] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, /// The pane to replace when opening in place, eg. terminal_1, plugin_2 or 3 (only /// effective with --in-place; defaults to the focused pane) #[clap( long, value_parser, requires("in_place"), conflicts_with("near_current_pane") )] pane_id: Option, /// Name of the new pane #[clap(short, long, value_parser)] name: Option, /// Close the pane immediately when its command exits #[clap(short, long, requires("command"))] close_on_exit: bool, /// Start the command suspended, only running it after the you first press ENTER #[clap(short, long, requires("command"))] start_suspended: bool, #[clap(long, value_parser)] configuration: Option, #[clap(long, value_parser)] skip_plugin_cache: bool, /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] x: Option, /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] y: Option, /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] width: Option, /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] height: Option, /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, #[clap(long, conflicts_with("floating"), conflicts_with("direction"))] stacked: bool, /// Block until the command has finished and its pane has been closed #[clap(short, long)] blocking: bool, /// Block until the command exits successfully (exit status 0) OR its pane has been closed #[clap( long, conflicts_with("blocking"), conflicts_with("block_until_exit_failure"), conflicts_with("block_until_exit") )] block_until_exit_success: bool, /// Block until the command exits with failure (non-zero exit status) OR its pane has been /// closed #[clap( long, conflicts_with("blocking"), conflicts_with("block_until_exit_success"), conflicts_with("block_until_exit") )] block_until_exit_failure: bool, /// Block until the command exits (regardless of exit status) OR its pane has been closed #[clap( long, conflicts_with("blocking"), conflicts_with("block_until_exit_success"), conflicts_with("block_until_exit_failure") )] block_until_exit: bool, #[clap(skip)] unblock_condition: Option, /// if set, will open the pane near the current one rather than following the user's focus #[clap(long)] near_current_pane: bool, /// Opaque agent-facing hint for what this pane is for, reported by /// `list-panes --json` (eg. dev-server). Zellij never interprets it. /// Not supported for in-place panes, which temporarily replace another /// pane rather than being a pane of their own. #[clap(long, value_parser, conflicts_with("in_place"))] purpose: Option, #[clap( long, help = "if set, will open the pane without changing the focus of any client, placing it relative to the pane the command was issued from" )] no_focus: bool, /// start this pane without a border (warning: will make it impossible to move with the /// mouse) #[clap(long, value_parser)] borderless: Option, /// Target a specific tab by ID #[clap( long, value_parser, conflicts_with("near_current_pane"), conflicts_with("in_place") )] tab_id: Option, }, /// Open the specified file in a new zellij pane with your default EDITOR /// Returns: Created pane ID (format: terminal_) Edit { file: PathBuf, /// Direction to open the new pane in #[clap(short, long, value_parser, conflicts_with("floating"))] direction: Option, /// Open the file in the specified line number #[clap(short, long, value_parser)] line_number: Option, /// Open the new pane in floating mode #[clap(short, long)] floating: bool, /// Open the new pane in place of the current pane, temporarily suspending it #[clap(short, long, conflicts_with("floating"), conflicts_with("direction"))] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, /// Change the working directory of the editor #[clap(long, value_parser)] cwd: Option, /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] x: Option, /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long, requires("floating"))] y: Option, /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] width: Option, /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long, requires("floating"))] height: Option, /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, /// if set, will open the pane near the current one rather than following the user's focus #[clap(long)] near_current_pane: bool, #[clap( long, help = "if set, will open the pane without changing the focus of any client, placing it relative to the pane the command was issued from" )] no_focus: bool, /// start this pane without a border (warning: will make it impossible to move with the /// mouse) #[clap(short, long, value_parser)] borderless: Option, /// Target a specific tab by ID #[clap( long, value_parser, conflicts_with("near_current_pane"), conflicts_with("in_place") )] tab_id: Option, }, /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session] SwitchMode { input_mode: InputMode, }, /// Embed focused pane if floating or float focused pane if embedded TogglePaneEmbedOrFloating { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Toggle the visibility of all floating panes in the current Tab, open one if none exist ToggleFloatingPanes { /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, /// Show all floating panes in the specified tab (or active tab if tab_id is not provided). /// /// Returns exit code 0 if state was changed, 2 if already visible, 1 if tab not found. ShowFloatingPanes { #[clap(short, long, value_parser)] tab_id: Option, }, /// Hide all floating panes in the specified tab (or active tab if tab_id is not provided). /// /// Returns exit code 0 if state was changed, 2 if already hidden, 1 if tab not found. HideFloatingPanes { #[clap(short, long, value_parser)] tab_id: Option, }, /// Check if floating panes are visible in the specified tab (or active tab). /// /// Prints "true" to stdout and exits 0 if visible. /// Prints "false" to stdout and exits 1 if not visible. AreFloatingPanesVisible { #[clap(short, long, value_parser)] tab_id: Option, }, /// Close the focused pane. ClosePane { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Renames the focused pane RenamePane { name: String, /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Remove a previously set pane name UndoRenamePane { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Go to the next tab. GoToNextTab, /// Go to the previous tab. GoToPreviousTab, /// Close the current tab. CloseTab { /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, /// Go to tab with index [index] GoToTab { index: u32, }, /// Go to tab with name [name] /// /// Returns: When --create is used and tab is created, outputs the tab ID as a single number GoToTabName { name: String, /// Create a tab if one does not exist. #[clap(short, long, value_parser)] create: bool, }, /// Renames the focused pane RenameTab { name: String, /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, /// Remove a previously set tab name UndoRenameTab { /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, /// Go to tab with stable ID GoToTabById { id: u64, }, /// Close tab with stable ID CloseTabById { id: u64, }, /// Rename tab by stable ID RenameTabById { id: u64, name: String, }, /// Create a new tab, optionally with a specified tab layout and name /// /// Returns: The created tab's ID as a single number on stdout NewTab { /// Layout to use for the new tab #[clap(short, long, value_parser, conflicts_with = "layout_string")] layout: Option, /// Raw KDL layout string to use directly (instead of a layout file path) #[clap(long, value_parser, conflicts_with = "layout")] layout_string: Option, /// Default folder to look for layouts #[clap(long, value_parser, requires("layout"))] layout_dir: Option, /// Name of the new tab #[clap(short, long, value_parser)] name: Option, /// Change the working directory of the new tab #[clap(short, long, value_parser)] cwd: Option, /// Optional initial command to run in the new tab #[clap(value_parser, conflicts_with("initial_plugin"), last(true))] initial_command: Vec, /// Initial plugin to load in the new tab #[clap(long, value_parser, conflicts_with("initial_command"))] initial_plugin: Option, /// Close the pane immediately when its command exits #[clap(long, requires("initial_command"))] close_on_exit: bool, /// Start the command suspended, only running it after you first press ENTER #[clap(long, requires("initial_command"))] start_suspended: bool, /// Block until the command exits successfully (exit status 0) OR its pane has been closed #[clap( long, requires("initial_command"), conflicts_with("block_until_exit_failure"), conflicts_with("block_until_exit") )] block_until_exit_success: bool, /// Block until the command exits with failure (non-zero exit status) OR its pane has been closed #[clap( long, requires("initial_command"), conflicts_with("block_until_exit_success"), conflicts_with("block_until_exit") )] block_until_exit_failure: bool, /// Block until the command exits (regardless of exit status) OR its pane has been closed #[clap( long, requires("initial_command"), conflicts_with("block_until_exit_success"), conflicts_with("block_until_exit_failure") )] block_until_exit: bool, #[clap( long, help = "if set, will create the tab without changing the focus of any client" )] no_focus: bool, }, /// Move the focused tab in the specified direction. [right|left] MoveTab { direction: Direction, /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, PreviousSwapLayout { /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, NextSwapLayout { /// Target a specific tab by ID #[clap(short, long, value_parser)] tab_id: Option, }, /// Override the layout of the active tab OverrideLayout { /// Path to the layout file #[clap( value_parser, required_unless_present = "layout_string", conflicts_with = "layout_string" )] layout: Option, /// Raw KDL layout string to use directly (instead of a layout file path) #[clap(long, value_parser, conflicts_with = "layout")] layout_string: Option, /// Default folder to look for layouts #[clap(long, value_parser)] layout_dir: Option, /// Retain existing terminal panes that do not fit in the layout (default: false) #[clap(long)] retain_existing_terminal_panes: bool, /// Retain existing plugin panes that do not fit with the layout default: false) #[clap(long)] retain_existing_plugin_panes: bool, /// Only apply the layout to the active tab (uses just the first layout tab if it has /// multiple) #[clap(long)] apply_only_to_active_tab: bool, }, /// Query all tab names QueryTabNames, StartOrReloadPlugin { url: String, #[clap(short, long, value_parser)] configuration: Option, }, /// Returns: Plugin pane ID (format: plugin_) when creating or focusing plugin LaunchOrFocusPlugin { #[clap(short, long, value_parser)] floating: bool, #[clap(short, long, value_parser)] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, #[clap(short, long, value_parser)] move_to_focused_tab: bool, url: String, #[clap(short, long, value_parser)] configuration: Option, #[clap(short, long, value_parser)] skip_plugin_cache: bool, /// Target a specific tab by ID #[clap(long, value_parser, conflicts_with("in_place"))] tab_id: Option, }, /// Returns: Plugin pane ID (format: plugin_) LaunchPlugin { #[clap(short, long, value_parser)] floating: bool, #[clap(short, long, value_parser)] in_place: bool, /// Close the replaced pane instead of suspending it (only effective with --in-place) #[clap(long, requires("in_place"))] close_replaced_pane: bool, url: Url, #[clap(short, long, value_parser)] configuration: Option, #[clap(short, long, value_parser)] skip_plugin_cache: bool, #[clap( long, help = "if set, will open the plugin pane without changing the focus of any client" )] no_focus: bool, /// Target a specific tab by ID #[clap(long, value_parser, conflicts_with("in_place"))] tab_id: Option, }, RenameSession { name: String, }, /// Send data to one or more plugins, launch them if they are not running. #[clap(override_usage( r#" zellij action pipe [OPTIONS] [--] * Send data to a specific plugin: zellij action pipe --plugin file:/path/to/my/plugin.wasm --name my_pipe_name -- my_arbitrary_data * To all running plugins (that are listening): zellij action pipe --name my_pipe_name -- my_arbitrary_data * Pipe data into this command's STDIN and get output from the plugin on this command's STDOUT tail -f /tmp/my-live-logfile | zellij action pipe --name logs --plugin https://example.com/my-plugin.wasm | wc -l "#))] Pipe { /// The name of the pipe #[clap(short, long, value_parser, display_order(1))] name: Option, /// The data to send down this pipe (if blank, will listen to STDIN) payload: Option, #[clap(short, long, value_parser, display_order(2))] /// The args of the pipe args: Option, // TODO: we might want to not re-use // PluginUserConfiguration /// The plugin url (eg. file:/tmp/my-plugin.wasm) to direct this pipe to, if not specified, /// will be sent to all plugins, if specified and is not running, the plugin will be launched #[clap(short, long, value_parser, display_order(3))] plugin: Option, /// The plugin configuration (note: the same plugin with different configuration is /// considered a different plugin for the purposes of determining the pipe destination) #[clap(short('c'), long, value_parser, display_order(4))] plugin_configuration: Option, /// Launch a new plugin even if one is already running #[clap(short('l'), long, display_order(5))] force_launch_plugin: bool, /// If launching a new plugin, skip cache and force-compile the plugin #[clap(short('s'), long, display_order(6))] skip_plugin_cache: bool, /// If launching a plugin, should it be floating or not, defaults to floating #[clap(short('f'), long, value_parser, display_order(7))] floating_plugin: Option, /// If launching a plugin, launch it in-place (on top of the current pane) #[clap( short('i'), long, value_parser, conflicts_with("floating_plugin"), display_order(8) )] in_place_plugin: Option, /// If launching a plugin, specify its working directory #[clap(short('w'), long, value_parser, display_order(9))] plugin_cwd: Option, /// If launching a plugin, specify its pane title #[clap(short('t'), long, value_parser, display_order(10))] plugin_title: Option, }, ListClients, /// List all panes in the current session /// /// Returns: Formatted list of panes (table or JSON) to stdout ListPanes { /// Include tab information (name, position, ID) #[clap(short, long, value_parser)] tab: bool, /// Include running command information #[clap(short, long, value_parser)] command: bool, /// Include pane state (focused, floating, exited, etc.) #[clap(short, long, value_parser)] state: bool, /// Include geometry (position, size) #[clap(short, long, value_parser)] geometry: bool, /// Include all available fields #[clap(short, long, value_parser)] all: bool, /// Output as JSON #[clap(short, long, value_parser)] json: bool, }, /// List all tabs with their information /// /// Returns: Tab information in table or JSON format ListTabs { /// Include state information (active, fullscreen, sync, floating visibility) #[clap(short, long, value_parser)] state: bool, /// Include dimension information (viewport, display area) #[clap(short, long, value_parser)] dimensions: bool, /// Include pane counts #[clap(short, long, value_parser)] panes: bool, /// Include layout information (swap layout name and dirty state) #[clap(short, long, value_parser)] layout: bool, /// Include all available fields #[clap(short, long, value_parser)] all: bool, /// Output as JSON #[clap(short, long, value_parser)] json: bool, }, /// Get information about the currently active tab /// /// Returns: Tab name and ID by default, or full info in JSON CurrentTabInfo { /// Output as JSON with full TabInfo #[clap(short, long, value_parser)] json: bool, }, TogglePanePinned { /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3) #[clap(short, long, value_parser)] pane_id: Option, }, /// Stack pane ids /// Ids are a space separated list of pane ids. /// They should either be in the form of `terminal_` (eg. terminal_1), `plugin_` (eg. /// plugin_1) or bare integers in which case they'll be considered terminals (eg. 1 is /// the equivalent of terminal_1) /// /// Example: zellij action stack-panes -- terminal_1 plugin_2 3 StackPanes { #[clap(last(true), required(true))] pane_ids: Vec, }, ChangeFloatingPaneCoordinates { /// The pane_id of the floating pane, eg. terminal_1, plugin_2 or 3 (equivalent to /// terminal_3) #[clap(short, long, value_parser)] pane_id: String, /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long)] x: Option, /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(short, long)] y: Option, /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long)] width: Option, /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%) #[clap(long)] height: Option, /// Whether to pin a floating pane so that it is always on top #[clap(long)] pinned: Option, /// change this pane to be with/without a border (warning: will make it impossible to move with the /// mouse if without a border) #[clap(short, long, value_parser)] borderless: Option, }, TogglePaneBorderless { /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3) #[clap(short, long, value_parser)] pane_id: String, }, SetPaneBorderless { /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3) #[clap(short, long, value_parser)] pane_id: String, /// Whether the pane should be borderless (flag present) or bordered (flag absent) #[clap(short, long, value_parser)] borderless: bool, }, /// Detach from the current session Detach, /// Switch the theme to dark (uses configured `theme_dark`). SetDarkTheme, /// Switch the theme to light (uses configured `theme_light`). SetLightTheme, /// Toggle between dark and light themes (used configured `theme_dark` and `theme_light`) ToggleTheme, /// Switch to a different session SwitchSession { /// Name of the session to switch to name: String, /// Optional tab position to focus #[clap(long)] tab_position: Option, /// Optional pane ID to focus (eg. "terminal_1" for terminal pane with id 1, or "plugin_2" for plugin pane with id 2) #[clap(long)] pane_id: Option, /// Layout to apply when switching to the session (relative paths start at layout-dir) #[clap(short, long, value_parser, conflicts_with = "layout_string")] layout: Option, /// Raw KDL layout string to use directly #[clap(long, value_parser, conflicts_with = "layout")] layout_string: Option, /// Default folder to look for layouts #[clap(long, value_parser, requires("layout"))] layout_dir: Option, /// Change the working directory when switching #[clap(short, long, value_parser)] cwd: Option, }, /// Set the default foreground/background color of a pane SetPaneColor { /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3). /// Defaults to $ZELLIJ_PANE_ID if not provided. #[clap(short, long, value_parser)] pane_id: Option, /// Foreground color (e.g. "#00e000", "rgb:00/e0/00") #[clap(long, value_parser)] fg: Option, /// Background color (e.g. "#001a3a", "rgb:00/1a/3a") #[clap(long, value_parser)] bg: Option, /// Reset pane colors to terminal defaults #[clap(long, value_parser, conflicts_with_all(&["fg", "bg"]))] reset: bool, }, } #[cfg(test)] mod tests { use super::*; use clap::Parser; fn parse_subscribe(args: &[&str]) -> SubscribeCli { let mut full_args = vec!["zellij"]; full_args.extend_from_slice(args); let cli = CliArgs::try_parse_from(full_args).unwrap(); match cli.command { Some(Command::Subscribe(s)) => s, other => panic!("Expected Subscribe, got {:?}", other), } } #[test] fn subscribe_scrollback_bare_flag() { let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1", "--scrollback"]); assert_eq!(s.scrollback, Some(0)); } #[test] fn subscribe_scrollback_with_value() { let s = parse_subscribe(&[ "subscribe", "--pane-id", "terminal_1", "--scrollback", "100", ]); assert_eq!(s.scrollback, Some(100)); } #[test] fn subscribe_scrollback_absent() { let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1"]); assert_eq!(s.scrollback, None); } #[test] fn subscribe_format_json() { let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1", "--format", "json"]); assert!(matches!(s.format, SubscribeFormat::Json)); } #[test] fn subscribe_format_default_raw() { let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1"]); assert!(matches!(s.format, SubscribeFormat::Raw)); } #[test] fn subscribe_multiple_pane_ids() { let s = parse_subscribe(&[ "subscribe", "--pane-id", "terminal_1", "--pane-id", "plugin_2", ]); assert_eq!( s.pane_id, vec!["terminal_1".to_string(), "plugin_2".to_string()] ); } #[test] fn subscribe_requires_pane_id() { let result = CliArgs::try_parse_from(["zellij", "subscribe"]); assert!(result.is_err()); } }