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