Skip to main content

zellij_utils/
cli.rs

1use crate::data::{Direction, InputMode, Resize, UnblockCondition};
2use crate::setup::Setup;
3use crate::{
4    consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV},
5    input::{layout::PluginUserConfiguration, options::Options},
6};
7use clap::{ArgEnum, Args, Parser, Subcommand};
8use serde::{Deserialize, Serialize};
9use std::net::IpAddr;
10use std::path::PathBuf;
11use url::Url;
12
13fn validate_session(name: &str) -> Result<String, String> {
14    #[cfg(unix)]
15    {
16        use crate::consts::ZELLIJ_SOCK_MAX_LENGTH;
17
18        let mut socket_path = crate::consts::ZELLIJ_SOCK_DIR.clone();
19        socket_path.push(name);
20
21        if socket_path.as_os_str().len() >= ZELLIJ_SOCK_MAX_LENGTH {
22            // socket path must be less than 108 bytes
23            let available_length = ZELLIJ_SOCK_MAX_LENGTH
24                .saturating_sub(socket_path.as_os_str().len())
25                .saturating_sub(1);
26
27            return Err(format!(
28                "session name must be less than {} characters",
29                available_length
30            ));
31        };
32    };
33
34    Ok(name.to_owned())
35}
36
37#[derive(Parser, Default, Debug, Clone, Serialize, Deserialize)]
38#[clap(version, name = "zellij")]
39pub struct CliArgs {
40    /// Maximum panes on screen, caution: opening more panes will close old ones
41    #[clap(long, value_parser)]
42    pub max_panes: Option<usize>,
43
44    /// Change where zellij looks for plugins
45    #[clap(long, value_parser, overrides_with = "data_dir")]
46    pub data_dir: Option<PathBuf>,
47
48    /// Run server listening at the specified socket path
49    #[clap(long, value_parser, hide = true, overrides_with = "server")]
50    pub server: Option<PathBuf>,
51
52    /// Specify name of a new session
53    #[clap(long, short, overrides_with = "session", value_parser = validate_session)]
54    pub session: Option<String>,
55
56    /// Name of a predefined layout inside the layout directory or the path to a layout file
57    /// if inside a session (or using the --session flag) will be added to the session as a new tab
58    /// or tabs, otherwise will start a new session
59    #[clap(short, long, value_parser, overrides_with = "layout")]
60    pub layout: Option<PathBuf>,
61
62    /// Raw KDL layout string to use directly (instead of a file path)
63    /// if inside a session (or using the --session flag) will be added to the session as a new tab
64    /// or tabs, otherwise will start a new session
65    #[clap(long, value_parser, conflicts_with_all = &["layout", "new-session-with-layout"])]
66    pub layout_string: Option<String>,
67
68    /// Name of a predefined layout inside the layout directory or the path to a layout file
69    /// Will always start a new session, even if inside an existing session
70    #[clap(short, long, value_parser, overrides_with = "new_session_with_layout")]
71    pub new_session_with_layout: Option<PathBuf>,
72
73    /// Change where zellij looks for the configuration file
74    #[clap(short, long, overrides_with = "config", env = ZELLIJ_CONFIG_FILE_ENV, value_parser)]
75    pub config: Option<PathBuf>,
76
77    /// Change where zellij looks for the configuration directory
78    #[clap(long, overrides_with = "config_dir", env = ZELLIJ_CONFIG_DIR_ENV, value_parser)]
79    pub config_dir: Option<PathBuf>,
80
81    #[clap(subcommand)]
82    pub command: Option<Command>,
83
84    /// Specify emitting additional debug information
85    #[clap(short, long, value_parser)]
86    pub debug: bool,
87}
88
89impl CliArgs {
90    pub fn is_setup_clean(&self) -> bool {
91        if let Some(Command::Setup(ref setup)) = &self.command {
92            if setup.clean {
93                return true;
94            }
95        }
96        false
97    }
98    pub fn options(&self) -> Option<Options> {
99        if let Some(Command::Options(options)) = &self.command {
100            return Some(options.clone());
101        }
102        None
103    }
104}
105
106#[derive(Debug, Subcommand, Clone, Serialize, Deserialize)]
107pub enum Command {
108    /// Change the behaviour of zellij
109    #[clap(name = "options", value_parser)]
110    Options(Options),
111
112    /// Setup zellij and check its configuration
113    #[clap(name = "setup", value_parser)]
114    Setup(Setup),
115
116    /// Run a web server to serve terminal sessions
117    #[clap(name = "web", value_parser)]
118    Web(WebCli),
119
120    /// Send actions to a specific session
121    #[clap(visible_alias = "ac")]
122    #[clap(subcommand)]
123    Action(Box<CliAction>),
124
125    /// Explore existing zellij sessions
126    #[clap(flatten)]
127    Sessions(Sessions),
128
129    /// Subscribe to pane render updates (viewport and scrollback)
130    #[clap(override_usage(
131        "zellij [--session <OTHER SESSION NAME>] subscribe [OPTIONS] --pane-id..."
132    ))]
133    Subscribe(SubscribeCli),
134}
135
136#[derive(Debug, Parser, Clone, Serialize, Deserialize)]
137pub struct SubscribeCli {
138    /// Pane ID(s) to subscribe to (e.g. terminal_1, plugin_2, or bare number like 1)
139    #[clap(
140        short,
141        long,
142        required = true,
143        multiple_values = true,
144        multiple_occurrences = true
145    )]
146    pub pane_id: Vec<String>,
147
148    /// Include scrollback lines in initial delivery.
149    /// Bare --scrollback = all scrollback, --scrollback N = last N lines.
150    #[clap(
151        short,
152        long,
153        min_values = 0,
154        max_values = 1,
155        default_missing_value = "0"
156    )]
157    pub scrollback: Option<usize>,
158
159    /// Output format
160    #[clap(short, long, default_value = "raw", arg_enum)]
161    pub format: SubscribeFormat,
162
163    /// Preserve ANSI styling in the output
164    #[clap(long, value_parser, default_value("false"), takes_value(false))]
165    pub ansi: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, ArgEnum)]
169pub enum SubscribeFormat {
170    Raw,
171    Json,
172}
173
174#[derive(Debug, Clone, Args, Serialize, Deserialize)]
175pub struct WebCli {
176    /// Start the server (default unless other arguments are specified)
177    #[clap(long, value_parser, display_order = 1)]
178    pub start: bool,
179
180    /// Stop the server
181    #[clap(long, value_parser, exclusive(true), display_order = 2)]
182    pub stop: bool,
183
184    /// Get the server status
185    #[clap(long, value_parser, conflicts_with("start"), display_order = 3)]
186    pub status: bool,
187
188    /// Timeout in seconds for the status check (default: 30)
189    #[clap(long, value_parser, requires = "status", display_order = 4)]
190    pub timeout: Option<u64>,
191
192    /// Run the server in the background
193    #[clap(
194        short,
195        long,
196        value_parser,
197        conflicts_with_all(&["stop", "status", "create-token", "revoke-token", "revoke-all-tokens"]),
198        display_order = 5
199    )]
200    pub daemonize: bool,
201    /// Timeout in seconds waiting for the server to start (default: 10).
202    /// Only used on Windows where the daemonized server is polled via TCP.
203    /// On Unix, startup signaling uses pipes and this option is ignored.
204    #[clap(long, value_parser, display_order = 6)]
205    pub server_startup_timeout: Option<u64>,
206    /// Create a login token for the web interface, will only be displayed once and cannot later be
207    /// retrieved. Returns the token name and the token.
208    #[clap(long, value_parser, exclusive(true), display_order = 7)]
209    pub create_token: bool,
210    /// Optional name for the token
211    #[clap(long, value_parser, value_name = "TOKEN_NAME", display_order = 8)]
212    pub token_name: Option<String>,
213    /// Create a read-only login token (can only attach to existing sessions as watcher)
214    #[clap(long, value_parser, exclusive(true), display_order = 9)]
215    pub create_read_only_token: bool,
216    /// Revoke a login token by its name
217    #[clap(
218        long,
219        value_parser,
220        exclusive(true),
221        value_name = "TOKEN NAME",
222        display_order = 10
223    )]
224    pub revoke_token: Option<String>,
225    /// Revoke all login tokens
226    #[clap(long, value_parser, exclusive(true), display_order = 11)]
227    pub revoke_all_tokens: bool,
228    /// List token names and their creation dates (cannot show actual tokens)
229    #[clap(long, value_parser, exclusive(true), display_order = 12)]
230    pub list_tokens: bool,
231    /// The ip address to listen on locally for connections (defaults to 127.0.0.1)
232    #[clap(
233        long,
234        value_parser,
235        conflicts_with_all(&["stop", "create-token", "revoke-token", "revoke-all-tokens"]),
236        display_order = 13
237    )]
238    pub ip: Option<IpAddr>,
239    /// The port to listen on locally for connections (defaults to 8082)
240    #[clap(
241        long,
242        value_parser,
243        conflicts_with_all(&["stop", "create-token", "revoke-token", "revoke-all-tokens"]),
244        display_order = 14
245    )]
246    pub port: Option<u16>,
247    /// The path to the SSL certificate (required if not listening on 127.0.0.1)
248    #[clap(
249        long,
250        value_parser,
251        conflicts_with_all(&["stop", "status", "create-token", "revoke-token", "revoke-all-tokens"]),
252        display_order = 15
253    )]
254    pub cert: Option<PathBuf>,
255    /// The path to the SSL key (required if not listening on 127.0.0.1)
256    #[clap(
257        long,
258        value_parser,
259        conflicts_with_all(&["stop", "status", "create-token", "revoke-token", "revoke-all-tokens"]),
260        display_order = 16
261    )]
262    pub key: Option<PathBuf>,
263}
264
265impl WebCli {
266    pub fn get_start(&self) -> bool {
267        self.start
268            || !(self.stop
269                || self.status
270                || self.create_token
271                || self.create_read_only_token
272                || self.revoke_token.is_some()
273                || self.revoke_all_tokens
274                || self.list_tokens)
275    }
276}
277
278#[derive(Debug, Subcommand, Clone, Serialize, Deserialize)]
279pub enum SessionCommand {
280    /// Change the behaviour of zellij
281    #[clap(name = "options")]
282    Options(Options),
283}
284
285#[derive(Debug, Subcommand, Clone, Serialize, Deserialize)]
286pub enum Sessions {
287    /// List active sessions
288    #[clap(visible_alias = "ls")]
289    ListSessions {
290        /// Do not add colors and formatting to the list (useful for parsing)
291        #[clap(short, long, value_parser, takes_value(false), default_value("false"))]
292        no_formatting: bool,
293
294        /// Print just the session name
295        #[clap(short, long, value_parser, takes_value(false), default_value("false"))]
296        short: bool,
297
298        /// List the sessions in reverse order (default is ascending order)
299        #[clap(short, long, value_parser, takes_value(false), default_value("false"))]
300        reverse: bool,
301    },
302    /// List existing plugin aliases
303    #[clap(visible_alias = "la")]
304    ListAliases,
305    /// Attach to a session
306    #[clap(visible_alias = "a")]
307    Attach {
308        /// Name of the session to attach to.
309        #[clap(value_parser)]
310        session_name: Option<String>,
311
312        /// Create a session if one does not exist.
313        #[clap(short, long, value_parser)]
314        create: bool,
315
316        /// Create a detached session in the background if one does not exist
317        #[clap(short('b'), long, value_parser)]
318        create_background: bool,
319
320        /// Number of the session index in the active sessions ordered creation date.
321        #[clap(long, value_parser)]
322        index: Option<usize>,
323
324        /// Change the behaviour of zellij
325        #[clap(subcommand, name = "options")]
326        options: Option<Box<SessionCommand>>,
327
328        /// If resurrecting a dead session, immediately run all its commands on startup
329        #[clap(short, long, value_parser, takes_value(false), default_value("false"))]
330        force_run_commands: bool,
331
332        /// Authentication token for remote sessions
333        #[clap(short('t'), long, value_parser)]
334        token: Option<String>,
335
336        /// Save session for automatic re-authentication (4 weeks)
337        #[clap(short('r'), long, value_parser)]
338        remember: bool,
339
340        /// Delete saved session before connecting
341        #[clap(long, value_parser)]
342        forget: bool,
343
344        /// Path to a custom CA certificate (PEM format) for verifying the remote server
345        #[clap(long, value_name = "FILE", value_parser)]
346        ca_cert: Option<PathBuf>,
347
348        /// Skip TLS certificate validation (DANGEROUS — development only)
349        #[clap(long, value_parser)]
350        insecure: bool,
351    },
352
353    /// Watch a session (read-only)
354    #[clap(visible_alias = "w")]
355    Watch {
356        /// Name of the session to watch
357        #[clap(value_parser)]
358        session_name: Option<String>,
359    },
360
361    /// Kill a specific session
362    #[clap(visible_alias = "k")]
363    KillSession {
364        /// Name of target session
365        #[clap(value_parser)]
366        target_session: Option<String>,
367    },
368
369    /// Delete a specific session
370    #[clap(visible_alias = "d")]
371    DeleteSession {
372        /// Name of target session
373        #[clap(value_parser)]
374        target_session: Option<String>,
375        /// Kill the session if it's running before deleting it
376        #[clap(short, long, value_parser, takes_value(false), default_value("false"))]
377        force: bool,
378    },
379
380    /// Kill all sessions
381    #[clap(visible_alias = "ka")]
382    KillAllSessions {
383        /// Automatic yes to prompts
384        #[clap(short, long, value_parser)]
385        yes: bool,
386    },
387
388    /// Delete all sessions
389    #[clap(visible_alias = "da")]
390    DeleteAllSessions {
391        /// Automatic yes to prompts
392        #[clap(short, long, value_parser)]
393        yes: bool,
394        /// Kill the sessions if they're running before deleting them
395        #[clap(short, long, value_parser, takes_value(false), default_value("false"))]
396        force: bool,
397    },
398
399    /// Run a command in a new pane
400    /// Returns: Created pane ID (format: terminal_<id>)
401    #[clap(visible_alias = "r")]
402    Run {
403        /// Command to run
404        #[clap(last(true), required(true))]
405        command: Vec<String>,
406
407        /// Direction to open the new pane in
408        #[clap(short, long, value_parser, conflicts_with("floating"))]
409        direction: Option<Direction>,
410
411        /// Change the working directory of the new pane
412        #[clap(long, value_parser)]
413        cwd: Option<PathBuf>,
414
415        /// Open the new pane in floating mode
416        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
417        floating: bool,
418
419        /// Open the new pane in place of the current pane, temporarily suspending it
420        #[clap(
421            short,
422            long,
423            value_parser,
424            default_value("false"),
425            takes_value(false),
426            conflicts_with("floating"),
427            conflicts_with("direction")
428        )]
429        in_place: bool,
430
431        /// Close the replaced pane instead of suspending it (only effective with --in-place)
432        #[clap(
433            long,
434            value_parser,
435            default_value("false"),
436            takes_value(false),
437            requires("in-place")
438        )]
439        close_replaced_pane: bool,
440
441        /// Name of the new pane
442        #[clap(short, long, value_parser)]
443        name: Option<String>,
444
445        /// Close the pane immediately when its command exits
446        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
447        close_on_exit: bool,
448
449        /// Start the command suspended, only running after you first presses ENTER
450        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
451        start_suspended: bool,
452
453        /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
454        #[clap(short, long, requires("floating"))]
455        x: Option<String>,
456        /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
457        #[clap(short, long, requires("floating"))]
458        y: Option<String>,
459        /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
460        #[clap(long, requires("floating"))]
461        width: Option<String>,
462        /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
463        #[clap(long, requires("floating"))]
464        height: Option<String>,
465        /// Whether to pin a floating pane so that it is always on top
466        #[clap(long, requires("floating"))]
467        pinned: Option<bool>,
468        #[clap(
469            long,
470            conflicts_with("floating"),
471            conflicts_with("direction"),
472            value_parser,
473            default_value("false"),
474            takes_value(false)
475        )]
476        stacked: bool,
477        /// Block until the command has finished and its pane has been closed
478        #[clap(long, value_parser, default_value("false"), takes_value(false))]
479        blocking: bool,
480
481        /// Block until the command exits successfully (exit status 0) OR its pane has been closed
482        #[clap(
483            long,
484            value_parser,
485            default_value("false"),
486            takes_value(false),
487            conflicts_with("blocking"),
488            conflicts_with("block-until-exit-failure"),
489            conflicts_with("block-until-exit")
490        )]
491        block_until_exit_success: bool,
492
493        /// Block until the command exits with failure (non-zero exit status) OR its pane has been
494        /// closed
495        #[clap(
496            long,
497            value_parser,
498            default_value("false"),
499            takes_value(false),
500            conflicts_with("blocking"),
501            conflicts_with("block-until-exit-success"),
502            conflicts_with("block-until-exit")
503        )]
504        block_until_exit_failure: bool,
505
506        /// Block until the command exits (regardless of exit status) OR its pane has been closed
507        #[clap(
508            long,
509            value_parser,
510            default_value("false"),
511            takes_value(false),
512            conflicts_with("blocking"),
513            conflicts_with("block-until-exit-success"),
514            conflicts_with("block-until-exit-failure")
515        )]
516        block_until_exit: bool,
517        /// if set, will open the pane near the current one rather than following the user's focus
518        #[clap(long)]
519        near_current_pane: bool,
520        /// start this pane without a border (warning: will make it impossible to move with the
521        /// mouse)
522        #[clap(short, long, value_parser)]
523        borderless: Option<bool>,
524        /// Target a specific tab by ID
525        #[clap(
526            long,
527            value_parser,
528            conflicts_with("near-current-pane"),
529            conflicts_with("in-place")
530        )]
531        tab_id: Option<usize>,
532    },
533    /// Load a plugin
534    /// Returns: Created pane ID (format: plugin_<id>)
535    #[clap(visible_alias = "p")]
536    Plugin {
537        /// Plugin URL, can either start with http(s), file: or zellij:
538        #[clap(last(true), required(true))]
539        url: String,
540
541        /// Plugin configuration
542        #[clap(short, long, value_parser)]
543        configuration: Option<PluginUserConfiguration>,
544
545        /// Open the new pane in floating mode
546        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
547        floating: bool,
548
549        /// Open the new pane in place of the current pane, temporarily suspending it
550        #[clap(
551            short,
552            long,
553            value_parser,
554            default_value("false"),
555            takes_value(false),
556            conflicts_with("floating")
557        )]
558        in_place: bool,
559
560        /// Close the replaced pane instead of suspending it (only effective with --in-place)
561        #[clap(
562            long,
563            value_parser,
564            default_value("false"),
565            takes_value(false),
566            requires("in-place")
567        )]
568        close_replaced_pane: bool,
569
570        /// Skip the memory and HD cache and force recompile of the plugin (good for development)
571        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
572        skip_plugin_cache: bool,
573        /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
574        #[clap(short, long, requires("floating"))]
575        x: Option<String>,
576        /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
577        #[clap(short, long, requires("floating"))]
578        y: Option<String>,
579        /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
580        #[clap(long, requires("floating"))]
581        width: Option<String>,
582        /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
583        #[clap(long, requires("floating"))]
584        height: Option<String>,
585        /// Whether to pin a floating pane so that it is always on top
586        #[clap(long, requires("floating"))]
587        pinned: Option<bool>,
588        /// start this pane without a border (warning: will make it impossible to move with the
589        /// mouse)
590        #[clap(short, long, value_parser)]
591        borderless: Option<bool>,
592        /// Target a specific tab by ID
593        #[clap(long, value_parser, conflicts_with("in-place"))]
594        tab_id: Option<usize>,
595    },
596    /// Edit file with default $EDITOR / $VISUAL
597    /// Returns: Created pane ID (format: terminal_<id>)
598    #[clap(visible_alias = "e")]
599    Edit {
600        file: PathBuf,
601
602        /// Open the file in the specified line number
603        #[clap(short, long, value_parser)]
604        line_number: Option<usize>,
605
606        /// Direction to open the new pane in
607        #[clap(short, long, value_parser, conflicts_with("floating"))]
608        direction: Option<Direction>,
609
610        /// Open the new pane in place of the current pane, temporarily suspending it
611        #[clap(
612            short,
613            long,
614            value_parser,
615            default_value("false"),
616            takes_value(false),
617            conflicts_with("floating"),
618            conflicts_with("direction")
619        )]
620        in_place: bool,
621
622        /// Close the replaced pane instead of suspending it (only effective with --in-place)
623        #[clap(
624            long,
625            value_parser,
626            default_value("false"),
627            takes_value(false),
628            requires("in-place")
629        )]
630        close_replaced_pane: bool,
631
632        /// Open the new pane in floating mode
633        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
634        floating: bool,
635
636        /// Change the working directory of the editor
637        #[clap(long, value_parser)]
638        cwd: Option<PathBuf>,
639        /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
640        #[clap(short, long, requires("floating"))]
641        x: Option<String>,
642        /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
643        #[clap(short, long, requires("floating"))]
644        y: Option<String>,
645        /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
646        #[clap(long, requires("floating"))]
647        width: Option<String>,
648        /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
649        #[clap(long, requires("floating"))]
650        height: Option<String>,
651        /// Whether to pin a floating pane so that it is always on top
652        #[clap(long, requires("floating"))]
653        pinned: Option<bool>,
654        /// if set, will open the pane near the current one rather than following the user's focus
655        #[clap(long)]
656        near_current_pane: bool,
657        /// start this pane without a border (warning: will make it impossible to move with the
658        /// mouse)
659        #[clap(short, long, value_parser)]
660        borderless: Option<bool>,
661        /// Target a specific tab by ID
662        #[clap(
663            long,
664            value_parser,
665            conflicts_with("near-current-pane"),
666            conflicts_with("in-place")
667        )]
668        tab_id: Option<usize>,
669    },
670    ConvertConfig {
671        old_config_file: PathBuf,
672    },
673    ConvertLayout {
674        old_layout_file: PathBuf,
675    },
676    ConvertTheme {
677        old_theme_file: PathBuf,
678    },
679    /// Send data to one or more plugins, launch them if they are not running.
680    #[clap(override_usage(
681r#"
682zellij pipe [OPTIONS] [--] <PAYLOAD>
683
684* Send data to a specific plugin:
685
686zellij pipe --plugin file:/path/to/my/plugin.wasm --name my_pipe_name -- my_arbitrary_data
687
688* To all running plugins (that are listening):
689
690zellij pipe --name my_pipe_name -- my_arbitrary_data
691
692* Pipe data into this command's STDIN and get output from the plugin on this command's STDOUT
693
694tail -f /tmp/my-live-logfile | zellij pipe --name logs --plugin https://example.com/my-plugin.wasm | wc -l
695"#))]
696    Pipe {
697        /// The name of the pipe
698        #[clap(short, long, value_parser, display_order(1))]
699        name: Option<String>,
700        /// The data to send down this pipe (if blank, will listen to STDIN)
701        payload: Option<String>,
702
703        #[clap(short, long, value_parser, display_order(2))]
704        /// The args of the pipe
705        args: Option<PluginUserConfiguration>, // TODO: we might want to not re-use
706        // PluginUserConfiguration
707        /// The plugin url (eg. file:/tmp/my-plugin.wasm) to direct this pipe to, if not specified,
708        /// will be sent to all plugins, if specified and is not running, the plugin will be launched
709        #[clap(short, long, value_parser, display_order(3))]
710        plugin: Option<String>,
711        /// The plugin configuration (note: the same plugin with different configuration is
712        /// considered a different plugin for the purposes of determining the pipe destination)
713        #[clap(short('c'), long, value_parser, display_order(4))]
714        plugin_configuration: Option<PluginUserConfiguration>,
715    },
716}
717
718#[derive(Debug, Subcommand, Clone, Serialize, Deserialize)]
719pub enum CliAction {
720    /// Write bytes to the terminal.
721    Write {
722        bytes: Vec<u8>,
723        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
724        #[clap(short, long, value_parser)]
725        pane_id: Option<String>,
726    },
727    /// Write characters to the terminal.
728    WriteChars {
729        chars: String,
730        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
731        #[clap(short, long, value_parser)]
732        pane_id: Option<String>,
733    },
734    /// Paste text to the terminal (using bracketed paste mode).
735    Paste {
736        chars: String,
737        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
738        #[clap(short, long, value_parser)]
739        pane_id: Option<String>,
740    },
741    /// Send one or more keys to the terminal (e.g., "Ctrl a", "F1", "Alt Shift b")
742    SendKeys {
743        /// Keys to send as space-separated strings
744        #[clap(value_parser, required = true)]
745        keys: Vec<String>,
746
747        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
748        #[clap(short, long, value_parser)]
749        pane_id: Option<String>,
750    },
751    /// [increase|decrease] the focused panes area at the [left|down|up|right] border.
752    Resize {
753        resize: Resize,
754        direction: Option<Direction>,
755        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
756        #[clap(short, long, value_parser)]
757        pane_id: Option<String>,
758    },
759    /// Change focus to the next pane
760    FocusNextPane,
761    /// Change focus to the previous pane
762    FocusPreviousPane,
763    /// Focus a specific pane by its ID
764    FocusPaneId {
765        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3
766        pane_id: String,
767    },
768    /// Move the focused pane in the specified direction. [right|left|up|down]
769    MoveFocus {
770        direction: Direction,
771    },
772    /// Move focus to the pane or tab (if on screen edge) in the specified direction
773    /// [right|left|up|down]
774    MoveFocusOrTab {
775        direction: Direction,
776    },
777    /// Change the location of the focused pane in the specified direction or rotate forwrads
778    /// [right|left|up|down]
779    MovePane {
780        direction: Option<Direction>,
781        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
782        #[clap(short, long, value_parser)]
783        pane_id: Option<String>,
784    },
785    /// Rotate the location of the previous pane backwards
786    MovePaneBackwards {
787        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
788        #[clap(short, long, value_parser)]
789        pane_id: Option<String>,
790    },
791    /// Clear all buffers for a focused pane
792    Clear {
793        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
794        #[clap(short, long, value_parser)]
795        pane_id: Option<String>,
796    },
797    /// Dumps the viewport and optionally scrollback of a pane to a file or STDOUT
798    DumpScreen {
799        /// File path to dump the pane content to. If omitted, prints to STDOUT.
800        #[clap(long, value_parser)]
801        path: Option<PathBuf>,
802
803        /// Dump the pane with full scrollback
804        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
805        full: bool,
806
807        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3). If not specified, dumps the focused pane.
808        #[clap(short, long, value_parser)]
809        pane_id: Option<String>,
810
811        /// Preserve ANSI styling in the dump output
812        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
813        ansi: bool,
814    },
815    /// Dump current layout to stdout
816    DumpLayout,
817    /// Save the current session state to disk immediately
818    SaveSession,
819    /// Open the pane scrollback in your default editor
820    EditScrollback {
821        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
822        #[clap(short, long, value_parser)]
823        pane_id: Option<String>,
824
825        /// Preserve ANSI styling in the scrollback dump
826        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
827        ansi: bool,
828    },
829    /// Scroll up in the focused pane
830    ScrollUp {
831        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
832        #[clap(short, long, value_parser)]
833        pane_id: Option<String>,
834    },
835    /// Scroll down in focus pane.
836    ScrollDown {
837        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
838        #[clap(short, long, value_parser)]
839        pane_id: Option<String>,
840    },
841    /// Scroll down to bottom in focus pane.
842    ScrollToBottom {
843        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
844        #[clap(short, long, value_parser)]
845        pane_id: Option<String>,
846    },
847    /// Scroll up to top in focus pane.
848    ScrollToTop {
849        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
850        #[clap(short, long, value_parser)]
851        pane_id: Option<String>,
852    },
853    /// Scroll up one page in focus pane.
854    PageScrollUp {
855        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
856        #[clap(short, long, value_parser)]
857        pane_id: Option<String>,
858    },
859    /// Scroll down one page in focus pane.
860    PageScrollDown {
861        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
862        #[clap(short, long, value_parser)]
863        pane_id: Option<String>,
864    },
865    /// Scroll up half page in focus pane.
866    HalfPageScrollUp {
867        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
868        #[clap(short, long, value_parser)]
869        pane_id: Option<String>,
870    },
871    /// Scroll down half page in focus pane.
872    HalfPageScrollDown {
873        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
874        #[clap(short, long, value_parser)]
875        pane_id: Option<String>,
876    },
877    /// Toggle between fullscreen focus pane and normal layout.
878    ToggleFullscreen {
879        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
880        #[clap(short, long, value_parser)]
881        pane_id: Option<String>,
882    },
883    /// Toggle frames around panes in the UI
884    TogglePaneFrames,
885    /// Toggle between sending text commands to all panes on the current tab and normal mode.
886    ToggleActiveSyncTab {
887        /// Target a specific tab by ID
888        #[clap(short, long, value_parser)]
889        tab_id: Option<usize>,
890    },
891    /// Open a new pane in the specified direction [right|down]
892    /// If no direction is specified, will try to use the biggest available space.
893    /// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
894    NewPane {
895        /// Direction to open the new pane in
896        #[clap(short, long, value_parser, conflicts_with("floating"))]
897        direction: Option<Direction>,
898
899        #[clap(last(true))]
900        command: Vec<String>,
901
902        #[clap(short, long, conflicts_with("command"), conflicts_with("direction"))]
903        plugin: Option<String>,
904
905        /// Change the working directory of the new pane
906        #[clap(long, value_parser)]
907        cwd: Option<PathBuf>,
908
909        /// Open the new pane in floating mode
910        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
911        floating: bool,
912
913        /// Open the new pane in place of the current pane, temporarily suspending it
914        #[clap(
915            short,
916            long,
917            value_parser,
918            default_value("false"),
919            takes_value(false),
920            conflicts_with("floating"),
921            conflicts_with("direction")
922        )]
923        in_place: bool,
924
925        /// Close the replaced pane instead of suspending it (only effective with --in-place)
926        #[clap(
927            long,
928            value_parser,
929            default_value("false"),
930            takes_value(false),
931            requires("in-place")
932        )]
933        close_replaced_pane: bool,
934
935        /// Name of the new pane
936        #[clap(short, long, value_parser)]
937        name: Option<String>,
938
939        /// Close the pane immediately when its command exits
940        #[clap(
941            short,
942            long,
943            value_parser,
944            default_value("false"),
945            takes_value(false),
946            requires("command")
947        )]
948        close_on_exit: bool,
949        /// Start the command suspended, only running it after the you first press ENTER
950        #[clap(
951            short,
952            long,
953            value_parser,
954            default_value("false"),
955            takes_value(false),
956            requires("command")
957        )]
958        start_suspended: bool,
959        #[clap(long, value_parser)]
960        configuration: Option<PluginUserConfiguration>,
961        #[clap(long, value_parser)]
962        skip_plugin_cache: bool,
963        /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
964        #[clap(short, long, requires("floating"))]
965        x: Option<String>,
966        /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
967        #[clap(short, long, requires("floating"))]
968        y: Option<String>,
969        /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
970        #[clap(long, requires("floating"))]
971        width: Option<String>,
972        /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
973        #[clap(long, requires("floating"))]
974        height: Option<String>,
975        /// Whether to pin a floating pane so that it is always on top
976        #[clap(long, requires("floating"))]
977        pinned: Option<bool>,
978        #[clap(
979            long,
980            conflicts_with("floating"),
981            conflicts_with("direction"),
982            value_parser,
983            default_value("false"),
984            takes_value(false)
985        )]
986        stacked: bool,
987        /// Block until the command has finished and its pane has been closed
988        #[clap(short, long)]
989        blocking: bool,
990
991        /// Block until the command exits successfully (exit status 0) OR its pane has been closed
992        #[clap(
993            long,
994            value_parser,
995            default_value("false"),
996            takes_value(false),
997            conflicts_with("blocking"),
998            conflicts_with("block-until-exit-failure"),
999            conflicts_with("block-until-exit")
1000        )]
1001        block_until_exit_success: bool,
1002
1003        /// Block until the command exits with failure (non-zero exit status) OR its pane has been
1004        /// closed
1005        #[clap(
1006            long,
1007            value_parser,
1008            default_value("false"),
1009            takes_value(false),
1010            conflicts_with("blocking"),
1011            conflicts_with("block-until-exit-success"),
1012            conflicts_with("block-until-exit")
1013        )]
1014        block_until_exit_failure: bool,
1015
1016        /// Block until the command exits (regardless of exit status) OR its pane has been closed
1017        #[clap(
1018            long,
1019            value_parser,
1020            default_value("false"),
1021            takes_value(false),
1022            conflicts_with("blocking"),
1023            conflicts_with("block-until-exit-success"),
1024            conflicts_with("block-until-exit-failure")
1025        )]
1026        block_until_exit: bool,
1027
1028        #[clap(skip)]
1029        unblock_condition: Option<UnblockCondition>,
1030
1031        /// if set, will open the pane near the current one rather than following the user's focus
1032        #[clap(long)]
1033        near_current_pane: bool,
1034        /// start this pane without a border (warning: will make it impossible to move with the
1035        /// mouse)
1036        #[clap(long, value_parser)]
1037        borderless: Option<bool>,
1038        /// Target a specific tab by ID
1039        #[clap(
1040            long,
1041            value_parser,
1042            conflicts_with("near-current-pane"),
1043            conflicts_with("in-place")
1044        )]
1045        tab_id: Option<usize>,
1046    },
1047    /// Open the specified file in a new zellij pane with your default EDITOR
1048    /// Returns: Created pane ID (format: terminal_<id>)
1049    Edit {
1050        file: PathBuf,
1051
1052        /// Direction to open the new pane in
1053        #[clap(short, long, value_parser, conflicts_with("floating"))]
1054        direction: Option<Direction>,
1055
1056        /// Open the file in the specified line number
1057        #[clap(short, long, value_parser)]
1058        line_number: Option<usize>,
1059
1060        /// Open the new pane in floating mode
1061        #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
1062        floating: bool,
1063
1064        /// Open the new pane in place of the current pane, temporarily suspending it
1065        #[clap(
1066            short,
1067            long,
1068            value_parser,
1069            default_value("false"),
1070            takes_value(false),
1071            conflicts_with("floating"),
1072            conflicts_with("direction")
1073        )]
1074        in_place: bool,
1075
1076        /// Close the replaced pane instead of suspending it (only effective with --in-place)
1077        #[clap(
1078            long,
1079            value_parser,
1080            default_value("false"),
1081            takes_value(false),
1082            requires("in-place")
1083        )]
1084        close_replaced_pane: bool,
1085
1086        /// Change the working directory of the editor
1087        #[clap(long, value_parser)]
1088        cwd: Option<PathBuf>,
1089        /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1090        #[clap(short, long, requires("floating"))]
1091        x: Option<String>,
1092        /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1093        #[clap(short, long, requires("floating"))]
1094        y: Option<String>,
1095        /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1096        #[clap(long, requires("floating"))]
1097        width: Option<String>,
1098        /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1099        #[clap(long, requires("floating"))]
1100        height: Option<String>,
1101        /// Whether to pin a floating pane so that it is always on top
1102        #[clap(long, requires("floating"))]
1103        pinned: Option<bool>,
1104        /// if set, will open the pane near the current one rather than following the user's focus
1105        #[clap(long)]
1106        near_current_pane: bool,
1107        /// start this pane without a border (warning: will make it impossible to move with the
1108        /// mouse)
1109        #[clap(short, long, value_parser)]
1110        borderless: Option<bool>,
1111        /// Target a specific tab by ID
1112        #[clap(
1113            long,
1114            value_parser,
1115            conflicts_with("near-current-pane"),
1116            conflicts_with("in-place")
1117        )]
1118        tab_id: Option<usize>,
1119    },
1120    /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session]
1121    SwitchMode {
1122        input_mode: InputMode,
1123    },
1124    /// Embed focused pane if floating or float focused pane if embedded
1125    TogglePaneEmbedOrFloating {
1126        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
1127        #[clap(short, long, value_parser)]
1128        pane_id: Option<String>,
1129    },
1130    /// Toggle the visibility of all floating panes in the current Tab, open one if none exist
1131    ToggleFloatingPanes {
1132        /// Target a specific tab by ID
1133        #[clap(short, long, value_parser)]
1134        tab_id: Option<usize>,
1135    },
1136    /// Show all floating panes in the specified tab (or active tab if tab_id is not provided).
1137    ///
1138    /// Returns exit code 0 if state was changed, 2 if already visible, 1 if tab not found.
1139    ShowFloatingPanes {
1140        #[clap(short, long, value_parser)]
1141        tab_id: Option<usize>,
1142    },
1143    /// Hide all floating panes in the specified tab (or active tab if tab_id is not provided).
1144    ///
1145    /// Returns exit code 0 if state was changed, 2 if already hidden, 1 if tab not found.
1146    HideFloatingPanes {
1147        #[clap(short, long, value_parser)]
1148        tab_id: Option<usize>,
1149    },
1150    /// Check if floating panes are visible in the specified tab (or active tab).
1151    ///
1152    /// Prints "true" to stdout and exits 0 if visible.
1153    /// Prints "false" to stdout and exits 1 if not visible.
1154    AreFloatingPanesVisible {
1155        #[clap(short, long, value_parser)]
1156        tab_id: Option<usize>,
1157    },
1158    /// Close the focused pane.
1159    ClosePane {
1160        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
1161        #[clap(short, long, value_parser)]
1162        pane_id: Option<String>,
1163    },
1164    /// Renames the focused pane
1165    RenamePane {
1166        name: String,
1167        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
1168        #[clap(short, long, value_parser)]
1169        pane_id: Option<String>,
1170    },
1171    /// Remove a previously set pane name
1172    UndoRenamePane {
1173        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
1174        #[clap(short, long, value_parser)]
1175        pane_id: Option<String>,
1176    },
1177    /// Go to the next tab.
1178    GoToNextTab,
1179    /// Go to the previous tab.
1180    GoToPreviousTab,
1181    /// Close the current tab.
1182    CloseTab {
1183        /// Target a specific tab by ID
1184        #[clap(short, long, value_parser)]
1185        tab_id: Option<usize>,
1186    },
1187    /// Go to tab with index [index]
1188    GoToTab {
1189        index: u32,
1190    },
1191    /// Go to tab with name [name]
1192    ///
1193    /// Returns: When --create is used and tab is created, outputs the tab ID as a single number
1194    GoToTabName {
1195        name: String,
1196        /// Create a tab if one does not exist.
1197        #[clap(short, long, value_parser)]
1198        create: bool,
1199    },
1200    /// Renames the focused pane
1201    RenameTab {
1202        name: String,
1203        /// Target a specific tab by ID
1204        #[clap(short, long, value_parser)]
1205        tab_id: Option<usize>,
1206    },
1207    /// Remove a previously set tab name
1208    UndoRenameTab {
1209        /// Target a specific tab by ID
1210        #[clap(short, long, value_parser)]
1211        tab_id: Option<usize>,
1212    },
1213    /// Go to tab with stable ID
1214    GoToTabById {
1215        id: u64,
1216    },
1217    /// Close tab with stable ID
1218    CloseTabById {
1219        id: u64,
1220    },
1221    /// Rename tab by stable ID
1222    RenameTabById {
1223        id: u64,
1224        name: String,
1225    },
1226    /// Create a new tab, optionally with a specified tab layout and name
1227    ///
1228    /// Returns: The created tab's ID as a single number on stdout
1229    NewTab {
1230        /// Layout to use for the new tab
1231        #[clap(short, long, value_parser, conflicts_with = "layout-string")]
1232        layout: Option<PathBuf>,
1233
1234        /// Raw KDL layout string to use directly (instead of a layout file path)
1235        #[clap(long, value_parser, conflicts_with = "layout")]
1236        layout_string: Option<String>,
1237
1238        /// Default folder to look for layouts
1239        #[clap(long, value_parser, requires("layout"))]
1240        layout_dir: Option<PathBuf>,
1241
1242        /// Name of the new tab
1243        #[clap(short, long, value_parser)]
1244        name: Option<String>,
1245
1246        /// Change the working directory of the new tab
1247        #[clap(short, long, value_parser)]
1248        cwd: Option<PathBuf>,
1249
1250        /// Optional initial command to run in the new tab
1251        #[clap(
1252            value_parser,
1253            conflicts_with("initial-plugin"),
1254            multiple_values(true),
1255            takes_value(true),
1256            last(true)
1257        )]
1258        initial_command: Vec<String>,
1259
1260        /// Initial plugin to load in the new tab
1261        #[clap(long, value_parser, conflicts_with("initial-command"))]
1262        initial_plugin: Option<String>,
1263
1264        /// Close the pane immediately when its command exits
1265        #[clap(
1266            long,
1267            value_parser,
1268            default_value("false"),
1269            takes_value(false),
1270            requires("initial-command")
1271        )]
1272        close_on_exit: bool,
1273
1274        /// Start the command suspended, only running it after you first press ENTER
1275        #[clap(
1276            long,
1277            value_parser,
1278            default_value("false"),
1279            takes_value(false),
1280            requires("initial-command")
1281        )]
1282        start_suspended: bool,
1283
1284        /// Block until the command exits successfully (exit status 0) OR its pane has been closed
1285        #[clap(
1286            long,
1287            value_parser,
1288            default_value("false"),
1289            takes_value(false),
1290            requires("initial-command"),
1291            conflicts_with("block-until-exit-failure"),
1292            conflicts_with("block-until-exit")
1293        )]
1294        block_until_exit_success: bool,
1295
1296        /// Block until the command exits with failure (non-zero exit status) OR its pane has been closed
1297        #[clap(
1298            long,
1299            value_parser,
1300            default_value("false"),
1301            takes_value(false),
1302            requires("initial-command"),
1303            conflicts_with("block-until-exit-success"),
1304            conflicts_with("block-until-exit")
1305        )]
1306        block_until_exit_failure: bool,
1307
1308        /// Block until the command exits (regardless of exit status) OR its pane has been closed
1309        #[clap(
1310            long,
1311            value_parser,
1312            default_value("false"),
1313            takes_value(false),
1314            requires("initial-command"),
1315            conflicts_with("block-until-exit-success"),
1316            conflicts_with("block-until-exit-failure")
1317        )]
1318        block_until_exit: bool,
1319    },
1320    /// Move the focused tab in the specified direction. [right|left]
1321    MoveTab {
1322        direction: Direction,
1323        /// Target a specific tab by ID
1324        #[clap(short, long, value_parser)]
1325        tab_id: Option<usize>,
1326    },
1327    PreviousSwapLayout {
1328        /// Target a specific tab by ID
1329        #[clap(short, long, value_parser)]
1330        tab_id: Option<usize>,
1331    },
1332    NextSwapLayout {
1333        /// Target a specific tab by ID
1334        #[clap(short, long, value_parser)]
1335        tab_id: Option<usize>,
1336    },
1337    /// Override the layout of the active tab
1338    OverrideLayout {
1339        /// Path to the layout file
1340        #[clap(
1341            value_parser,
1342            required_unless_present = "layout-string",
1343            conflicts_with = "layout-string"
1344        )]
1345        layout: Option<PathBuf>,
1346
1347        /// Raw KDL layout string to use directly (instead of a layout file path)
1348        #[clap(long, value_parser, conflicts_with = "layout")]
1349        layout_string: Option<String>,
1350
1351        /// Default folder to look for layouts
1352        #[clap(long, value_parser)]
1353        layout_dir: Option<PathBuf>,
1354
1355        /// Retain existing terminal panes that do not fit in the layout (default: false)
1356        #[clap(long, value_parser, takes_value(false), default_value("false"))]
1357        retain_existing_terminal_panes: bool,
1358
1359        /// Retain existing plugin panes that do not fit with the layout default: false)
1360        #[clap(long, value_parser, takes_value(false), default_value("false"))]
1361        retain_existing_plugin_panes: bool,
1362
1363        /// Only apply the layout to the active tab (uses just the first layout tab if it has
1364        /// multiple)
1365        #[clap(long, value_parser, takes_value(false), default_value("false"))]
1366        apply_only_to_active_tab: bool,
1367    },
1368    /// Query all tab names
1369    QueryTabNames,
1370    StartOrReloadPlugin {
1371        url: String,
1372        #[clap(short, long, value_parser)]
1373        configuration: Option<PluginUserConfiguration>,
1374    },
1375    /// Returns: Plugin pane ID (format: plugin_<id>) when creating or focusing plugin
1376    LaunchOrFocusPlugin {
1377        #[clap(short, long, value_parser)]
1378        floating: bool,
1379        #[clap(short, long, value_parser)]
1380        in_place: bool,
1381        /// Close the replaced pane instead of suspending it (only effective with --in-place)
1382        #[clap(
1383            long,
1384            value_parser,
1385            default_value("false"),
1386            takes_value(false),
1387            requires("in-place")
1388        )]
1389        close_replaced_pane: bool,
1390        #[clap(short, long, value_parser)]
1391        move_to_focused_tab: bool,
1392        url: String,
1393        #[clap(short, long, value_parser)]
1394        configuration: Option<PluginUserConfiguration>,
1395        #[clap(short, long, value_parser)]
1396        skip_plugin_cache: bool,
1397        /// Target a specific tab by ID
1398        #[clap(long, value_parser, conflicts_with("in-place"))]
1399        tab_id: Option<usize>,
1400    },
1401    /// Returns: Plugin pane ID (format: plugin_<id>)
1402    LaunchPlugin {
1403        #[clap(short, long, value_parser)]
1404        floating: bool,
1405        #[clap(short, long, value_parser)]
1406        in_place: bool,
1407        /// Close the replaced pane instead of suspending it (only effective with --in-place)
1408        #[clap(
1409            long,
1410            value_parser,
1411            default_value("false"),
1412            takes_value(false),
1413            requires("in-place")
1414        )]
1415        close_replaced_pane: bool,
1416        url: Url,
1417        #[clap(short, long, value_parser)]
1418        configuration: Option<PluginUserConfiguration>,
1419        #[clap(short, long, value_parser)]
1420        skip_plugin_cache: bool,
1421        /// Target a specific tab by ID
1422        #[clap(long, value_parser, conflicts_with("in-place"))]
1423        tab_id: Option<usize>,
1424    },
1425    RenameSession {
1426        name: String,
1427    },
1428    /// Send data to one or more plugins, launch them if they are not running.
1429    #[clap(override_usage(
1430r#"
1431zellij action pipe [OPTIONS] [--] <PAYLOAD>
1432
1433* Send data to a specific plugin:
1434
1435zellij action pipe --plugin file:/path/to/my/plugin.wasm --name my_pipe_name -- my_arbitrary_data
1436
1437* To all running plugins (that are listening):
1438
1439zellij action pipe --name my_pipe_name -- my_arbitrary_data
1440
1441* Pipe data into this command's STDIN and get output from the plugin on this command's STDOUT
1442
1443tail -f /tmp/my-live-logfile | zellij action pipe --name logs --plugin https://example.com/my-plugin.wasm | wc -l
1444"#))]
1445    Pipe {
1446        /// The name of the pipe
1447        #[clap(short, long, value_parser, display_order(1))]
1448        name: Option<String>,
1449        /// The data to send down this pipe (if blank, will listen to STDIN)
1450        payload: Option<String>,
1451
1452        #[clap(short, long, value_parser, display_order(2))]
1453        /// The args of the pipe
1454        args: Option<PluginUserConfiguration>, // TODO: we might want to not re-use
1455        // PluginUserConfiguration
1456        /// The plugin url (eg. file:/tmp/my-plugin.wasm) to direct this pipe to, if not specified,
1457        /// will be sent to all plugins, if specified and is not running, the plugin will be launched
1458        #[clap(short, long, value_parser, display_order(3))]
1459        plugin: Option<String>,
1460        /// The plugin configuration (note: the same plugin with different configuration is
1461        /// considered a different plugin for the purposes of determining the pipe destination)
1462        #[clap(short('c'), long, value_parser, display_order(4))]
1463        plugin_configuration: Option<PluginUserConfiguration>,
1464        /// Launch a new plugin even if one is already running
1465        #[clap(
1466            short('l'),
1467            long,
1468            value_parser,
1469            takes_value(false),
1470            default_value("false"),
1471            display_order(5)
1472        )]
1473        force_launch_plugin: bool,
1474        /// If launching a new plugin, skip cache and force-compile the plugin
1475        #[clap(
1476            short('s'),
1477            long,
1478            value_parser,
1479            takes_value(false),
1480            default_value("false"),
1481            display_order(6)
1482        )]
1483        skip_plugin_cache: bool,
1484        /// If launching a plugin, should it be floating or not, defaults to floating
1485        #[clap(short('f'), long, value_parser, display_order(7))]
1486        floating_plugin: Option<bool>,
1487        /// If launching a plugin, launch it in-place (on top of the current pane)
1488        #[clap(
1489            short('i'),
1490            long,
1491            value_parser,
1492            conflicts_with("floating-plugin"),
1493            display_order(8)
1494        )]
1495        in_place_plugin: Option<bool>,
1496        /// If launching a plugin, specify its working directory
1497        #[clap(short('w'), long, value_parser, display_order(9))]
1498        plugin_cwd: Option<PathBuf>,
1499        /// If launching a plugin, specify its pane title
1500        #[clap(short('t'), long, value_parser, display_order(10))]
1501        plugin_title: Option<String>,
1502    },
1503    ListClients,
1504    /// List all panes in the current session
1505    ///
1506    /// Returns: Formatted list of panes (table or JSON) to stdout
1507    ListPanes {
1508        /// Include tab information (name, position, ID)
1509        #[clap(short, long, value_parser)]
1510        tab: bool,
1511
1512        /// Include running command information
1513        #[clap(short, long, value_parser)]
1514        command: bool,
1515
1516        /// Include pane state (focused, floating, exited, etc.)
1517        #[clap(short, long, value_parser)]
1518        state: bool,
1519
1520        /// Include geometry (position, size)
1521        #[clap(short, long, value_parser)]
1522        geometry: bool,
1523
1524        /// Include all available fields
1525        #[clap(short, long, value_parser)]
1526        all: bool,
1527
1528        /// Output as JSON
1529        #[clap(short, long, value_parser)]
1530        json: bool,
1531    },
1532    /// List all tabs with their information
1533    ///
1534    /// Returns: Tab information in table or JSON format
1535    ListTabs {
1536        /// Include state information (active, fullscreen, sync, floating visibility)
1537        #[clap(short, long, value_parser)]
1538        state: bool,
1539
1540        /// Include dimension information (viewport, display area)
1541        #[clap(short, long, value_parser)]
1542        dimensions: bool,
1543
1544        /// Include pane counts
1545        #[clap(short, long, value_parser)]
1546        panes: bool,
1547
1548        /// Include layout information (swap layout name and dirty state)
1549        #[clap(short, long, value_parser)]
1550        layout: bool,
1551
1552        /// Include all available fields
1553        #[clap(short, long, value_parser)]
1554        all: bool,
1555
1556        /// Output as JSON
1557        #[clap(short, long, value_parser)]
1558        json: bool,
1559    },
1560    /// Get information about the currently active tab
1561    ///
1562    /// Returns: Tab name and ID by default, or full info in JSON
1563    CurrentTabInfo {
1564        /// Output as JSON with full TabInfo
1565        #[clap(short, long, value_parser)]
1566        json: bool,
1567    },
1568    TogglePanePinned {
1569        /// Target a specific pane by ID (eg. terminal_1, plugin_2, or 3)
1570        #[clap(short, long, value_parser)]
1571        pane_id: Option<String>,
1572    },
1573    /// Stack pane ids
1574    /// Ids are a space separated list of pane ids.
1575    /// They should either be in the form of `terminal_<int>` (eg. terminal_1), `plugin_<int>` (eg.
1576    /// plugin_1) or bare integers in which case they'll be considered terminals (eg. 1 is
1577    /// the equivalent of terminal_1)
1578    ///
1579    /// Example: zellij action stack-panes -- terminal_1 plugin_2 3
1580    StackPanes {
1581        #[clap(last(true), required(true))]
1582        pane_ids: Vec<String>,
1583    },
1584    ChangeFloatingPaneCoordinates {
1585        /// The pane_id of the floating pane, eg.  terminal_1, plugin_2 or 3 (equivalent to
1586        /// terminal_3)
1587        #[clap(short, long, value_parser)]
1588        pane_id: String,
1589        /// The x coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1590        #[clap(short, long)]
1591        x: Option<String>,
1592        /// The y coordinates if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1593        #[clap(short, long)]
1594        y: Option<String>,
1595        /// The width if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1596        #[clap(long)]
1597        width: Option<String>,
1598        /// The height if the pane is floating as a bare integer (eg. 1) or percent (eg. 10%)
1599        #[clap(long)]
1600        height: Option<String>,
1601        /// Whether to pin a floating pane so that it is always on top
1602        #[clap(long)]
1603        pinned: Option<bool>,
1604        /// change this pane to be with/without a border (warning: will make it impossible to move with the
1605        /// mouse if without a border)
1606        #[clap(short, long, value_parser)]
1607        borderless: Option<bool>,
1608    },
1609    TogglePaneBorderless {
1610        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
1611        #[clap(short, long, value_parser)]
1612        pane_id: String,
1613    },
1614    SetPaneBorderless {
1615        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3)
1616        #[clap(short, long, value_parser)]
1617        pane_id: String,
1618        /// Whether the pane should be borderless (flag present) or bordered (flag absent)
1619        #[clap(short, long, value_parser)]
1620        borderless: bool,
1621    },
1622    /// Detach from the current session
1623    Detach,
1624    /// Switch the theme to dark (uses configured `theme_dark`).
1625    SetDarkTheme,
1626    /// Switch the theme to light (uses configured `theme_light`).
1627    SetLightTheme,
1628    /// Toggle between dark and light themes (used configured `theme_dark` and `theme_light`)
1629    ToggleTheme,
1630    /// Switch to a different session
1631    SwitchSession {
1632        /// Name of the session to switch to
1633        name: String,
1634        /// Optional tab position to focus
1635        #[clap(long)]
1636        tab_position: Option<usize>,
1637        /// Optional pane ID to focus (eg. "terminal_1" for terminal pane with id 1, or "plugin_2" for plugin pane with id 2)
1638        #[clap(long)]
1639        pane_id: Option<String>,
1640        /// Layout to apply when switching to the session (relative paths start at layout-dir)
1641        #[clap(short, long, value_parser, conflicts_with = "layout-string")]
1642        layout: Option<PathBuf>,
1643        /// Raw KDL layout string to use directly
1644        #[clap(long, value_parser, conflicts_with = "layout")]
1645        layout_string: Option<String>,
1646        /// Default folder to look for layouts
1647        #[clap(long, value_parser, requires("layout"))]
1648        layout_dir: Option<PathBuf>,
1649        /// Change the working directory when switching
1650        #[clap(short, long, value_parser)]
1651        cwd: Option<PathBuf>,
1652    },
1653    /// Set the default foreground/background color of a pane
1654    SetPaneColor {
1655        /// The pane_id of the pane, eg. terminal_1, plugin_2 or 3 (equivalent to terminal_3).
1656        /// Defaults to $ZELLIJ_PANE_ID if not provided.
1657        #[clap(short, long, value_parser)]
1658        pane_id: Option<String>,
1659        /// Foreground color (e.g. "#00e000", "rgb:00/e0/00")
1660        #[clap(long, value_parser)]
1661        fg: Option<String>,
1662        /// Background color (e.g. "#001a3a", "rgb:00/1a/3a")
1663        #[clap(long, value_parser)]
1664        bg: Option<String>,
1665        /// Reset pane colors to terminal defaults
1666        #[clap(long, value_parser, conflicts_with_all(&["fg", "bg"]))]
1667        reset: bool,
1668    },
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673    use super::*;
1674    use clap::Parser;
1675
1676    fn parse_subscribe(args: &[&str]) -> SubscribeCli {
1677        let mut full_args = vec!["zellij"];
1678        full_args.extend_from_slice(args);
1679        let cli = CliArgs::try_parse_from(full_args).unwrap();
1680        match cli.command {
1681            Some(Command::Subscribe(s)) => s,
1682            other => panic!("Expected Subscribe, got {:?}", other),
1683        }
1684    }
1685
1686    #[test]
1687    fn subscribe_scrollback_bare_flag() {
1688        let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1", "--scrollback"]);
1689        assert_eq!(s.scrollback, Some(0));
1690    }
1691
1692    #[test]
1693    fn subscribe_scrollback_with_value() {
1694        let s = parse_subscribe(&[
1695            "subscribe",
1696            "--pane-id",
1697            "terminal_1",
1698            "--scrollback",
1699            "100",
1700        ]);
1701        assert_eq!(s.scrollback, Some(100));
1702    }
1703
1704    #[test]
1705    fn subscribe_scrollback_absent() {
1706        let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1"]);
1707        assert_eq!(s.scrollback, None);
1708    }
1709
1710    #[test]
1711    fn subscribe_format_json() {
1712        let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1", "--format", "json"]);
1713        assert!(matches!(s.format, SubscribeFormat::Json));
1714    }
1715
1716    #[test]
1717    fn subscribe_format_default_raw() {
1718        let s = parse_subscribe(&["subscribe", "--pane-id", "terminal_1"]);
1719        assert!(matches!(s.format, SubscribeFormat::Raw));
1720    }
1721
1722    #[test]
1723    fn subscribe_multiple_pane_ids() {
1724        let s = parse_subscribe(&[
1725            "subscribe",
1726            "--pane-id",
1727            "terminal_1",
1728            "--pane-id",
1729            "plugin_2",
1730        ]);
1731        assert_eq!(
1732            s.pane_id,
1733            vec!["terminal_1".to_string(), "plugin_2".to_string()]
1734        );
1735    }
1736
1737    #[test]
1738    fn subscribe_requires_pane_id() {
1739        let result = CliArgs::try_parse_from(["zellij", "subscribe"]);
1740        assert!(result.is_err());
1741    }
1742}