Skip to main content

zellij_utils/input/
actions.rs

1//! Definition of the actions that can be bound to keys.
2
3pub use super::command::{OpenFilePayload, RunCommandAction};
4use super::layout::{
5    FloatingPaneLayout, Layout, PluginAlias, RunPlugin, RunPluginLocation, RunPluginOrAlias,
6    SwapFloatingLayout, SwapTiledLayout, TabLayoutInfo, TiledPaneLayout,
7};
8use crate::cli::CliAction;
9use crate::data::{
10    CommandOrPlugin, Direction, KeyWithModifier, LayoutInfo, NewPanePlacement, OriginatingPlugin,
11    PaneId, Resize, UnblockCondition,
12};
13use crate::data::{FloatingPaneCoordinates, InputMode};
14use crate::home::{find_default_config_dir, get_layout_dir};
15use crate::input::config::{Config, ConfigError, KdlError};
16use crate::input::mouse::MouseEvent;
17use crate::input::options::OnForceClose;
18use miette::{NamedSource, Report};
19use serde::{Deserialize, Serialize};
20use std::collections::BTreeMap;
21use uuid::Uuid;
22
23use std::path::PathBuf;
24use std::str::FromStr;
25
26use crate::position::Position;
27
28#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
29pub enum ResizeDirection {
30    Left,
31    Right,
32    Up,
33    Down,
34    Increase,
35    Decrease,
36}
37
38impl FromStr for ResizeDirection {
39    type Err = String;
40    fn from_str(s: &str) -> Result<Self, Self::Err> {
41        match s {
42            "Left" | "left" => Ok(ResizeDirection::Left),
43            "Right" | "right" => Ok(ResizeDirection::Right),
44            "Up" | "up" => Ok(ResizeDirection::Up),
45            "Down" | "down" => Ok(ResizeDirection::Down),
46            "Increase" | "increase" | "+" => Ok(ResizeDirection::Increase),
47            "Decrease" | "decrease" | "-" => Ok(ResizeDirection::Decrease),
48            _ => Err(format!(
49                "Failed to parse ResizeDirection. Unknown ResizeDirection: {}",
50                s
51            )),
52        }
53    }
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
57pub enum SearchDirection {
58    Down,
59    Up,
60}
61
62impl FromStr for SearchDirection {
63    type Err = String;
64    fn from_str(s: &str) -> Result<Self, Self::Err> {
65        match s {
66            "Down" | "down" => Ok(SearchDirection::Down),
67            "Up" | "up" => Ok(SearchDirection::Up),
68            _ => Err(format!(
69                "Failed to parse SearchDirection. Unknown SearchDirection: {}",
70                s
71            )),
72        }
73    }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
77pub enum SearchOption {
78    CaseSensitivity,
79    WholeWord,
80    Wrap,
81}
82
83impl FromStr for SearchOption {
84    type Err = String;
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        match s {
87            "CaseSensitivity" | "casesensitivity" | "Casesensitivity" => {
88                Ok(SearchOption::CaseSensitivity)
89            },
90            "WholeWord" | "wholeword" | "Wholeword" => Ok(SearchOption::WholeWord),
91            "Wrap" | "wrap" => Ok(SearchOption::Wrap),
92            _ => Err(format!(
93                "Failed to parse SearchOption. Unknown SearchOption: {}",
94                s
95            )),
96        }
97    }
98}
99
100// As these actions are bound to the default config, please
101// do take care when refactoring - or renaming.
102// They might need to be adjusted in the default config
103// as well `../../assets/config/default.yaml`
104/// Actions that can be bound to keys.
105#[derive(
106    Clone,
107    Debug,
108    PartialEq,
109    Eq,
110    Deserialize,
111    Serialize,
112    strum_macros::Display,
113    strum_macros::EnumString,
114    strum_macros::EnumIter,
115)]
116#[strum(ascii_case_insensitive)]
117pub enum Action {
118    /// Quit Zellij.
119    Quit,
120    /// Write to the terminal.
121    Write {
122        key_with_modifier: Option<KeyWithModifier>,
123        bytes: Vec<u8>,
124        is_kitty_keyboard_protocol: bool,
125    },
126    /// Write Characters to the terminal.
127    WriteChars {
128        chars: String,
129    },
130    /// Write to a specific pane by ID.
131    WriteToPaneId {
132        bytes: Vec<u8>,
133        pane_id: PaneId,
134    },
135    /// Write Characters to a specific pane by ID.
136    WriteCharsToPaneId {
137        chars: String,
138        pane_id: PaneId,
139    },
140    /// Paste text using bracketed paste mode, optionally to a specific pane.
141    Paste {
142        chars: String,
143        pane_id: Option<PaneId>,
144    },
145    /// Switch to the specified input mode.
146    SwitchToMode {
147        input_mode: InputMode,
148    },
149    /// Switch all connected clients to the specified input mode.
150    SwitchModeForAllClients {
151        input_mode: InputMode,
152    },
153    /// Shrink/enlarge focused pane at specified border
154    Resize {
155        resize: Resize,
156        direction: Option<Direction>,
157    },
158    /// Switch focus to next pane in specified direction.
159    FocusNextPane,
160    FocusPreviousPane,
161    /// Move the focus pane in specified direction.
162    SwitchFocus,
163    MoveFocus {
164        direction: Direction,
165    },
166    /// Tries to move the focus pane in specified direction.
167    /// If there is no pane in the direction, move to previous/next Tab.
168    MoveFocusOrTab {
169        direction: Direction,
170    },
171    MovePane {
172        direction: Option<Direction>,
173    },
174    MovePaneBackwards,
175    /// Clear all buffers of a current screen
176    ClearScreen,
177    /// Dumps the screen to a file or STDOUT
178    DumpScreen {
179        file_path: Option<String>,
180        include_scrollback: bool,
181        pane_id: Option<PaneId>,
182        ansi: bool,
183    },
184    /// Dumps
185    DumpLayout,
186    /// Save the current session state to disk
187    SaveSession,
188    EditScrollback {
189        ansi: bool,
190    },
191    /// Scroll up in focus pane.
192    ScrollUp,
193    /// Scroll up at point
194    ScrollUpAt {
195        position: Position,
196    },
197    /// Scroll down in focus pane.
198    ScrollDown,
199    /// Scroll down at point
200    ScrollDownAt {
201        position: Position,
202    },
203    /// Scroll down to bottom in focus pane.
204    ScrollToBottom,
205    /// Scroll up to top in focus pane.
206    ScrollToTop,
207    /// Scroll up one page in focus pane.
208    PageScrollUp,
209    /// Scroll down one page in focus pane.
210    PageScrollDown,
211    /// Scroll up half page in focus pane.
212    HalfPageScrollUp,
213    /// Scroll down half page in focus pane.
214    HalfPageScrollDown,
215    /// Toggle between fullscreen focus pane and normal layout.
216    ToggleFocusFullscreen,
217    /// Toggle frames around panes in the UI
218    TogglePaneFrames,
219    /// Toggle between sending text commands to all panes on the current tab and normal mode.
220    ToggleActiveSyncTab,
221    /// Open a new pane in the specified direction (relative to focus).
222    /// If no direction is specified, will try to use the biggest available space.
223    NewPane {
224        direction: Option<Direction>,
225        pane_name: Option<String>,
226        start_suppressed: bool,
227    },
228    /// Returns: Created pane ID (format: terminal_<id>)
229    NewBlockingPane {
230        placement: NewPanePlacement,
231        pane_name: Option<String>,
232        command: Option<RunCommandAction>,
233        unblock_condition: Option<UnblockCondition>,
234        near_current_pane: bool,
235        tab_id: Option<usize>,
236    },
237    /// Open the file in a new pane using the default editor
238    /// Returns: Created pane ID (format: terminal_<id>)
239    EditFile {
240        payload: OpenFilePayload,
241        direction: Option<Direction>,
242        floating: bool,
243        in_place: bool,
244        close_replaced_pane: bool,
245        start_suppressed: bool,
246        coordinates: Option<FloatingPaneCoordinates>,
247        near_current_pane: bool,
248        tab_id: Option<usize>,
249    },
250    /// Open a new floating pane
251    /// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
252    NewFloatingPane {
253        command: Option<RunCommandAction>,
254        pane_name: Option<String>,
255        coordinates: Option<FloatingPaneCoordinates>,
256        near_current_pane: bool,
257        tab_id: Option<usize>,
258    },
259    /// Open a new tiled (embedded, non-floating) pane
260    /// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
261    NewTiledPane {
262        direction: Option<Direction>,
263        command: Option<RunCommandAction>,
264        pane_name: Option<String>,
265        near_current_pane: bool,
266        borderless: Option<bool>,
267        tab_id: Option<usize>,
268    },
269    /// Open a new pane in place of the focused one, suppressing it instead
270    /// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
271    NewInPlacePane {
272        command: Option<RunCommandAction>,
273        pane_name: Option<String>,
274        near_current_pane: bool,
275        pane_id_to_replace: Option<PaneId>,
276        close_replaced_pane: bool,
277        tab_id: Option<usize>,
278    },
279    /// Returns: Created pane ID (format: terminal_<id> or plugin_<id>)
280    NewStackedPane {
281        command: Option<RunCommandAction>,
282        pane_name: Option<String>,
283        near_current_pane: bool,
284        tab_id: Option<usize>,
285    },
286    /// Embed focused pane in tab if floating or float focused pane if embedded
287    TogglePaneEmbedOrFloating,
288    /// Toggle the visibility of all floating panes (if any) in the current Tab
289    ToggleFloatingPanes,
290    /// Show all floating panes in the specified tab (or active tab if tab_id is None)
291    ShowFloatingPanes {
292        tab_id: Option<usize>,
293    },
294    /// Hide all floating panes in the specified tab (or active tab if tab_id is None)
295    HideFloatingPanes {
296        tab_id: Option<usize>,
297    },
298    /// Check if floating panes are visible in the specified tab (or active tab if tab_id is None)
299    AreFloatingPanesVisible {
300        tab_id: Option<usize>,
301    },
302    /// Close the focus pane.
303    CloseFocus,
304    PaneNameInput {
305        input: Vec<u8>,
306    },
307    UndoRenamePane,
308    /// Create a new tab, optionally with a specified tab layout.
309    NewTab {
310        tiled_layout: Option<TiledPaneLayout>,
311        floating_layouts: Vec<FloatingPaneLayout>,
312        swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
313        swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
314        tab_name: Option<String>,
315        should_change_focus_to_new_tab: bool,
316        cwd: Option<PathBuf>,
317        initial_panes: Option<Vec<CommandOrPlugin>>,
318        first_pane_unblock_condition: Option<UnblockCondition>,
319    },
320    /// Do nothing.
321    NoOp,
322    /// Go to the next tab.
323    GoToNextTab,
324    /// Go to the previous tab.
325    GoToPreviousTab,
326    /// Close the current tab.
327    CloseTab,
328    GoToTab {
329        index: u32,
330    },
331    GoToTabName {
332        name: String,
333        create: bool,
334    },
335    ToggleTab,
336    TabNameInput {
337        input: Vec<u8>,
338    },
339    UndoRenameTab,
340    MoveTab {
341        direction: Direction,
342    },
343    /// Run specified command in new pane.
344    Run {
345        command: RunCommandAction,
346        near_current_pane: bool,
347    },
348    /// Set pane default foreground/background color
349    SetPaneColor {
350        pane_id: PaneId,
351        fg: Option<String>,
352        bg: Option<String>,
353    },
354    /// Detach session and exit
355    Detach,
356    /// Switch to a different session
357    SwitchSession {
358        name: String,
359        tab_position: Option<usize>,
360        pane_id: Option<(u32, bool)>, // (id, is_plugin)
361        layout: Option<LayoutInfo>,
362        cwd: Option<PathBuf>,
363    },
364    /// Returns: Plugin pane ID (format: plugin_<id>) when creating or focusing plugin
365    LaunchOrFocusPlugin {
366        plugin: RunPluginOrAlias,
367        should_float: bool,
368        move_to_focused_tab: bool,
369        should_open_in_place: bool,
370        close_replaced_pane: bool,
371        skip_cache: bool,
372        tab_id: Option<usize>,
373    },
374    /// Returns: Plugin pane ID (format: plugin_<id>)
375    LaunchPlugin {
376        plugin: RunPluginOrAlias,
377        should_float: bool,
378        should_open_in_place: bool,
379        close_replaced_pane: bool,
380        skip_cache: bool,
381        cwd: Option<PathBuf>,
382        tab_id: Option<usize>,
383    },
384    MouseEvent {
385        event: MouseEvent,
386    },
387    Copy,
388    /// Confirm a prompt
389    Confirm,
390    /// Deny a prompt
391    Deny,
392    /// Confirm an action that invokes a prompt automatically
393    SkipConfirm {
394        action: Box<Action>,
395    },
396    /// Search for String
397    SearchInput {
398        input: Vec<u8>,
399    },
400    /// Search for something
401    Search {
402        direction: SearchDirection,
403    },
404    /// Toggle case sensitivity of search
405    SearchToggleOption {
406        option: SearchOption,
407    },
408    ToggleMouseMode,
409    PreviousSwapLayout,
410    NextSwapLayout,
411    /// Override the layout of the active tab
412    OverrideLayout {
413        tabs: Vec<TabLayoutInfo>,
414        retain_existing_terminal_panes: bool,
415        retain_existing_plugin_panes: bool,
416        apply_only_to_active_tab: bool,
417    },
418    /// Query all tab names
419    QueryTabNames,
420    /// Open a new tiled (embedded, non-floating) plugin pane
421    /// Returns: Created pane ID (format: plugin_<id>)
422    NewTiledPluginPane {
423        plugin: RunPluginOrAlias,
424        pane_name: Option<String>,
425        skip_cache: bool,
426        cwd: Option<PathBuf>,
427        tab_id: Option<usize>,
428    },
429    /// Returns: Created pane ID (format: plugin_<id>)
430    NewFloatingPluginPane {
431        plugin: RunPluginOrAlias,
432        pane_name: Option<String>,
433        skip_cache: bool,
434        cwd: Option<PathBuf>,
435        coordinates: Option<FloatingPaneCoordinates>,
436        tab_id: Option<usize>,
437    },
438    /// Returns: Created pane ID (format: plugin_<id>)
439    NewInPlacePluginPane {
440        plugin: RunPluginOrAlias,
441        pane_name: Option<String>,
442        skip_cache: bool,
443        close_replaced_pane: bool,
444        tab_id: Option<usize>,
445    },
446    StartOrReloadPlugin {
447        plugin: RunPluginOrAlias,
448    },
449    CloseTerminalPane {
450        pane_id: u32,
451    },
452    ClosePluginPane {
453        pane_id: u32,
454    },
455    FocusTerminalPaneWithId {
456        pane_id: u32,
457        should_float_if_hidden: bool,
458        should_be_in_place_if_hidden: bool,
459    },
460    FocusPluginPaneWithId {
461        pane_id: u32,
462        should_float_if_hidden: bool,
463        should_be_in_place_if_hidden: bool,
464    },
465    RenameTerminalPane {
466        pane_id: u32,
467        name: Vec<u8>,
468    },
469    RenamePluginPane {
470        pane_id: u32,
471        name: Vec<u8>,
472    },
473    RenameTab {
474        tab_index: u32,
475        name: Vec<u8>,
476    },
477    GoToTabById {
478        id: u64,
479    },
480    CloseTabById {
481        id: u64,
482    },
483    RenameTabById {
484        id: u64,
485        name: String,
486    },
487    BreakPane,
488    BreakPaneRight,
489    BreakPaneLeft,
490    RenameSession {
491        name: String,
492    },
493    CliPipe {
494        pipe_id: String,
495        name: Option<String>,
496        payload: Option<String>,
497        args: Option<BTreeMap<String, String>>,
498        plugin: Option<String>,
499        configuration: Option<BTreeMap<String, String>>,
500        launch_new: bool,
501        skip_cache: bool,
502        floating: Option<bool>,
503        in_place: Option<bool>,
504        cwd: Option<PathBuf>,
505        pane_title: Option<String>,
506    },
507    KeybindPipe {
508        name: Option<String>,
509        payload: Option<String>,
510        args: Option<BTreeMap<String, String>>,
511        plugin: Option<String>,
512        plugin_id: Option<u32>, // supercedes plugin if present
513        configuration: Option<BTreeMap<String, String>>,
514        launch_new: bool,
515        skip_cache: bool,
516        floating: Option<bool>,
517        in_place: Option<bool>,
518        cwd: Option<PathBuf>,
519        pane_title: Option<String>,
520    },
521    ListClients,
522    ListPanes {
523        show_tab: bool,
524        show_command: bool,
525        show_state: bool,
526        show_geometry: bool,
527        show_all: bool,
528        output_json: bool,
529    },
530    ListTabs {
531        show_state: bool,
532        show_dimensions: bool,
533        show_panes: bool,
534        show_layout: bool,
535        show_all: bool,
536        output_json: bool,
537    },
538    CurrentTabInfo {
539        output_json: bool,
540    },
541    TogglePanePinned,
542    StackPanes {
543        pane_ids: Vec<PaneId>,
544    },
545    ChangeFloatingPaneCoordinates {
546        pane_id: PaneId,
547        coordinates: FloatingPaneCoordinates,
548    },
549    TogglePaneBorderless {
550        pane_id: PaneId,
551    },
552    SetPaneBorderless {
553        pane_id: PaneId,
554        borderless: bool,
555    },
556    TogglePaneInGroup,
557    ToggleGroupMarking,
558    // Pane-targeting CLI-only variants
559    ScrollUpByPaneId {
560        pane_id: PaneId,
561    },
562    ScrollDownByPaneId {
563        pane_id: PaneId,
564    },
565    ScrollToTopByPaneId {
566        pane_id: PaneId,
567    },
568    ScrollToBottomByPaneId {
569        pane_id: PaneId,
570    },
571    PageScrollUpByPaneId {
572        pane_id: PaneId,
573    },
574    PageScrollDownByPaneId {
575        pane_id: PaneId,
576    },
577    HalfPageScrollUpByPaneId {
578        pane_id: PaneId,
579    },
580    HalfPageScrollDownByPaneId {
581        pane_id: PaneId,
582    },
583    ResizeByPaneId {
584        pane_id: PaneId,
585        resize: Resize,
586        direction: Option<Direction>,
587    },
588    MovePaneByPaneId {
589        pane_id: PaneId,
590        direction: Option<Direction>,
591    },
592    MovePaneBackwardsByPaneId {
593        pane_id: PaneId,
594    },
595    ClearScreenByPaneId {
596        pane_id: PaneId,
597    },
598    EditScrollbackByPaneId {
599        pane_id: PaneId,
600        ansi: bool,
601    },
602    ToggleFocusFullscreenByPaneId {
603        pane_id: PaneId,
604    },
605    TogglePaneEmbedOrFloatingByPaneId {
606        pane_id: PaneId,
607    },
608    CloseFocusByPaneId {
609        pane_id: PaneId,
610    },
611    RenamePaneByPaneId {
612        pane_id: Option<PaneId>,
613        name: Vec<u8>,
614    },
615    UndoRenamePaneByPaneId {
616        pane_id: PaneId,
617    },
618    TogglePanePinnedByPaneId {
619        pane_id: PaneId,
620    },
621    FocusPaneByPaneId {
622        pane_id: PaneId,
623    },
624    // Tab-targeting CLI-only variants
625    UndoRenameTabByTabId {
626        id: u64,
627    },
628    ToggleActiveSyncTabByTabId {
629        id: u64,
630    },
631    ToggleFloatingPanesByTabId {
632        id: u64,
633    },
634    PreviousSwapLayoutByTabId {
635        id: u64,
636    },
637    NextSwapLayoutByTabId {
638        id: u64,
639    },
640    MoveTabByTabId {
641        id: u64,
642        direction: Direction,
643    },
644}
645
646impl Default for Action {
647    fn default() -> Self {
648        Action::NoOp
649    }
650}
651
652impl Default for SearchDirection {
653    fn default() -> Self {
654        SearchDirection::Down
655    }
656}
657
658impl Default for SearchOption {
659    fn default() -> Self {
660        SearchOption::CaseSensitivity
661    }
662}
663
664impl Action {
665    /// Checks that two Action are match except their mutable attributes.
666    pub fn shallow_eq(&self, other_action: &Action) -> bool {
667        match (self, other_action) {
668            (Action::NewTab { .. }, Action::NewTab { .. }) => true,
669            (Action::LaunchOrFocusPlugin { .. }, Action::LaunchOrFocusPlugin { .. }) => true,
670            (Action::LaunchPlugin { .. }, Action::LaunchPlugin { .. }) => true,
671            (Action::OverrideLayout { .. }, Action::OverrideLayout { .. }) => true,
672            _ => self == other_action,
673        }
674    }
675
676    pub fn actions_from_cli(
677        cli_action: CliAction,
678        get_current_dir: Box<dyn Fn() -> PathBuf>,
679        config: Option<Config>,
680    ) -> Result<Vec<Action>, String> {
681        match cli_action {
682            CliAction::Write { bytes, pane_id } => match pane_id {
683                Some(pane_id_str) => {
684                    let parsed_pane_id = PaneId::from_str(&pane_id_str);
685                    match parsed_pane_id {
686                            Ok(parsed_pane_id) => {
687                                Ok(vec![Action::WriteToPaneId {
688                                    bytes,
689                                    pane_id: parsed_pane_id,
690                                }])
691                            },
692                            Err(_e) => {
693                                Err(format!(
694                                    "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
695                                    pane_id_str
696                                ))
697                            }
698                        }
699                },
700                None => Ok(vec![Action::Write {
701                    key_with_modifier: None,
702                    bytes,
703                    is_kitty_keyboard_protocol: false,
704                }]),
705            },
706            CliAction::WriteChars { chars, pane_id } => match pane_id {
707                Some(pane_id_str) => {
708                    let parsed_pane_id = PaneId::from_str(&pane_id_str);
709                    match parsed_pane_id {
710                            Ok(parsed_pane_id) => {
711                                Ok(vec![Action::WriteCharsToPaneId {
712                                    chars,
713                                    pane_id: parsed_pane_id,
714                                }])
715                            },
716                            Err(_e) => {
717                                Err(format!(
718                                    "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
719                                    pane_id_str
720                                ))
721                            }
722                        }
723                },
724                None => Ok(vec![Action::WriteChars { chars }]),
725            },
726            CliAction::Paste { chars, pane_id } => match pane_id {
727                Some(pane_id_str) => {
728                    let parsed_pane_id = PaneId::from_str(&pane_id_str);
729                    match parsed_pane_id {
730                        Ok(parsed_pane_id) => {
731                            Ok(vec![Action::Paste {
732                                chars,
733                                pane_id: Some(parsed_pane_id),
734                            }])
735                        },
736                        Err(_e) => {
737                            Err(format!(
738                                "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
739                                pane_id_str
740                            ))
741                        }
742                    }
743                },
744                None => Ok(vec![Action::Paste {
745                    chars,
746                    pane_id: None,
747                }]),
748            },
749            CliAction::SendKeys { keys, pane_id } => {
750                let mut actions = Vec::new();
751
752                for (index, key_str) in keys.iter().enumerate() {
753                    let key = KeyWithModifier::from_str(key_str).map_err(|e| {
754                        let suggestion = suggest_key_fix(key_str);
755                        format!(
756                            "Invalid key at position {}: \"{}\"\n  Error: {}\n{}",
757                            index + 1,
758                            key_str,
759                            e,
760                            suggestion
761                        )
762                    })?;
763
764                    #[cfg(not(target_family = "wasm"))]
765                    let bytes = key
766                        .serialize_kitty()
767                        .map(|s| s.into_bytes())
768                        .unwrap_or_else(Vec::new);
769
770                    #[cfg(target_family = "wasm")]
771                    let bytes = vec![];
772
773                    match &pane_id {
774                        Some(pane_id_str) => {
775                            let parsed_pane_id = PaneId::from_str(pane_id_str)
776                                .map_err(|_| format!(
777                                    "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
778                                    pane_id_str
779                                ))?;
780                            actions.push(Action::WriteToPaneId {
781                                bytes,
782                                pane_id: parsed_pane_id,
783                            });
784                        },
785                        None => {
786                            actions.push(Action::Write {
787                                key_with_modifier: Some(key),
788                                bytes,
789                                is_kitty_keyboard_protocol: true,
790                            });
791                        },
792                    }
793                }
794
795                Ok(actions)
796            },
797            CliAction::Resize {
798                resize,
799                direction,
800                pane_id,
801            } => match pane_id {
802                Some(pane_id_str) => {
803                    let pane_id = PaneId::from_str(&pane_id_str)
804                        .map_err(|_| format!(
805                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
806                        ))?;
807                    Ok(vec![Action::ResizeByPaneId {
808                        pane_id,
809                        resize,
810                        direction,
811                    }])
812                },
813                None => Ok(vec![Action::Resize { resize, direction }]),
814            },
815            CliAction::FocusNextPane => Ok(vec![Action::FocusNextPane]),
816            CliAction::FocusPreviousPane => Ok(vec![Action::FocusPreviousPane]),
817            CliAction::FocusPaneId { pane_id } => {
818                let pane_id = PaneId::from_str(&pane_id)
819                    .map_err(|_| format!(
820                        "Malformed pane id: {pane_id}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
821                    ))?;
822                Ok(vec![Action::FocusPaneByPaneId { pane_id }])
823            },
824            CliAction::MoveFocus { direction } => Ok(vec![Action::MoveFocus { direction }]),
825            CliAction::MoveFocusOrTab { direction } => {
826                Ok(vec![Action::MoveFocusOrTab { direction }])
827            },
828            CliAction::MovePane { direction, pane_id } => match pane_id {
829                Some(pane_id_str) => {
830                    let pane_id = PaneId::from_str(&pane_id_str)
831                        .map_err(|_| format!(
832                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
833                        ))?;
834                    Ok(vec![Action::MovePaneByPaneId { pane_id, direction }])
835                },
836                None => Ok(vec![Action::MovePane { direction }]),
837            },
838            CliAction::MovePaneBackwards { pane_id } => match pane_id {
839                Some(pane_id_str) => {
840                    let pane_id = PaneId::from_str(&pane_id_str)
841                        .map_err(|_| format!(
842                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
843                        ))?;
844                    Ok(vec![Action::MovePaneBackwardsByPaneId { pane_id }])
845                },
846                None => Ok(vec![Action::MovePaneBackwards]),
847            },
848            CliAction::MoveTab { direction, tab_id } => match tab_id {
849                Some(id) => Ok(vec![Action::MoveTabByTabId {
850                    id: id as u64,
851                    direction,
852                }]),
853                None => Ok(vec![Action::MoveTab { direction }]),
854            },
855            CliAction::Clear { pane_id } => match pane_id {
856                Some(pane_id_str) => {
857                    let pane_id = PaneId::from_str(&pane_id_str)
858                        .map_err(|_| format!(
859                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
860                        ))?;
861                    Ok(vec![Action::ClearScreenByPaneId { pane_id }])
862                },
863                None => Ok(vec![Action::ClearScreen]),
864            },
865            CliAction::DumpScreen {
866                path,
867                full,
868                pane_id,
869                ansi,
870            } => match pane_id {
871                Some(pane_id_str) => {
872                    let parsed_pane_id = PaneId::from_str(&pane_id_str);
873                    match parsed_pane_id {
874                        Ok(parsed_pane_id) => {
875                            Ok(vec![Action::DumpScreen {
876                                file_path: path.map(|p| p.as_os_str().to_string_lossy().into()),
877                                include_scrollback: full,
878                                pane_id: Some(parsed_pane_id),
879                                ansi,
880                            }])
881                        },
882                        Err(_e) => {
883                            Err(format!(
884                                "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
885                                pane_id_str
886                            ))
887                        }
888                    }
889                },
890                None => Ok(vec![Action::DumpScreen {
891                    file_path: path.map(|p| p.as_os_str().to_string_lossy().into()),
892                    include_scrollback: full,
893                    pane_id: None,
894                    ansi,
895                }]),
896            },
897            CliAction::DumpLayout => Ok(vec![Action::DumpLayout]),
898            CliAction::SaveSession => Ok(vec![Action::SaveSession]),
899            CliAction::EditScrollback { pane_id, ansi } => match pane_id {
900                Some(pane_id_str) => {
901                    let pane_id = PaneId::from_str(&pane_id_str)
902                        .map_err(|_| format!(
903                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
904                        ))?;
905                    Ok(vec![Action::EditScrollbackByPaneId { pane_id, ansi }])
906                },
907                None => Ok(vec![Action::EditScrollback { ansi }]),
908            },
909            CliAction::ScrollUp { pane_id } => match pane_id {
910                Some(pane_id_str) => {
911                    let pane_id = PaneId::from_str(&pane_id_str)
912                        .map_err(|_| format!(
913                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
914                        ))?;
915                    Ok(vec![Action::ScrollUpByPaneId { pane_id }])
916                },
917                None => Ok(vec![Action::ScrollUp]),
918            },
919            CliAction::ScrollDown { pane_id } => match pane_id {
920                Some(pane_id_str) => {
921                    let pane_id = PaneId::from_str(&pane_id_str)
922                        .map_err(|_| format!(
923                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
924                        ))?;
925                    Ok(vec![Action::ScrollDownByPaneId { pane_id }])
926                },
927                None => Ok(vec![Action::ScrollDown]),
928            },
929            CliAction::ScrollToBottom { pane_id } => match pane_id {
930                Some(pane_id_str) => {
931                    let pane_id = PaneId::from_str(&pane_id_str)
932                        .map_err(|_| format!(
933                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
934                        ))?;
935                    Ok(vec![Action::ScrollToBottomByPaneId { pane_id }])
936                },
937                None => Ok(vec![Action::ScrollToBottom]),
938            },
939            CliAction::ScrollToTop { pane_id } => match pane_id {
940                Some(pane_id_str) => {
941                    let pane_id = PaneId::from_str(&pane_id_str)
942                        .map_err(|_| format!(
943                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
944                        ))?;
945                    Ok(vec![Action::ScrollToTopByPaneId { pane_id }])
946                },
947                None => Ok(vec![Action::ScrollToTop]),
948            },
949            CliAction::PageScrollUp { pane_id } => match pane_id {
950                Some(pane_id_str) => {
951                    let pane_id = PaneId::from_str(&pane_id_str)
952                        .map_err(|_| format!(
953                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
954                        ))?;
955                    Ok(vec![Action::PageScrollUpByPaneId { pane_id }])
956                },
957                None => Ok(vec![Action::PageScrollUp]),
958            },
959            CliAction::PageScrollDown { pane_id } => match pane_id {
960                Some(pane_id_str) => {
961                    let pane_id = PaneId::from_str(&pane_id_str)
962                        .map_err(|_| format!(
963                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
964                        ))?;
965                    Ok(vec![Action::PageScrollDownByPaneId { pane_id }])
966                },
967                None => Ok(vec![Action::PageScrollDown]),
968            },
969            CliAction::HalfPageScrollUp { pane_id } => match pane_id {
970                Some(pane_id_str) => {
971                    let pane_id = PaneId::from_str(&pane_id_str)
972                        .map_err(|_| format!(
973                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
974                        ))?;
975                    Ok(vec![Action::HalfPageScrollUpByPaneId { pane_id }])
976                },
977                None => Ok(vec![Action::HalfPageScrollUp]),
978            },
979            CliAction::HalfPageScrollDown { pane_id } => match pane_id {
980                Some(pane_id_str) => {
981                    let pane_id = PaneId::from_str(&pane_id_str)
982                        .map_err(|_| format!(
983                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
984                        ))?;
985                    Ok(vec![Action::HalfPageScrollDownByPaneId { pane_id }])
986                },
987                None => Ok(vec![Action::HalfPageScrollDown]),
988            },
989            CliAction::ToggleFullscreen { pane_id } => match pane_id {
990                Some(pane_id_str) => {
991                    let pane_id = PaneId::from_str(&pane_id_str)
992                        .map_err(|_| format!(
993                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
994                        ))?;
995                    Ok(vec![Action::ToggleFocusFullscreenByPaneId { pane_id }])
996                },
997                None => Ok(vec![Action::ToggleFocusFullscreen]),
998            },
999            CliAction::TogglePaneFrames => Ok(vec![Action::TogglePaneFrames]),
1000            CliAction::ToggleActiveSyncTab { tab_id } => match tab_id {
1001                Some(id) => Ok(vec![Action::ToggleActiveSyncTabByTabId { id: id as u64 }]),
1002                None => Ok(vec![Action::ToggleActiveSyncTab]),
1003            },
1004            CliAction::NewPane {
1005                direction,
1006                command,
1007                plugin,
1008                cwd,
1009                floating,
1010                in_place,
1011                close_replaced_pane,
1012                name,
1013                close_on_exit,
1014                start_suspended,
1015                configuration,
1016                skip_plugin_cache,
1017                x,
1018                y,
1019                width,
1020                height,
1021                pinned,
1022                stacked,
1023                blocking,
1024                block_until_exit_success,
1025                block_until_exit_failure,
1026                block_until_exit,
1027                unblock_condition,
1028                near_current_pane,
1029                borderless,
1030                tab_id,
1031            } => {
1032                let current_dir = get_current_dir();
1033                // cwd should only be specified in a plugin alias if it was explicitly given to us,
1034                // otherwise the current_dir might override a cwd defined in the alias itself
1035                let alias_cwd = cwd.clone().map(|cwd| current_dir.join(cwd));
1036                let cwd = cwd
1037                    .map(|cwd| current_dir.join(cwd))
1038                    .or_else(|| Some(current_dir.clone()));
1039                let unblock_condition = unblock_condition.or_else(|| {
1040                    if block_until_exit_success {
1041                        Some(UnblockCondition::OnExitSuccess)
1042                    } else if block_until_exit_failure {
1043                        Some(UnblockCondition::OnExitFailure)
1044                    } else if block_until_exit {
1045                        Some(UnblockCondition::OnAnyExit)
1046                    } else {
1047                        None
1048                    }
1049                });
1050                if blocking || unblock_condition.is_some() {
1051                    // For blocking panes, we don't support plugins
1052                    if plugin.is_some() {
1053                        return Err("Blocking panes do not support plugin variants".to_string());
1054                    }
1055
1056                    let command = if !command.is_empty() {
1057                        let mut command = command.clone();
1058                        let (command, args) = (PathBuf::from(command.remove(0)), command);
1059                        let hold_on_start = start_suspended;
1060                        let hold_on_close = !close_on_exit;
1061                        Some(RunCommandAction {
1062                            command,
1063                            args,
1064                            cwd,
1065                            direction,
1066                            hold_on_close,
1067                            hold_on_start,
1068                            ..Default::default()
1069                        })
1070                    } else {
1071                        None
1072                    };
1073
1074                    let placement = if floating {
1075                        NewPanePlacement::Floating(FloatingPaneCoordinates::new(
1076                            x, y, width, height, pinned, borderless,
1077                        ))
1078                    } else if in_place {
1079                        NewPanePlacement::InPlace {
1080                            pane_id_to_replace: None,
1081                            close_replaced_pane,
1082                            borderless,
1083                        }
1084                    } else if stacked {
1085                        NewPanePlacement::Stacked {
1086                            pane_id_to_stack_under: None,
1087                            borderless,
1088                        }
1089                    } else {
1090                        NewPanePlacement::Tiled {
1091                            direction,
1092                            borderless,
1093                        }
1094                    };
1095
1096                    Ok(vec![Action::NewBlockingPane {
1097                        placement,
1098                        pane_name: name,
1099                        command,
1100                        unblock_condition,
1101                        near_current_pane,
1102                        tab_id,
1103                    }])
1104                } else if let Some(plugin) = plugin {
1105                    let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) {
1106                        Ok(location) => {
1107                            let user_configuration = configuration.unwrap_or_default();
1108                            RunPluginOrAlias::RunPlugin(RunPlugin {
1109                                _allow_exec_host_cmd: false,
1110                                location,
1111                                configuration: user_configuration,
1112                                initial_cwd: cwd.clone(),
1113                            })
1114                        },
1115                        Err(_) => {
1116                            let mut plugin_alias = PluginAlias::new(
1117                                &plugin,
1118                                &configuration.map(|c| c.inner().clone()),
1119                                alias_cwd,
1120                            );
1121                            plugin_alias.set_caller_cwd_if_not_set(Some(current_dir));
1122                            RunPluginOrAlias::Alias(plugin_alias)
1123                        },
1124                    };
1125                    if floating {
1126                        Ok(vec![Action::NewFloatingPluginPane {
1127                            plugin,
1128                            pane_name: name,
1129                            skip_cache: skip_plugin_cache,
1130                            cwd,
1131                            coordinates: FloatingPaneCoordinates::new(
1132                                x, y, width, height, pinned, borderless,
1133                            ),
1134                            tab_id,
1135                        }])
1136                    } else if in_place {
1137                        Ok(vec![Action::NewInPlacePluginPane {
1138                            plugin,
1139                            pane_name: name,
1140                            skip_cache: skip_plugin_cache,
1141                            close_replaced_pane,
1142                            tab_id,
1143                        }])
1144                    } else {
1145                        // it is intentional that a new tiled plugin pane cannot include a
1146                        // direction
1147                        // this is because the cli client opening a tiled plugin pane is a
1148                        // different client than the one opening the pane, and this can potentially
1149                        // create very confusing races if the client changes focus while the plugin
1150                        // is being loaded
1151                        // this is not the case with terminal panes for historical reasons of
1152                        // backwards compatibility to a time before we had auto layouts
1153                        Ok(vec![Action::NewTiledPluginPane {
1154                            plugin,
1155                            pane_name: name,
1156                            skip_cache: skip_plugin_cache,
1157                            cwd,
1158                            tab_id,
1159                        }])
1160                    }
1161                } else if !command.is_empty() {
1162                    let mut command = command.clone();
1163                    let (command, args) = (PathBuf::from(command.remove(0)), command);
1164                    let hold_on_start = start_suspended;
1165                    let hold_on_close = !close_on_exit;
1166                    let run_command_action = RunCommandAction {
1167                        command,
1168                        args,
1169                        cwd,
1170                        direction,
1171                        hold_on_close,
1172                        hold_on_start,
1173                        ..Default::default()
1174                    };
1175                    if floating {
1176                        Ok(vec![Action::NewFloatingPane {
1177                            command: Some(run_command_action),
1178                            pane_name: name,
1179                            coordinates: FloatingPaneCoordinates::new(
1180                                x, y, width, height, pinned, borderless,
1181                            ),
1182                            near_current_pane,
1183                            tab_id,
1184                        }])
1185                    } else if in_place {
1186                        Ok(vec![Action::NewInPlacePane {
1187                            command: Some(run_command_action),
1188                            pane_name: name,
1189                            near_current_pane,
1190                            pane_id_to_replace: None, // TODO: support this
1191                            close_replaced_pane,
1192                            tab_id,
1193                        }])
1194                    } else if stacked {
1195                        Ok(vec![Action::NewStackedPane {
1196                            command: Some(run_command_action),
1197                            pane_name: name,
1198                            near_current_pane,
1199                            tab_id,
1200                        }])
1201                    } else {
1202                        Ok(vec![Action::NewTiledPane {
1203                            direction,
1204                            command: Some(run_command_action),
1205                            pane_name: name,
1206                            near_current_pane,
1207                            borderless,
1208                            tab_id,
1209                        }])
1210                    }
1211                } else {
1212                    if floating {
1213                        Ok(vec![Action::NewFloatingPane {
1214                            command: None,
1215                            pane_name: name,
1216                            coordinates: FloatingPaneCoordinates::new(
1217                                x, y, width, height, pinned, borderless,
1218                            ),
1219                            near_current_pane,
1220                            tab_id,
1221                        }])
1222                    } else if in_place {
1223                        Ok(vec![Action::NewInPlacePane {
1224                            command: None,
1225                            pane_name: name,
1226                            near_current_pane,
1227                            pane_id_to_replace: None, // TODO: support this
1228                            close_replaced_pane,
1229                            tab_id,
1230                        }])
1231                    } else if stacked {
1232                        Ok(vec![Action::NewStackedPane {
1233                            command: None,
1234                            pane_name: name,
1235                            near_current_pane,
1236                            tab_id,
1237                        }])
1238                    } else {
1239                        Ok(vec![Action::NewTiledPane {
1240                            direction,
1241                            command: None,
1242                            pane_name: name,
1243                            near_current_pane,
1244                            borderless,
1245                            tab_id,
1246                        }])
1247                    }
1248                }
1249            },
1250            CliAction::Edit {
1251                direction,
1252                file,
1253                line_number,
1254                floating,
1255                in_place,
1256                close_replaced_pane,
1257                cwd,
1258                x,
1259                y,
1260                width,
1261                height,
1262                pinned,
1263                near_current_pane,
1264                borderless,
1265                tab_id,
1266            } => {
1267                let mut file = file;
1268                let current_dir = get_current_dir();
1269                let cwd = cwd
1270                    .map(|cwd| current_dir.join(cwd))
1271                    .or_else(|| Some(current_dir));
1272                if file.is_relative() {
1273                    if let Some(cwd) = cwd.as_ref() {
1274                        file = cwd.join(file);
1275                    }
1276                }
1277                let start_suppressed = false;
1278                Ok(vec![Action::EditFile {
1279                    payload: OpenFilePayload::new(file, line_number, cwd),
1280                    direction,
1281                    floating,
1282                    in_place,
1283                    close_replaced_pane,
1284                    start_suppressed,
1285                    coordinates: FloatingPaneCoordinates::new(
1286                        x, y, width, height, pinned, borderless,
1287                    ),
1288                    near_current_pane,
1289                    tab_id,
1290                }])
1291            },
1292            CliAction::SwitchMode { input_mode } => Ok(vec![Action::SwitchToMode { input_mode }]),
1293            CliAction::TogglePaneEmbedOrFloating { pane_id } => match pane_id {
1294                Some(pane_id_str) => {
1295                    let pane_id = PaneId::from_str(&pane_id_str)
1296                        .map_err(|_| format!(
1297                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
1298                        ))?;
1299                    Ok(vec![Action::TogglePaneEmbedOrFloatingByPaneId { pane_id }])
1300                },
1301                None => Ok(vec![Action::TogglePaneEmbedOrFloating]),
1302            },
1303            CliAction::ToggleFloatingPanes { tab_id } => match tab_id {
1304                Some(id) => Ok(vec![Action::ToggleFloatingPanesByTabId { id: id as u64 }]),
1305                None => Ok(vec![Action::ToggleFloatingPanes]),
1306            },
1307            CliAction::ShowFloatingPanes { tab_id } => {
1308                Ok(vec![Action::ShowFloatingPanes { tab_id }])
1309            },
1310            CliAction::HideFloatingPanes { tab_id } => {
1311                Ok(vec![Action::HideFloatingPanes { tab_id }])
1312            },
1313            CliAction::AreFloatingPanesVisible { tab_id } => {
1314                Ok(vec![Action::AreFloatingPanesVisible { tab_id }])
1315            },
1316            CliAction::ClosePane { pane_id } => match pane_id {
1317                Some(pane_id_str) => {
1318                    let pane_id = PaneId::from_str(&pane_id_str)
1319                        .map_err(|_| format!(
1320                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
1321                        ))?;
1322                    Ok(vec![Action::CloseFocusByPaneId { pane_id }])
1323                },
1324                None => Ok(vec![Action::CloseFocus]),
1325            },
1326            CliAction::RenamePane { name, pane_id } => {
1327                let pane_id = match pane_id {
1328                    Some(pane_id_str) => Some(
1329                        PaneId::from_str(&pane_id_str).map_err(|_| format!(
1330                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
1331                        ))?,
1332                    ),
1333                    None => None,
1334                };
1335                Ok(vec![Action::RenamePaneByPaneId {
1336                    pane_id,
1337                    name: name.as_bytes().to_vec(),
1338                }])
1339            },
1340            CliAction::UndoRenamePane { pane_id } => match pane_id {
1341                Some(pane_id_str) => {
1342                    let pane_id = PaneId::from_str(&pane_id_str)
1343                        .map_err(|_| format!(
1344                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
1345                        ))?;
1346                    Ok(vec![Action::UndoRenamePaneByPaneId { pane_id }])
1347                },
1348                None => Ok(vec![Action::UndoRenamePane]),
1349            },
1350            CliAction::GoToNextTab => Ok(vec![Action::GoToNextTab]),
1351            CliAction::GoToPreviousTab => Ok(vec![Action::GoToPreviousTab]),
1352            CliAction::CloseTab { tab_id } => match tab_id {
1353                Some(id) => Ok(vec![Action::CloseTabById { id: id as u64 }]),
1354                None => Ok(vec![Action::CloseTab]),
1355            },
1356            CliAction::GoToTab { index } => Ok(vec![Action::GoToTab { index }]),
1357            CliAction::GoToTabName { name, create } => {
1358                Ok(vec![Action::GoToTabName { name, create }])
1359            },
1360            CliAction::RenameTab { name, tab_id } => match tab_id {
1361                Some(id) => Ok(vec![Action::RenameTabById {
1362                    id: id as u64,
1363                    name,
1364                }]),
1365                None => Ok(vec![
1366                    Action::TabNameInput { input: vec![0] },
1367                    Action::TabNameInput {
1368                        input: name.as_bytes().to_vec(),
1369                    },
1370                ]),
1371            },
1372            CliAction::UndoRenameTab { tab_id } => match tab_id {
1373                Some(id) => Ok(vec![Action::UndoRenameTabByTabId { id: id as u64 }]),
1374                None => Ok(vec![Action::UndoRenameTab]),
1375            },
1376            CliAction::GoToTabById { id } => Ok(vec![Action::GoToTabById { id }]),
1377            CliAction::CloseTabById { id } => Ok(vec![Action::CloseTabById { id }]),
1378            CliAction::RenameTabById { id, name } => Ok(vec![Action::RenameTabById { id, name }]),
1379            CliAction::NewTab {
1380                name,
1381                layout,
1382                layout_string,
1383                layout_dir,
1384                cwd,
1385                initial_command,
1386                initial_plugin,
1387                close_on_exit,
1388                start_suspended,
1389                block_until_exit_success,
1390                block_until_exit_failure,
1391                block_until_exit,
1392            } => {
1393                let current_dir = get_current_dir();
1394                let cwd = cwd
1395                    .map(|cwd| current_dir.join(cwd))
1396                    .or_else(|| Some(current_dir.clone()));
1397
1398                // Map CLI flags to UnblockCondition
1399                let first_pane_unblock_condition = if block_until_exit_success {
1400                    Some(UnblockCondition::OnExitSuccess)
1401                } else if block_until_exit_failure {
1402                    Some(UnblockCondition::OnExitFailure)
1403                } else if block_until_exit {
1404                    Some(UnblockCondition::OnAnyExit)
1405                } else {
1406                    None
1407                };
1408
1409                // Parse initial_panes from initial_command or initial_plugin
1410                let initial_panes = if let Some(plugin_url) = initial_plugin {
1411                    let plugin = match RunPluginLocation::parse(&plugin_url, cwd.clone()) {
1412                        Ok(location) => RunPluginOrAlias::RunPlugin(RunPlugin {
1413                            _allow_exec_host_cmd: false,
1414                            location,
1415                            configuration: Default::default(),
1416                            initial_cwd: cwd.clone(),
1417                        }),
1418                        Err(_) => {
1419                            let mut plugin_alias =
1420                                PluginAlias::new(&plugin_url, &None, cwd.clone());
1421                            plugin_alias.set_caller_cwd_if_not_set(Some(current_dir.clone()));
1422                            RunPluginOrAlias::Alias(plugin_alias)
1423                        },
1424                    };
1425                    Some(vec![CommandOrPlugin::Plugin(plugin)])
1426                } else if !initial_command.is_empty() {
1427                    let mut command: Vec<String> = initial_command.clone();
1428                    let (command, args) = (
1429                        PathBuf::from(command.remove(0)),
1430                        command.into_iter().collect(),
1431                    );
1432                    let hold_on_close = !close_on_exit;
1433                    let hold_on_start = start_suspended;
1434                    let run_command_action = RunCommandAction {
1435                        command,
1436                        args,
1437                        cwd: cwd.clone(),
1438                        direction: None,
1439                        hold_on_close,
1440                        hold_on_start,
1441                        ..Default::default()
1442                    };
1443                    Some(vec![CommandOrPlugin::Command(run_command_action)])
1444                } else {
1445                    None
1446                };
1447                if let Some(raw_layout) = layout_string {
1448                    let layout_source_name = "layout-string".to_owned();
1449                    let path_to_raw_layout = layout_source_name.clone();
1450                    let swap_layouts: Option<(String, String)> = None;
1451                    let should_start_layout_commands_suspended = false;
1452                    let raw_layout_for_error = raw_layout.clone();
1453                    let mut layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| {
1454                        let stringified_error = match e {
1455                            ConfigError::KdlError(kdl_error) => {
1456                                let error = kdl_error.add_src(layout_source_name.clone(), raw_layout_for_error);
1457                                let report: Report = error.into();
1458                                format!("{:?}", report)
1459                            }
1460                            ConfigError::KdlDeserializationError(kdl_error) => {
1461                                let error_message = match kdl_error.kind {
1462                                    kdl::KdlErrorKind::Context("valid node terminator") => {
1463                                        format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
1464                                        "- Missing `;` after a node name, eg. { node; another_node; }",
1465                                        "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
1466                                        "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
1467                                        "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
1468                                    },
1469                                    _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")),
1470                                };
1471                                let kdl_error = KdlError {
1472                                    error_message,
1473                                    src: Some(NamedSource::new(layout_source_name.clone(), raw_layout_for_error)),
1474                                    offset: Some(kdl_error.span.offset()),
1475                                    len: Some(kdl_error.span.len()),
1476                                    help_message: None,
1477                                };
1478                                let report: Report = kdl_error.into();
1479                                format!("{:?}", report)
1480                            },
1481                            e => format!("{}", e)
1482                        };
1483                        stringified_error
1484                    })?;
1485                    if should_start_layout_commands_suspended {
1486                        layout.recursively_add_start_suspended_including_template(Some(true));
1487                    }
1488                    let mut tabs = layout.tabs();
1489                    if !tabs.is_empty() {
1490                        let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
1491                        let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
1492                        let mut new_tab_actions = vec![];
1493                        let mut has_focused_tab = tabs
1494                            .iter()
1495                            .any(|(_, layout, _)| layout.focus.unwrap_or(false));
1496                        for (tab_name, layout, floating_panes_layout) in tabs.drain(..) {
1497                            let name = tab_name.or_else(|| name.clone());
1498                            let should_change_focus_to_new_tab =
1499                                layout.focus.unwrap_or_else(|| {
1500                                    if !has_focused_tab {
1501                                        has_focused_tab = true;
1502                                        true
1503                                    } else {
1504                                        false
1505                                    }
1506                                });
1507                            new_tab_actions.push(Action::NewTab {
1508                                tiled_layout: Some(layout),
1509                                floating_layouts: floating_panes_layout,
1510                                swap_tiled_layouts: swap_tiled_layouts.clone(),
1511                                swap_floating_layouts: swap_floating_layouts.clone(),
1512                                tab_name: name,
1513                                should_change_focus_to_new_tab,
1514                                cwd: None,
1515                                initial_panes: initial_panes.clone(),
1516                                first_pane_unblock_condition,
1517                            });
1518                        }
1519                        Ok(new_tab_actions)
1520                    } else {
1521                        let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
1522                        let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
1523                        let (layout, floating_panes_layout) = layout.new_tab();
1524                        let should_change_focus_to_new_tab = true;
1525                        Ok(vec![Action::NewTab {
1526                            tiled_layout: Some(layout),
1527                            floating_layouts: floating_panes_layout,
1528                            swap_tiled_layouts,
1529                            swap_floating_layouts,
1530                            tab_name: name,
1531                            should_change_focus_to_new_tab,
1532                            cwd: None,
1533                            initial_panes,
1534                            first_pane_unblock_condition,
1535                        }])
1536                    }
1537                } else if let Some(layout_path) = layout {
1538                    let layout_dir = layout_dir
1539                        .or_else(|| config.and_then(|c| c.options.layout_dir))
1540                        .or_else(|| get_layout_dir(find_default_config_dir()));
1541
1542                    let mut should_start_layout_commands_suspended = false;
1543                    let layout_source_name;
1544                    let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(layout_url) =
1545                        layout_path.to_str().and_then(|l| {
1546                            if l.starts_with("http://") || l.starts_with("https://") {
1547                                Some(l)
1548                            } else {
1549                                None
1550                            }
1551                        }) {
1552                        should_start_layout_commands_suspended = true;
1553                        layout_source_name = layout_url.to_owned();
1554                        (
1555                            layout_url.to_owned(),
1556                            Layout::stringified_from_url(layout_url)
1557                                .map_err(|e| format!("Failed to load layout: {}", e))?,
1558                            None,
1559                        )
1560                    } else {
1561                        layout_source_name = layout_path
1562                            .as_path()
1563                            .as_os_str()
1564                            .to_string_lossy()
1565                            .to_string();
1566                        Layout::stringified_from_path_or_default(Some(&layout_path), layout_dir)
1567                            .map_err(|e| format!("Failed to load layout: {}", e))?
1568                    };
1569                    let mut layout = Layout::from_str(&raw_layout, path_to_raw_layout, swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())), cwd).map_err(|e| {
1570                        let stringified_error = match e {
1571                            ConfigError::KdlError(kdl_error) => {
1572                                let error = kdl_error.add_src(layout_source_name.clone(), String::from(raw_layout));
1573                                let report: Report = error.into();
1574                                format!("{:?}", report)
1575                            }
1576                            ConfigError::KdlDeserializationError(kdl_error) => {
1577                                let error_message = match kdl_error.kind {
1578                                    kdl::KdlErrorKind::Context("valid node terminator") => {
1579                                        format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
1580                                        "- Missing `;` after a node name, eg. { node; another_node; }",
1581                                        "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
1582                                        "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
1583                                        "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
1584                                    },
1585                                    _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")),
1586                                };
1587                                let kdl_error = KdlError {
1588                                    error_message,
1589                                    src: Some(NamedSource::new(layout_source_name.clone(), String::from(raw_layout))),
1590                                    offset: Some(kdl_error.span.offset()),
1591                                    len: Some(kdl_error.span.len()),
1592                                    help_message: None,
1593                                };
1594                                let report: Report = kdl_error.into();
1595                                format!("{:?}", report)
1596                            },
1597                            e => format!("{}", e)
1598                        };
1599                        stringified_error
1600                    })?;
1601                    if should_start_layout_commands_suspended {
1602                        layout.recursively_add_start_suspended_including_template(Some(true));
1603                    }
1604                    let mut tabs = layout.tabs();
1605                    if !tabs.is_empty() {
1606                        let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
1607                        let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
1608                        let mut new_tab_actions = vec![];
1609                        let mut has_focused_tab = tabs
1610                            .iter()
1611                            .any(|(_, layout, _)| layout.focus.unwrap_or(false));
1612                        for (tab_name, layout, floating_panes_layout) in tabs.drain(..) {
1613                            let name = tab_name.or_else(|| name.clone());
1614                            let should_change_focus_to_new_tab =
1615                                layout.focus.unwrap_or_else(|| {
1616                                    if !has_focused_tab {
1617                                        has_focused_tab = true;
1618                                        true
1619                                    } else {
1620                                        false
1621                                    }
1622                                });
1623                            new_tab_actions.push(Action::NewTab {
1624                                tiled_layout: Some(layout),
1625                                floating_layouts: floating_panes_layout,
1626                                swap_tiled_layouts: swap_tiled_layouts.clone(),
1627                                swap_floating_layouts: swap_floating_layouts.clone(),
1628                                tab_name: name,
1629                                should_change_focus_to_new_tab,
1630                                cwd: None, // the cwd is done through the layout
1631                                initial_panes: initial_panes.clone(),
1632                                first_pane_unblock_condition,
1633                            });
1634                        }
1635                        Ok(new_tab_actions)
1636                    } else {
1637                        let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
1638                        let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
1639                        let (layout, floating_panes_layout) = layout.new_tab();
1640                        let should_change_focus_to_new_tab = true;
1641                        Ok(vec![Action::NewTab {
1642                            tiled_layout: Some(layout),
1643                            floating_layouts: floating_panes_layout,
1644                            swap_tiled_layouts,
1645                            swap_floating_layouts,
1646                            tab_name: name,
1647                            should_change_focus_to_new_tab,
1648                            cwd: None, // the cwd is done through the layout
1649                            initial_panes,
1650                            first_pane_unblock_condition,
1651                        }])
1652                    }
1653                } else {
1654                    let should_change_focus_to_new_tab = true;
1655                    Ok(vec![Action::NewTab {
1656                        tiled_layout: None,
1657                        floating_layouts: vec![],
1658                        swap_tiled_layouts: None,
1659                        swap_floating_layouts: None,
1660                        tab_name: name,
1661                        should_change_focus_to_new_tab,
1662                        cwd,
1663                        initial_panes,
1664                        first_pane_unblock_condition,
1665                    }])
1666                }
1667            },
1668            CliAction::PreviousSwapLayout { tab_id } => match tab_id {
1669                Some(id) => Ok(vec![Action::PreviousSwapLayoutByTabId { id: id as u64 }]),
1670                None => Ok(vec![Action::PreviousSwapLayout]),
1671            },
1672            CliAction::NextSwapLayout { tab_id } => match tab_id {
1673                Some(id) => Ok(vec![Action::NextSwapLayoutByTabId { id: id as u64 }]),
1674                None => Ok(vec![Action::NextSwapLayout]),
1675            },
1676            CliAction::OverrideLayout {
1677                layout,
1678                layout_string,
1679                layout_dir,
1680                retain_existing_terminal_panes,
1681                retain_existing_plugin_panes,
1682                apply_only_to_active_tab,
1683            } => {
1684                // Determine layout_dir: CLI arg > config > default
1685                let layout_dir = layout_dir
1686                    .or_else(|| config.and_then(|c| c.options.layout_dir))
1687                    .or_else(|| get_layout_dir(find_default_config_dir()));
1688
1689                // Load layout from string, URL, or file path
1690                let layout_source_name;
1691                let (path_to_raw_layout, raw_layout, swap_layouts) = if let Some(raw) =
1692                    layout_string
1693                {
1694                    layout_source_name = "layout-string".to_owned();
1695                    (layout_source_name.clone(), raw, None)
1696                } else if let Some(layout_path) = &layout {
1697                    if let Some(layout_url) = layout_path.to_str().and_then(|l| {
1698                        if l.starts_with("http://") || l.starts_with("https://") {
1699                            Some(l)
1700                        } else {
1701                            None
1702                        }
1703                    }) {
1704                        layout_source_name = layout_url.to_owned();
1705                        (
1706                            layout_url.to_owned(),
1707                            Layout::stringified_from_url(layout_url)
1708                                .map_err(|e| format!("Failed to load layout from URL: {}", e))?,
1709                            None,
1710                        )
1711                    } else {
1712                        layout_source_name = layout_path
1713                            .as_path()
1714                            .as_os_str()
1715                            .to_string_lossy()
1716                            .to_string();
1717                        Layout::stringified_from_path_or_default(Some(layout_path), layout_dir)
1718                            .map_err(|e| format!("Failed to load layout: {}", e))?
1719                    }
1720                } else {
1721                    return Err("Either layout or layout-string must be provided".to_string());
1722                };
1723
1724                // Parse KDL layout
1725                let layout = Layout::from_str(
1726                    &raw_layout,
1727                    path_to_raw_layout,
1728                    swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())),
1729                    None, // cwd
1730                )
1731                .map_err(|e| {
1732                    let stringified_error = match e {
1733                        ConfigError::KdlError(kdl_error) => {
1734                            let error = kdl_error
1735                                .add_src(layout_source_name.clone(), String::from(raw_layout));
1736                            let report: Report = error.into();
1737                            format!("{:?}", report)
1738                        },
1739                        ConfigError::KdlDeserializationError(kdl_error) => {
1740                            let error_message = kdl_error.to_string();
1741                            format!("Failed to deserialize KDL layout: {}", error_message)
1742                        },
1743                        e => format!("{}", e),
1744                    };
1745                    stringified_error
1746                })?;
1747
1748                // Convert all tabs to Vec<TabLayoutInfo>
1749                let tabs: Vec<TabLayoutInfo> = layout
1750                    .tabs
1751                    .iter()
1752                    .enumerate()
1753                    .map(|(index, (tab_name, tiled, floating))| TabLayoutInfo {
1754                        tab_index: index,
1755                        tab_name: tab_name.clone(),
1756                        tiled_layout: tiled.clone(),
1757                        floating_layouts: floating.clone(),
1758                        swap_tiled_layouts: Some(layout.swap_tiled_layouts.clone()),
1759                        swap_floating_layouts: Some(layout.swap_floating_layouts.clone()),
1760                    })
1761                    .collect();
1762
1763                // If no tabs, create default tab
1764                let tabs = if tabs.is_empty() {
1765                    let (tiled, floating) = layout.new_tab();
1766                    vec![TabLayoutInfo {
1767                        tab_index: 0,
1768                        tab_name: None,
1769                        tiled_layout: tiled,
1770                        floating_layouts: floating,
1771                        swap_tiled_layouts: Some(layout.swap_tiled_layouts),
1772                        swap_floating_layouts: Some(layout.swap_floating_layouts),
1773                    }]
1774                } else {
1775                    tabs
1776                };
1777
1778                Ok(vec![Action::OverrideLayout {
1779                    tabs,
1780                    retain_existing_terminal_panes,
1781                    retain_existing_plugin_panes,
1782                    apply_only_to_active_tab,
1783                }])
1784            },
1785            CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
1786            CliAction::StartOrReloadPlugin { url, configuration } => {
1787                let current_dir = get_current_dir();
1788                let run_plugin_or_alias = RunPluginOrAlias::from_url(
1789                    &url,
1790                    &configuration.map(|c| c.inner().clone()),
1791                    None,
1792                    Some(current_dir),
1793                )?;
1794                Ok(vec![Action::StartOrReloadPlugin {
1795                    plugin: run_plugin_or_alias,
1796                }])
1797            },
1798            CliAction::LaunchOrFocusPlugin {
1799                url,
1800                floating,
1801                in_place,
1802                close_replaced_pane,
1803                move_to_focused_tab,
1804                configuration,
1805                skip_plugin_cache,
1806                tab_id,
1807            } => {
1808                let current_dir = get_current_dir();
1809                let run_plugin_or_alias = RunPluginOrAlias::from_url(
1810                    url.as_str(),
1811                    &configuration.map(|c| c.inner().clone()),
1812                    None,
1813                    Some(current_dir),
1814                )?;
1815                Ok(vec![Action::LaunchOrFocusPlugin {
1816                    plugin: run_plugin_or_alias,
1817                    should_float: floating,
1818                    move_to_focused_tab,
1819                    should_open_in_place: in_place,
1820                    close_replaced_pane,
1821                    skip_cache: skip_plugin_cache,
1822                    tab_id,
1823                }])
1824            },
1825            CliAction::LaunchPlugin {
1826                url,
1827                floating,
1828                in_place,
1829                close_replaced_pane,
1830                configuration,
1831                skip_plugin_cache,
1832                tab_id,
1833            } => {
1834                let current_dir = get_current_dir();
1835                let run_plugin_or_alias = RunPluginOrAlias::from_url(
1836                    &url.as_str(),
1837                    &configuration.map(|c| c.inner().clone()),
1838                    None,
1839                    Some(current_dir.clone()),
1840                )?;
1841                Ok(vec![Action::LaunchPlugin {
1842                    plugin: run_plugin_or_alias,
1843                    should_float: floating,
1844                    should_open_in_place: in_place,
1845                    close_replaced_pane,
1846                    skip_cache: skip_plugin_cache,
1847                    cwd: Some(current_dir),
1848                    tab_id,
1849                }])
1850            },
1851            CliAction::RenameSession { name } => Ok(vec![Action::RenameSession { name }]),
1852            CliAction::Pipe {
1853                name,
1854                payload,
1855                args,
1856                plugin,
1857                plugin_configuration,
1858                force_launch_plugin,
1859                skip_plugin_cache,
1860                floating_plugin,
1861                in_place_plugin,
1862                plugin_cwd,
1863                plugin_title,
1864            } => {
1865                let current_dir = get_current_dir();
1866                let cwd = plugin_cwd
1867                    .map(|cwd| current_dir.join(cwd))
1868                    .or_else(|| Some(current_dir));
1869                let skip_cache = skip_plugin_cache;
1870                let pipe_id = Uuid::new_v4().to_string();
1871                Ok(vec![Action::CliPipe {
1872                    pipe_id,
1873                    name,
1874                    payload,
1875                    args: args.map(|a| a.inner().clone()), // TODO: no clone somehow
1876                    plugin,
1877                    configuration: plugin_configuration.map(|a| a.inner().clone()), // TODO: no clone
1878                    // somehow
1879                    launch_new: force_launch_plugin,
1880                    floating: floating_plugin,
1881                    in_place: in_place_plugin,
1882                    cwd,
1883                    pane_title: plugin_title,
1884                    skip_cache,
1885                }])
1886            },
1887            CliAction::ListClients => Ok(vec![Action::ListClients]),
1888            CliAction::ListPanes {
1889                tab,
1890                command,
1891                state,
1892                geometry,
1893                all,
1894                json,
1895            } => Ok(vec![Action::ListPanes {
1896                show_tab: tab,
1897                show_command: command,
1898                show_state: state,
1899                show_geometry: geometry,
1900                show_all: all,
1901                output_json: json,
1902            }]),
1903            CliAction::ListTabs {
1904                state,
1905                dimensions,
1906                panes,
1907                layout,
1908                all,
1909                json,
1910            } => Ok(vec![Action::ListTabs {
1911                show_state: state,
1912                show_dimensions: dimensions,
1913                show_panes: panes,
1914                show_layout: layout,
1915                show_all: all,
1916                output_json: json,
1917            }]),
1918            CliAction::CurrentTabInfo { json } => {
1919                Ok(vec![Action::CurrentTabInfo { output_json: json }])
1920            },
1921            CliAction::TogglePanePinned { pane_id } => match pane_id {
1922                Some(pane_id_str) => {
1923                    let pane_id = PaneId::from_str(&pane_id_str)
1924                        .map_err(|_| format!(
1925                            "Malformed pane id: {pane_id_str}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)"
1926                        ))?;
1927                    Ok(vec![Action::TogglePanePinnedByPaneId { pane_id }])
1928                },
1929                None => Ok(vec![Action::TogglePanePinned]),
1930            },
1931            CliAction::StackPanes { pane_ids } => {
1932                let mut malformed_ids = vec![];
1933                let pane_ids = pane_ids
1934                    .iter()
1935                    .filter_map(
1936                        |stringified_pane_id| match PaneId::from_str(stringified_pane_id) {
1937                            Ok(pane_id) => Some(pane_id),
1938                            Err(_e) => {
1939                                malformed_ids.push(stringified_pane_id.to_owned());
1940                                None
1941                            },
1942                        },
1943                    )
1944                    .collect();
1945                if !malformed_ids.is_empty() {
1946                    Err(
1947                        format!(
1948                            "Malformed pane ids: {}, expecting a space separated list of either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
1949                            malformed_ids.join(", ")
1950                        )
1951                    )
1952                } else {
1953                    Ok(vec![Action::StackPanes { pane_ids }])
1954                }
1955            },
1956            CliAction::ChangeFloatingPaneCoordinates {
1957                pane_id,
1958                x,
1959                y,
1960                width,
1961                height,
1962                pinned,
1963                borderless,
1964            } => {
1965                let Some(coordinates) =
1966                    FloatingPaneCoordinates::new(x, y, width, height, pinned, borderless)
1967                else {
1968                    return Err(format!("Failed to parse floating pane coordinates"));
1969                };
1970                let parsed_pane_id = PaneId::from_str(&pane_id);
1971                match parsed_pane_id {
1972                    Ok(parsed_pane_id) => {
1973                        Ok(vec![Action::ChangeFloatingPaneCoordinates {
1974                            pane_id: parsed_pane_id,
1975                            coordinates,
1976                        }])
1977                    },
1978                    Err(_e) => {
1979                        Err(format!(
1980                            "Malformed pane id: {}, expecting a space separated list of either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
1981                            pane_id
1982                        ))
1983                    }
1984                }
1985            },
1986            CliAction::TogglePaneBorderless { pane_id } => {
1987                let parsed_pane_id = PaneId::from_str(&pane_id);
1988                match parsed_pane_id {
1989                    Ok(parsed_pane_id) => {
1990                        Ok(vec![Action::TogglePaneBorderless {
1991                            pane_id: parsed_pane_id,
1992                        }])
1993                    },
1994                    Err(_e) => {
1995                        Err(format!(
1996                            "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
1997                            pane_id
1998                        ))
1999                    }
2000                }
2001            },
2002            CliAction::SetPaneBorderless {
2003                pane_id,
2004                borderless,
2005            } => {
2006                let parsed_pane_id = PaneId::from_str(&pane_id);
2007                match parsed_pane_id {
2008                    Ok(parsed_pane_id) => {
2009                        Ok(vec![Action::SetPaneBorderless {
2010                            pane_id: parsed_pane_id,
2011                            borderless,
2012                        }])
2013                    },
2014                    Err(_e) => {
2015                        Err(format!(
2016                            "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
2017                            pane_id
2018                        ))
2019                    }
2020                }
2021            },
2022            CliAction::SetPaneColor {
2023                pane_id,
2024                fg,
2025                bg,
2026                reset,
2027            } => {
2028                let pane_id_str = match pane_id {
2029                    Some(id) => id,
2030                    None => std::env::var("ZELLIJ_PANE_ID").map_err(|_| {
2031                        "No --pane-id provided and ZELLIJ_PANE_ID is not set".to_string()
2032                    })?,
2033                };
2034                let parsed_pane_id = PaneId::from_str(&pane_id_str);
2035                match parsed_pane_id {
2036                    Ok(parsed_pane_id) => {
2037                        let (fg, bg) = if reset {
2038                            (None, None)
2039                        } else {
2040                            (fg, bg)
2041                        };
2042                        Ok(vec![Action::SetPaneColor {
2043                            pane_id: parsed_pane_id,
2044                            fg,
2045                            bg,
2046                        }])
2047                    },
2048                    Err(_e) => Err(format!(
2049                        "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
2050                        pane_id_str
2051                    )),
2052                }
2053            },
2054            CliAction::Detach => Ok(vec![Action::Detach]),
2055            CliAction::SwitchSession {
2056                name,
2057                tab_position,
2058                pane_id,
2059                layout,
2060                layout_string,
2061                layout_dir,
2062                cwd,
2063            } => {
2064                let pane_id = match pane_id {
2065                    Some(stringified_pane_id) => match PaneId::from_str(&stringified_pane_id) {
2066                        Ok(PaneId::Terminal(id)) => Some((id, false)),
2067                        Ok(PaneId::Plugin(id)) => Some((id, true)),
2068                        Err(_e) => {
2069                            return Err(format!(
2070                                "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)",
2071                                stringified_pane_id
2072                            ));
2073                        },
2074                    },
2075                    None => None,
2076                };
2077
2078                let cwd = cwd.map(|cwd| {
2079                    let current_dir = get_current_dir();
2080                    current_dir.join(cwd)
2081                });
2082
2083                let layout_dir = layout_dir.map(|layout_dir| {
2084                    let current_dir = get_current_dir();
2085                    current_dir.join(layout_dir)
2086                });
2087
2088                let layout_info = if let Some(layout_string) = layout_string {
2089                    // validate the layout string before sending it to the target session
2090                    let layout_source_name = "layout-string".to_owned();
2091                    let raw_layout_for_error = layout_string.clone();
2092                    Layout::from_str(&layout_string, layout_source_name.clone(), None, None)
2093                        .map_err(|e| {
2094                            match e {
2095                                ConfigError::KdlError(kdl_error) => {
2096                                    let error = kdl_error.add_src(layout_source_name, raw_layout_for_error);
2097                                    let report: Report = error.into();
2098                                    format!("{:?}", report)
2099                                },
2100                                ConfigError::KdlDeserializationError(kdl_error) => {
2101                                    let error_message = match kdl_error.kind {
2102                                        kdl::KdlErrorKind::Context("valid node terminator") => {
2103                                            format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
2104                                            "- Missing `;` after a node name, eg. {{ node; another_node; }}",
2105                                            "- Missing quotations (\") around an argument node eg. {{ first_node \"argument_node\"; }}",
2106                                            "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
2107                                            "- Found an extraneous equal sign (=) between node child arguments and their values. eg. {{ argument=\"value\" }}")
2108                                        },
2109                                        _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")),
2110                                    };
2111                                    let kdl_error = KdlError {
2112                                        error_message,
2113                                        src: Some(NamedSource::new(layout_source_name, raw_layout_for_error)),
2114                                        offset: Some(kdl_error.span.offset()),
2115                                        len: Some(kdl_error.span.len()),
2116                                        help_message: None,
2117                                    };
2118                                    let report: Report = kdl_error.into();
2119                                    format!("{:?}", report)
2120                                },
2121                                e => format!("{}", e),
2122                            }
2123                        })?;
2124                    Some(LayoutInfo::Stringified(layout_string))
2125                } else if let Some(layout_path) = layout {
2126                    let layout_dir = layout_dir
2127                        .or_else(|| config.and_then(|c| c.options.layout_dir.clone()))
2128                        .or_else(|| get_layout_dir(find_default_config_dir()));
2129                    // validate the layout file before sending it to the target session
2130                    let layout_source_name = layout_path.display().to_string();
2131                    Layout::from_path_or_default_without_config(
2132                        Some(&layout_path),
2133                        layout_dir.clone(),
2134                    )
2135                    .map_err(|e| {
2136                        match e {
2137                            ConfigError::KdlError(kdl_error) => {
2138                                let report: Report = kdl_error.into();
2139                                format!("{:?}", report)
2140                            },
2141                            ConfigError::KdlDeserializationError(kdl_error) => {
2142                                let error_message = match kdl_error.kind {
2143                                    kdl::KdlErrorKind::Context("valid node terminator") => {
2144                                        format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
2145                                        "- Missing `;` after a node name, eg. {{ node; another_node; }}",
2146                                        "- Missing quotations (\") around an argument node eg. {{ first_node \"argument_node\"; }}",
2147                                        "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
2148                                        "- Found an extraneous equal sign (=) between node child arguments and their values. eg. {{ argument=\"value\" }}")
2149                                    },
2150                                    _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")),
2151                                };
2152                                let kdl_error = KdlError {
2153                                    error_message,
2154                                    src: Some(NamedSource::new(layout_source_name, String::new())),
2155                                    offset: Some(kdl_error.span.offset()),
2156                                    len: Some(kdl_error.span.len()),
2157                                    help_message: None,
2158                                };
2159                                let report: Report = kdl_error.into();
2160                                format!("{:?}", report)
2161                            },
2162                            e => format!("{}", e),
2163                        }
2164                    })?;
2165                    LayoutInfo::from_config(&layout_dir, &Some(layout_path))
2166                } else {
2167                    None
2168                };
2169
2170                Ok(vec![Action::SwitchSession {
2171                    name: name.clone(),
2172                    tab_position: tab_position.clone(),
2173                    pane_id,
2174                    layout: layout_info,
2175                    cwd,
2176                }])
2177            },
2178        }
2179    }
2180    pub fn populate_originating_plugin(&mut self, originating_plugin: OriginatingPlugin) {
2181        match self {
2182            Action::NewBlockingPane { command, .. }
2183            | Action::NewFloatingPane { command, .. }
2184            | Action::NewTiledPane { command, .. }
2185            | Action::NewInPlacePane { command, .. }
2186            | Action::NewStackedPane { command, .. } => {
2187                command
2188                    .as_mut()
2189                    .map(|c| c.populate_originating_plugin(originating_plugin));
2190            },
2191            Action::Run { command, .. } => {
2192                command.populate_originating_plugin(originating_plugin);
2193            },
2194            Action::EditFile { payload, .. } => {
2195                payload.originating_plugin = Some(originating_plugin);
2196            },
2197            Action::NewTab { initial_panes, .. } => {
2198                if let Some(initial_panes) = initial_panes.as_mut() {
2199                    for pane in initial_panes.iter_mut() {
2200                        match pane {
2201                            CommandOrPlugin::Command(run_command) => {
2202                                run_command.populate_originating_plugin(originating_plugin.clone());
2203                            },
2204                            _ => {},
2205                        }
2206                    }
2207                }
2208            },
2209            _ => {},
2210        }
2211    }
2212    pub fn launches_plugin(&self, plugin_url: &str) -> bool {
2213        match self {
2214            Action::LaunchPlugin { plugin, .. } => &plugin.location_string() == plugin_url,
2215            Action::LaunchOrFocusPlugin { plugin, .. } => &plugin.location_string() == plugin_url,
2216            _ => false,
2217        }
2218    }
2219    pub fn is_mouse_action(&self) -> bool {
2220        if let Action::MouseEvent { .. } = self {
2221            return true;
2222        }
2223        false
2224    }
2225}
2226
2227fn suggest_key_fix(key_str: &str) -> String {
2228    if key_str.contains('-') {
2229        return "  Hint: Use spaces instead of hyphens (e.g., \"Ctrl a\" not \"Ctrl-a\")"
2230            .to_string();
2231    }
2232
2233    if key_str.trim().is_empty() {
2234        return "  Hint: Key string cannot be empty".to_string();
2235    }
2236
2237    let parts: Vec<&str> = key_str.split_whitespace().collect();
2238    if parts.len() > 1 {
2239        for part in &parts[..parts.len() - 1] {
2240            let lower = part.to_ascii_lowercase();
2241            if lower.starts_with("ctr") && lower != "ctrl" {
2242                return format!("  Hint: Did you mean \"Ctrl\" instead of \"{}\"?", part);
2243            }
2244            if !matches!(lower.as_str(), "ctrl" | "alt" | "shift" | "super") {
2245                return "  Hint: Valid modifiers are: Ctrl, Alt, Shift, Super".to_string();
2246            }
2247        }
2248    }
2249
2250    "  Hint: Use format like \"Ctrl a\", \"Alt Shift F1\", or \"Enter\"".to_string()
2251}
2252
2253impl From<OnForceClose> for Action {
2254    fn from(ofc: OnForceClose) -> Action {
2255        match ofc {
2256            OnForceClose::Quit => Action::Quit,
2257            OnForceClose::Detach => Action::Detach,
2258        }
2259    }
2260}
2261
2262#[cfg(test)]
2263mod tests {
2264    use super::*;
2265    use crate::data::BareKey;
2266    use crate::data::KeyModifier;
2267    use std::path::PathBuf;
2268
2269    #[test]
2270    fn test_send_keys_single_key() {
2271        let cli_action = CliAction::SendKeys {
2272            keys: vec!["Enter".to_string()],
2273            pane_id: None,
2274        };
2275        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2276        assert!(result.is_ok());
2277        let actions = result.unwrap();
2278        assert_eq!(actions.len(), 1);
2279        match &actions[0] {
2280            Action::Write {
2281                key_with_modifier,
2282                bytes,
2283                is_kitty_keyboard_protocol,
2284            } => {
2285                assert!(key_with_modifier.is_some());
2286                let key = key_with_modifier.as_ref().unwrap();
2287                assert_eq!(key.bare_key, BareKey::Enter);
2288                assert!(key.key_modifiers.is_empty());
2289                assert!(!bytes.is_empty());
2290                assert_eq!(*is_kitty_keyboard_protocol, true);
2291            },
2292            _ => panic!("Expected Write action"),
2293        }
2294    }
2295
2296    #[test]
2297    fn test_send_keys_with_modifier() {
2298        let cli_action = CliAction::SendKeys {
2299            keys: vec!["Ctrl a".to_string()],
2300            pane_id: None,
2301        };
2302        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2303        assert!(result.is_ok());
2304        let actions = result.unwrap();
2305        assert_eq!(actions.len(), 1);
2306        match &actions[0] {
2307            Action::Write {
2308                key_with_modifier,
2309                is_kitty_keyboard_protocol,
2310                ..
2311            } => {
2312                assert!(key_with_modifier.is_some());
2313                let key = key_with_modifier.as_ref().unwrap();
2314                assert_eq!(key.bare_key, BareKey::Char('a'));
2315                assert!(key.key_modifiers.contains(&KeyModifier::Ctrl));
2316                assert_eq!(*is_kitty_keyboard_protocol, true);
2317            },
2318            _ => panic!("Expected Write action"),
2319        }
2320    }
2321
2322    #[test]
2323    fn test_send_keys_multiple_keys() {
2324        let cli_action = CliAction::SendKeys {
2325            keys: vec!["Ctrl a".to_string(), "F1".to_string(), "Enter".to_string()],
2326            pane_id: None,
2327        };
2328        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2329        assert!(result.is_ok());
2330        let actions = result.unwrap();
2331        assert_eq!(actions.len(), 3);
2332        for action in &actions {
2333            match action {
2334                Action::Write {
2335                    is_kitty_keyboard_protocol,
2336                    ..
2337                } => {
2338                    assert_eq!(*is_kitty_keyboard_protocol, true);
2339                },
2340                _ => panic!("Expected Write action"),
2341            }
2342        }
2343    }
2344
2345    #[test]
2346    fn test_send_keys_error_hyphen_syntax() {
2347        let cli_action = CliAction::SendKeys {
2348            keys: vec!["Ctrl-a".to_string()],
2349            pane_id: None,
2350        };
2351        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2352        assert!(result.is_err());
2353        let err = result.unwrap_err();
2354        assert!(err.contains("Use spaces instead of hyphens"));
2355    }
2356
2357    #[test]
2358    fn test_send_keys_error_typo() {
2359        let cli_action = CliAction::SendKeys {
2360            keys: vec!["Ctrll a".to_string()],
2361            pane_id: None,
2362        };
2363        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2364        assert!(result.is_err());
2365        let err = result.unwrap_err();
2366        assert!(err.contains("Ctrl") || err.contains("modifier"));
2367    }
2368
2369    #[test]
2370    fn test_send_keys_with_pane_id() {
2371        let cli_action = CliAction::SendKeys {
2372            keys: vec!["a".to_string()],
2373            pane_id: Some("terminal_1".to_string()),
2374        };
2375        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2376        assert!(result.is_ok());
2377        let actions = result.unwrap();
2378        assert_eq!(actions.len(), 1);
2379        match &actions[0] {
2380            Action::WriteToPaneId { pane_id, bytes } => {
2381                assert!(matches!(pane_id, PaneId::Terminal(1)));
2382                assert!(!bytes.is_empty());
2383            },
2384            _ => panic!("Expected WriteToPaneId action"),
2385        }
2386    }
2387
2388    #[test]
2389    fn test_send_keys_error_invalid_pane_id() {
2390        let cli_action = CliAction::SendKeys {
2391            keys: vec!["a".to_string()],
2392            pane_id: Some("invalid_id".to_string()),
2393        };
2394        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2395        assert!(result.is_err());
2396        let err = result.unwrap_err();
2397        assert!(err.contains("Malformed pane id"));
2398    }
2399
2400    // =============================================
2401    // Category 1: Pane-targeting tests
2402    // =============================================
2403
2404    // 1. ScrollUp
2405    #[test]
2406    fn test_scroll_up_with_pane_id() {
2407        let cli_action = CliAction::ScrollUp {
2408            pane_id: Some("terminal_5".to_string()),
2409        };
2410        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2411        assert!(result.is_ok());
2412        let actions = result.unwrap();
2413        assert_eq!(actions.len(), 1);
2414        match &actions[0] {
2415            Action::ScrollUpByPaneId { pane_id } => {
2416                assert!(matches!(pane_id, PaneId::Terminal(5)));
2417            },
2418            _ => panic!("Expected ScrollUpByPaneId action"),
2419        }
2420    }
2421
2422    #[test]
2423    fn test_scroll_up_without_pane_id() {
2424        let cli_action = CliAction::ScrollUp { pane_id: None };
2425        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2426        assert!(result.is_ok());
2427        let actions = result.unwrap();
2428        assert_eq!(actions.len(), 1);
2429        assert!(matches!(actions[0], Action::ScrollUp));
2430    }
2431
2432    // 2. ScrollDown
2433    #[test]
2434    fn test_scroll_down_with_pane_id() {
2435        let cli_action = CliAction::ScrollDown {
2436            pane_id: Some("terminal_2".to_string()),
2437        };
2438        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2439        assert!(result.is_ok());
2440        let actions = result.unwrap();
2441        assert_eq!(actions.len(), 1);
2442        match &actions[0] {
2443            Action::ScrollDownByPaneId { pane_id } => {
2444                assert!(matches!(pane_id, PaneId::Terminal(2)));
2445            },
2446            _ => panic!("Expected ScrollDownByPaneId action"),
2447        }
2448    }
2449
2450    #[test]
2451    fn test_scroll_down_without_pane_id() {
2452        let cli_action = CliAction::ScrollDown { pane_id: None };
2453        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2454        assert!(result.is_ok());
2455        let actions = result.unwrap();
2456        assert_eq!(actions.len(), 1);
2457        assert!(matches!(actions[0], Action::ScrollDown));
2458    }
2459
2460    // 3. ScrollToTop
2461    #[test]
2462    fn test_scroll_to_top_with_pane_id() {
2463        let cli_action = CliAction::ScrollToTop {
2464            pane_id: Some("terminal_1".to_string()),
2465        };
2466        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2467        assert!(result.is_ok());
2468        let actions = result.unwrap();
2469        assert_eq!(actions.len(), 1);
2470        match &actions[0] {
2471            Action::ScrollToTopByPaneId { pane_id } => {
2472                assert!(matches!(pane_id, PaneId::Terminal(1)));
2473            },
2474            _ => panic!("Expected ScrollToTopByPaneId action"),
2475        }
2476    }
2477
2478    #[test]
2479    fn test_scroll_to_top_without_pane_id() {
2480        let cli_action = CliAction::ScrollToTop { pane_id: None };
2481        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2482        assert!(result.is_ok());
2483        let actions = result.unwrap();
2484        assert_eq!(actions.len(), 1);
2485        assert!(matches!(actions[0], Action::ScrollToTop));
2486    }
2487
2488    // 4. ScrollToBottom
2489    #[test]
2490    fn test_scroll_to_bottom_with_pane_id() {
2491        let cli_action = CliAction::ScrollToBottom {
2492            pane_id: Some("terminal_4".to_string()),
2493        };
2494        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2495        assert!(result.is_ok());
2496        let actions = result.unwrap();
2497        assert_eq!(actions.len(), 1);
2498        match &actions[0] {
2499            Action::ScrollToBottomByPaneId { pane_id } => {
2500                assert!(matches!(pane_id, PaneId::Terminal(4)));
2501            },
2502            _ => panic!("Expected ScrollToBottomByPaneId action"),
2503        }
2504    }
2505
2506    #[test]
2507    fn test_scroll_to_bottom_without_pane_id() {
2508        let cli_action = CliAction::ScrollToBottom { pane_id: None };
2509        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2510        assert!(result.is_ok());
2511        let actions = result.unwrap();
2512        assert_eq!(actions.len(), 1);
2513        assert!(matches!(actions[0], Action::ScrollToBottom));
2514    }
2515
2516    // 5. PageScrollUp
2517    #[test]
2518    fn test_page_scroll_up_with_pane_id() {
2519        let cli_action = CliAction::PageScrollUp {
2520            pane_id: Some("terminal_6".to_string()),
2521        };
2522        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2523        assert!(result.is_ok());
2524        let actions = result.unwrap();
2525        assert_eq!(actions.len(), 1);
2526        match &actions[0] {
2527            Action::PageScrollUpByPaneId { pane_id } => {
2528                assert!(matches!(pane_id, PaneId::Terminal(6)));
2529            },
2530            _ => panic!("Expected PageScrollUpByPaneId action"),
2531        }
2532    }
2533
2534    #[test]
2535    fn test_page_scroll_up_without_pane_id() {
2536        let cli_action = CliAction::PageScrollUp { pane_id: None };
2537        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2538        assert!(result.is_ok());
2539        let actions = result.unwrap();
2540        assert_eq!(actions.len(), 1);
2541        assert!(matches!(actions[0], Action::PageScrollUp));
2542    }
2543
2544    // 6. PageScrollDown
2545    #[test]
2546    fn test_page_scroll_down_with_pane_id() {
2547        let cli_action = CliAction::PageScrollDown {
2548            pane_id: Some("terminal_8".to_string()),
2549        };
2550        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2551        assert!(result.is_ok());
2552        let actions = result.unwrap();
2553        assert_eq!(actions.len(), 1);
2554        match &actions[0] {
2555            Action::PageScrollDownByPaneId { pane_id } => {
2556                assert!(matches!(pane_id, PaneId::Terminal(8)));
2557            },
2558            _ => panic!("Expected PageScrollDownByPaneId action"),
2559        }
2560    }
2561
2562    #[test]
2563    fn test_page_scroll_down_without_pane_id() {
2564        let cli_action = CliAction::PageScrollDown { pane_id: None };
2565        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2566        assert!(result.is_ok());
2567        let actions = result.unwrap();
2568        assert_eq!(actions.len(), 1);
2569        assert!(matches!(actions[0], Action::PageScrollDown));
2570    }
2571
2572    // 7. HalfPageScrollUp
2573    #[test]
2574    fn test_half_page_scroll_up_with_pane_id() {
2575        let cli_action = CliAction::HalfPageScrollUp {
2576            pane_id: Some("terminal_10".to_string()),
2577        };
2578        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2579        assert!(result.is_ok());
2580        let actions = result.unwrap();
2581        assert_eq!(actions.len(), 1);
2582        match &actions[0] {
2583            Action::HalfPageScrollUpByPaneId { pane_id } => {
2584                assert!(matches!(pane_id, PaneId::Terminal(10)));
2585            },
2586            _ => panic!("Expected HalfPageScrollUpByPaneId action"),
2587        }
2588    }
2589
2590    #[test]
2591    fn test_half_page_scroll_up_without_pane_id() {
2592        let cli_action = CliAction::HalfPageScrollUp { pane_id: None };
2593        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2594        assert!(result.is_ok());
2595        let actions = result.unwrap();
2596        assert_eq!(actions.len(), 1);
2597        assert!(matches!(actions[0], Action::HalfPageScrollUp));
2598    }
2599
2600    // 8. HalfPageScrollDown
2601    #[test]
2602    fn test_half_page_scroll_down_with_pane_id() {
2603        let cli_action = CliAction::HalfPageScrollDown {
2604            pane_id: Some("terminal_12".to_string()),
2605        };
2606        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2607        assert!(result.is_ok());
2608        let actions = result.unwrap();
2609        assert_eq!(actions.len(), 1);
2610        match &actions[0] {
2611            Action::HalfPageScrollDownByPaneId { pane_id } => {
2612                assert!(matches!(pane_id, PaneId::Terminal(12)));
2613            },
2614            _ => panic!("Expected HalfPageScrollDownByPaneId action"),
2615        }
2616    }
2617
2618    #[test]
2619    fn test_half_page_scroll_down_without_pane_id() {
2620        let cli_action = CliAction::HalfPageScrollDown { pane_id: None };
2621        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2622        assert!(result.is_ok());
2623        let actions = result.unwrap();
2624        assert_eq!(actions.len(), 1);
2625        assert!(matches!(actions[0], Action::HalfPageScrollDown));
2626    }
2627
2628    // 9. Resize
2629    #[test]
2630    fn test_resize_with_pane_id() {
2631        let cli_action = CliAction::Resize {
2632            resize: Resize::Increase,
2633            direction: Some(Direction::Left),
2634            pane_id: Some("terminal_3".to_string()),
2635        };
2636        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2637        assert!(result.is_ok());
2638        let actions = result.unwrap();
2639        assert_eq!(actions.len(), 1);
2640        match &actions[0] {
2641            Action::ResizeByPaneId {
2642                pane_id,
2643                resize,
2644                direction,
2645            } => {
2646                assert!(matches!(pane_id, PaneId::Terminal(3)));
2647                assert!(matches!(resize, Resize::Increase));
2648                assert!(matches!(direction, Some(Direction::Left)));
2649            },
2650            _ => panic!("Expected ResizeByPaneId action"),
2651        }
2652    }
2653
2654    #[test]
2655    fn test_resize_without_pane_id() {
2656        let cli_action = CliAction::Resize {
2657            resize: Resize::Increase,
2658            direction: Some(Direction::Left),
2659            pane_id: None,
2660        };
2661        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2662        assert!(result.is_ok());
2663        let actions = result.unwrap();
2664        assert_eq!(actions.len(), 1);
2665        match &actions[0] {
2666            Action::Resize { resize, direction } => {
2667                assert!(matches!(resize, Resize::Increase));
2668                assert!(matches!(direction, Some(Direction::Left)));
2669            },
2670            _ => panic!("Expected Resize action"),
2671        }
2672    }
2673
2674    // 10. MovePane
2675    #[test]
2676    fn test_move_pane_with_pane_id() {
2677        let cli_action = CliAction::MovePane {
2678            direction: Some(Direction::Right),
2679            pane_id: Some("terminal_9".to_string()),
2680        };
2681        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2682        assert!(result.is_ok());
2683        let actions = result.unwrap();
2684        assert_eq!(actions.len(), 1);
2685        match &actions[0] {
2686            Action::MovePaneByPaneId { pane_id, direction } => {
2687                assert!(matches!(pane_id, PaneId::Terminal(9)));
2688                assert!(matches!(direction, Some(Direction::Right)));
2689            },
2690            _ => panic!("Expected MovePaneByPaneId action"),
2691        }
2692    }
2693
2694    #[test]
2695    fn test_move_pane_without_pane_id() {
2696        let cli_action = CliAction::MovePane {
2697            direction: Some(Direction::Right),
2698            pane_id: None,
2699        };
2700        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2701        assert!(result.is_ok());
2702        let actions = result.unwrap();
2703        assert_eq!(actions.len(), 1);
2704        match &actions[0] {
2705            Action::MovePane { direction } => {
2706                assert!(matches!(direction, Some(Direction::Right)));
2707            },
2708            _ => panic!("Expected MovePane action"),
2709        }
2710    }
2711
2712    // 11. MovePaneBackwards
2713    #[test]
2714    fn test_move_pane_backwards_with_pane_id() {
2715        let cli_action = CliAction::MovePaneBackwards {
2716            pane_id: Some("terminal_11".to_string()),
2717        };
2718        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2719        assert!(result.is_ok());
2720        let actions = result.unwrap();
2721        assert_eq!(actions.len(), 1);
2722        match &actions[0] {
2723            Action::MovePaneBackwardsByPaneId { pane_id } => {
2724                assert!(matches!(pane_id, PaneId::Terminal(11)));
2725            },
2726            _ => panic!("Expected MovePaneBackwardsByPaneId action"),
2727        }
2728    }
2729
2730    #[test]
2731    fn test_move_pane_backwards_without_pane_id() {
2732        let cli_action = CliAction::MovePaneBackwards { pane_id: None };
2733        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2734        assert!(result.is_ok());
2735        let actions = result.unwrap();
2736        assert_eq!(actions.len(), 1);
2737        assert!(matches!(actions[0], Action::MovePaneBackwards));
2738    }
2739
2740    // 12. Clear
2741    #[test]
2742    fn test_clear_with_pane_id() {
2743        let cli_action = CliAction::Clear {
2744            pane_id: Some("terminal_14".to_string()),
2745        };
2746        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2747        assert!(result.is_ok());
2748        let actions = result.unwrap();
2749        assert_eq!(actions.len(), 1);
2750        match &actions[0] {
2751            Action::ClearScreenByPaneId { pane_id } => {
2752                assert!(matches!(pane_id, PaneId::Terminal(14)));
2753            },
2754            _ => panic!("Expected ClearScreenByPaneId action"),
2755        }
2756    }
2757
2758    #[test]
2759    fn test_clear_without_pane_id() {
2760        let cli_action = CliAction::Clear { pane_id: None };
2761        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2762        assert!(result.is_ok());
2763        let actions = result.unwrap();
2764        assert_eq!(actions.len(), 1);
2765        assert!(matches!(actions[0], Action::ClearScreen));
2766    }
2767
2768    // 13. EditScrollback
2769    #[test]
2770    fn test_edit_scrollback_with_pane_id() {
2771        let cli_action = CliAction::EditScrollback {
2772            pane_id: Some("terminal_15".to_string()),
2773            ansi: false,
2774        };
2775        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2776        assert!(result.is_ok());
2777        let actions = result.unwrap();
2778        assert_eq!(actions.len(), 1);
2779        match &actions[0] {
2780            Action::EditScrollbackByPaneId { pane_id, ansi } => {
2781                assert!(matches!(pane_id, PaneId::Terminal(15)));
2782                assert!(!ansi);
2783            },
2784            _ => panic!("Expected EditScrollbackByPaneId action"),
2785        }
2786    }
2787
2788    #[test]
2789    fn test_edit_scrollback_without_pane_id() {
2790        let cli_action = CliAction::EditScrollback {
2791            pane_id: None,
2792            ansi: false,
2793        };
2794        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2795        assert!(result.is_ok());
2796        let actions = result.unwrap();
2797        assert_eq!(actions.len(), 1);
2798        assert!(matches!(actions[0], Action::EditScrollback { ansi: false }));
2799    }
2800
2801    // 14. ToggleFullscreen
2802    #[test]
2803    fn test_toggle_fullscreen_with_pane_id() {
2804        let cli_action = CliAction::ToggleFullscreen {
2805            pane_id: Some("terminal_16".to_string()),
2806        };
2807        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2808        assert!(result.is_ok());
2809        let actions = result.unwrap();
2810        assert_eq!(actions.len(), 1);
2811        match &actions[0] {
2812            Action::ToggleFocusFullscreenByPaneId { pane_id } => {
2813                assert!(matches!(pane_id, PaneId::Terminal(16)));
2814            },
2815            _ => panic!("Expected ToggleFocusFullscreenByPaneId action"),
2816        }
2817    }
2818
2819    #[test]
2820    fn test_toggle_fullscreen_without_pane_id() {
2821        let cli_action = CliAction::ToggleFullscreen { pane_id: None };
2822        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2823        assert!(result.is_ok());
2824        let actions = result.unwrap();
2825        assert_eq!(actions.len(), 1);
2826        assert!(matches!(actions[0], Action::ToggleFocusFullscreen));
2827    }
2828
2829    // 15. TogglePaneEmbedOrFloating
2830    #[test]
2831    fn test_toggle_pane_embed_or_floating_with_pane_id() {
2832        let cli_action = CliAction::TogglePaneEmbedOrFloating {
2833            pane_id: Some("terminal_17".to_string()),
2834        };
2835        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2836        assert!(result.is_ok());
2837        let actions = result.unwrap();
2838        assert_eq!(actions.len(), 1);
2839        match &actions[0] {
2840            Action::TogglePaneEmbedOrFloatingByPaneId { pane_id } => {
2841                assert!(matches!(pane_id, PaneId::Terminal(17)));
2842            },
2843            _ => panic!("Expected TogglePaneEmbedOrFloatingByPaneId action"),
2844        }
2845    }
2846
2847    #[test]
2848    fn test_toggle_pane_embed_or_floating_without_pane_id() {
2849        let cli_action = CliAction::TogglePaneEmbedOrFloating { pane_id: None };
2850        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2851        assert!(result.is_ok());
2852        let actions = result.unwrap();
2853        assert_eq!(actions.len(), 1);
2854        assert!(matches!(actions[0], Action::TogglePaneEmbedOrFloating));
2855    }
2856
2857    // 16. ClosePane
2858    #[test]
2859    fn test_close_pane_with_pane_id() {
2860        let cli_action = CliAction::ClosePane {
2861            pane_id: Some("terminal_18".to_string()),
2862        };
2863        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2864        assert!(result.is_ok());
2865        let actions = result.unwrap();
2866        assert_eq!(actions.len(), 1);
2867        match &actions[0] {
2868            Action::CloseFocusByPaneId { pane_id } => {
2869                assert!(matches!(pane_id, PaneId::Terminal(18)));
2870            },
2871            _ => panic!("Expected CloseFocusByPaneId action"),
2872        }
2873    }
2874
2875    #[test]
2876    fn test_close_pane_without_pane_id() {
2877        let cli_action = CliAction::ClosePane { pane_id: None };
2878        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2879        assert!(result.is_ok());
2880        let actions = result.unwrap();
2881        assert_eq!(actions.len(), 1);
2882        assert!(matches!(actions[0], Action::CloseFocus));
2883    }
2884
2885    // 17. RenamePane
2886    #[test]
2887    fn test_rename_pane_with_pane_id() {
2888        let cli_action = CliAction::RenamePane {
2889            name: "my-pane".to_string(),
2890            pane_id: Some("terminal_19".to_string()),
2891        };
2892        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2893        assert!(result.is_ok());
2894        let actions = result.unwrap();
2895        assert_eq!(actions.len(), 1);
2896        match &actions[0] {
2897            Action::RenamePaneByPaneId { pane_id, name } => {
2898                assert!(matches!(pane_id, Some(PaneId::Terminal(19))));
2899                assert_eq!(name, &"my-pane".as_bytes().to_vec());
2900            },
2901            _ => panic!("Expected RenamePaneByPaneId action"),
2902        }
2903    }
2904
2905    #[test]
2906    fn test_rename_pane_without_pane_id() {
2907        let cli_action = CliAction::RenamePane {
2908            name: "my-pane".to_string(),
2909            pane_id: None,
2910        };
2911        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2912        assert!(result.is_ok());
2913        let actions = result.unwrap();
2914        assert_eq!(actions.len(), 1);
2915        match &actions[0] {
2916            Action::RenamePaneByPaneId { pane_id, name } => {
2917                assert!(pane_id.is_none());
2918                assert_eq!(name, &"my-pane".as_bytes().to_vec());
2919            },
2920            _ => panic!("Expected RenamePaneByPaneId action"),
2921        }
2922    }
2923
2924    // 18. UndoRenamePane
2925    #[test]
2926    fn test_undo_rename_pane_with_pane_id() {
2927        let cli_action = CliAction::UndoRenamePane {
2928            pane_id: Some("terminal_20".to_string()),
2929        };
2930        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2931        assert!(result.is_ok());
2932        let actions = result.unwrap();
2933        assert_eq!(actions.len(), 1);
2934        match &actions[0] {
2935            Action::UndoRenamePaneByPaneId { pane_id } => {
2936                assert!(matches!(pane_id, PaneId::Terminal(20)));
2937            },
2938            _ => panic!("Expected UndoRenamePaneByPaneId action"),
2939        }
2940    }
2941
2942    #[test]
2943    fn test_undo_rename_pane_without_pane_id() {
2944        let cli_action = CliAction::UndoRenamePane { pane_id: None };
2945        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2946        assert!(result.is_ok());
2947        let actions = result.unwrap();
2948        assert_eq!(actions.len(), 1);
2949        assert!(matches!(actions[0], Action::UndoRenamePane));
2950    }
2951
2952    // 19. TogglePanePinned
2953    #[test]
2954    fn test_toggle_pane_pinned_with_pane_id() {
2955        let cli_action = CliAction::TogglePanePinned {
2956            pane_id: Some("terminal_21".to_string()),
2957        };
2958        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2959        assert!(result.is_ok());
2960        let actions = result.unwrap();
2961        assert_eq!(actions.len(), 1);
2962        match &actions[0] {
2963            Action::TogglePanePinnedByPaneId { pane_id } => {
2964                assert!(matches!(pane_id, PaneId::Terminal(21)));
2965            },
2966            _ => panic!("Expected TogglePanePinnedByPaneId action"),
2967        }
2968    }
2969
2970    #[test]
2971    fn test_toggle_pane_pinned_without_pane_id() {
2972        let cli_action = CliAction::TogglePanePinned { pane_id: None };
2973        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2974        assert!(result.is_ok());
2975        let actions = result.unwrap();
2976        assert_eq!(actions.len(), 1);
2977        assert!(matches!(actions[0], Action::TogglePanePinned));
2978    }
2979
2980    // Extra pane tests
2981    #[test]
2982    fn test_scroll_up_with_plugin_pane_id() {
2983        let cli_action = CliAction::ScrollUp {
2984            pane_id: Some("plugin_3".to_string()),
2985        };
2986        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
2987        assert!(result.is_ok());
2988        let actions = result.unwrap();
2989        assert_eq!(actions.len(), 1);
2990        match &actions[0] {
2991            Action::ScrollUpByPaneId { pane_id } => {
2992                assert!(matches!(pane_id, PaneId::Plugin(3)));
2993            },
2994            _ => panic!("Expected ScrollUpByPaneId action with plugin pane id"),
2995        }
2996    }
2997
2998    #[test]
2999    fn test_scroll_up_with_bare_integer_pane_id() {
3000        let cli_action = CliAction::ScrollUp {
3001            pane_id: Some("7".to_string()),
3002        };
3003        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3004        assert!(result.is_ok());
3005        let actions = result.unwrap();
3006        assert_eq!(actions.len(), 1);
3007        match &actions[0] {
3008            Action::ScrollUpByPaneId { pane_id } => {
3009                assert!(matches!(pane_id, PaneId::Terminal(7)));
3010            },
3011            _ => panic!("Expected ScrollUpByPaneId action with bare integer pane id"),
3012        }
3013    }
3014
3015    #[test]
3016    fn test_scroll_up_with_invalid_pane_id() {
3017        let cli_action = CliAction::ScrollUp {
3018            pane_id: Some("invalid_id".to_string()),
3019        };
3020        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3021        assert!(result.is_err());
3022        let err = result.unwrap_err();
3023        assert!(err.contains("Malformed pane id"));
3024    }
3025
3026    // =============================================
3027    // Category 1: Tab-targeting tests
3028    // =============================================
3029
3030    // 20. CloseTab
3031    #[test]
3032    fn test_close_tab_with_tab_id() {
3033        let cli_action = CliAction::CloseTab { tab_id: Some(5) };
3034        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3035        assert!(result.is_ok());
3036        let actions = result.unwrap();
3037        assert_eq!(actions.len(), 1);
3038        match &actions[0] {
3039            Action::CloseTabById { id } => {
3040                assert_eq!(*id, 5u64);
3041            },
3042            _ => panic!("Expected CloseTabById action"),
3043        }
3044    }
3045
3046    #[test]
3047    fn test_close_tab_without_tab_id() {
3048        let cli_action = CliAction::CloseTab { tab_id: None };
3049        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3050        assert!(result.is_ok());
3051        let actions = result.unwrap();
3052        assert_eq!(actions.len(), 1);
3053        assert!(matches!(actions[0], Action::CloseTab));
3054    }
3055
3056    // 21. RenameTab
3057    #[test]
3058    fn test_rename_tab_with_tab_id() {
3059        let cli_action = CliAction::RenameTab {
3060            name: "my-tab".to_string(),
3061            tab_id: Some(3),
3062        };
3063        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3064        assert!(result.is_ok());
3065        let actions = result.unwrap();
3066        assert_eq!(actions.len(), 1);
3067        match &actions[0] {
3068            Action::RenameTabById { id, name } => {
3069                assert_eq!(*id, 3u64);
3070                assert_eq!(name, "my-tab");
3071            },
3072            _ => panic!("Expected RenameTabById action"),
3073        }
3074    }
3075
3076    #[test]
3077    fn test_rename_tab_without_tab_id() {
3078        let cli_action = CliAction::RenameTab {
3079            name: "my-tab".to_string(),
3080            tab_id: None,
3081        };
3082        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3083        assert!(result.is_ok());
3084        let actions = result.unwrap();
3085        assert_eq!(actions.len(), 2);
3086        assert!(matches!(actions[0], Action::TabNameInput { .. }));
3087        assert!(matches!(actions[1], Action::TabNameInput { .. }));
3088    }
3089
3090    // 22. UndoRenameTab
3091    #[test]
3092    fn test_undo_rename_tab_with_tab_id() {
3093        let cli_action = CliAction::UndoRenameTab { tab_id: Some(7) };
3094        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3095        assert!(result.is_ok());
3096        let actions = result.unwrap();
3097        assert_eq!(actions.len(), 1);
3098        match &actions[0] {
3099            Action::UndoRenameTabByTabId { id } => {
3100                assert_eq!(*id, 7u64);
3101            },
3102            _ => panic!("Expected UndoRenameTabByTabId action"),
3103        }
3104    }
3105
3106    #[test]
3107    fn test_undo_rename_tab_without_tab_id() {
3108        let cli_action = CliAction::UndoRenameTab { tab_id: None };
3109        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3110        assert!(result.is_ok());
3111        let actions = result.unwrap();
3112        assert_eq!(actions.len(), 1);
3113        assert!(matches!(actions[0], Action::UndoRenameTab));
3114    }
3115
3116    // 23. ToggleActiveSyncTab
3117    #[test]
3118    fn test_toggle_active_sync_tab_with_tab_id() {
3119        let cli_action = CliAction::ToggleActiveSyncTab { tab_id: Some(2) };
3120        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3121        assert!(result.is_ok());
3122        let actions = result.unwrap();
3123        assert_eq!(actions.len(), 1);
3124        match &actions[0] {
3125            Action::ToggleActiveSyncTabByTabId { id } => {
3126                assert_eq!(*id, 2u64);
3127            },
3128            _ => panic!("Expected ToggleActiveSyncTabByTabId action"),
3129        }
3130    }
3131
3132    #[test]
3133    fn test_toggle_active_sync_tab_without_tab_id() {
3134        let cli_action = CliAction::ToggleActiveSyncTab { tab_id: None };
3135        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3136        assert!(result.is_ok());
3137        let actions = result.unwrap();
3138        assert_eq!(actions.len(), 1);
3139        assert!(matches!(actions[0], Action::ToggleActiveSyncTab));
3140    }
3141
3142    // 24. ToggleFloatingPanes
3143    #[test]
3144    fn test_toggle_floating_panes_with_tab_id() {
3145        let cli_action = CliAction::ToggleFloatingPanes { tab_id: Some(4) };
3146        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3147        assert!(result.is_ok());
3148        let actions = result.unwrap();
3149        assert_eq!(actions.len(), 1);
3150        match &actions[0] {
3151            Action::ToggleFloatingPanesByTabId { id } => {
3152                assert_eq!(*id, 4u64);
3153            },
3154            _ => panic!("Expected ToggleFloatingPanesByTabId action"),
3155        }
3156    }
3157
3158    #[test]
3159    fn test_toggle_floating_panes_without_tab_id() {
3160        let cli_action = CliAction::ToggleFloatingPanes { tab_id: None };
3161        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3162        assert!(result.is_ok());
3163        let actions = result.unwrap();
3164        assert_eq!(actions.len(), 1);
3165        assert!(matches!(actions[0], Action::ToggleFloatingPanes));
3166    }
3167
3168    // 25. PreviousSwapLayout
3169    #[test]
3170    fn test_previous_swap_layout_with_tab_id() {
3171        let cli_action = CliAction::PreviousSwapLayout { tab_id: Some(6) };
3172        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3173        assert!(result.is_ok());
3174        let actions = result.unwrap();
3175        assert_eq!(actions.len(), 1);
3176        match &actions[0] {
3177            Action::PreviousSwapLayoutByTabId { id } => {
3178                assert_eq!(*id, 6u64);
3179            },
3180            _ => panic!("Expected PreviousSwapLayoutByTabId action"),
3181        }
3182    }
3183
3184    #[test]
3185    fn test_previous_swap_layout_without_tab_id() {
3186        let cli_action = CliAction::PreviousSwapLayout { tab_id: None };
3187        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3188        assert!(result.is_ok());
3189        let actions = result.unwrap();
3190        assert_eq!(actions.len(), 1);
3191        assert!(matches!(actions[0], Action::PreviousSwapLayout));
3192    }
3193
3194    // 26. NextSwapLayout
3195    #[test]
3196    fn test_next_swap_layout_with_tab_id() {
3197        let cli_action = CliAction::NextSwapLayout { tab_id: Some(8) };
3198        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3199        assert!(result.is_ok());
3200        let actions = result.unwrap();
3201        assert_eq!(actions.len(), 1);
3202        match &actions[0] {
3203            Action::NextSwapLayoutByTabId { id } => {
3204                assert_eq!(*id, 8u64);
3205            },
3206            _ => panic!("Expected NextSwapLayoutByTabId action"),
3207        }
3208    }
3209
3210    #[test]
3211    fn test_next_swap_layout_without_tab_id() {
3212        let cli_action = CliAction::NextSwapLayout { tab_id: None };
3213        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3214        assert!(result.is_ok());
3215        let actions = result.unwrap();
3216        assert_eq!(actions.len(), 1);
3217        assert!(matches!(actions[0], Action::NextSwapLayout));
3218    }
3219
3220    // 27. MoveTab
3221    #[test]
3222    fn test_move_tab_with_tab_id() {
3223        let cli_action = CliAction::MoveTab {
3224            direction: Direction::Right,
3225            tab_id: Some(10),
3226        };
3227        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3228        assert!(result.is_ok());
3229        let actions = result.unwrap();
3230        assert_eq!(actions.len(), 1);
3231        match &actions[0] {
3232            Action::MoveTabByTabId { id, direction } => {
3233                assert_eq!(*id, 10u64);
3234                assert!(matches!(direction, Direction::Right));
3235            },
3236            _ => panic!("Expected MoveTabByTabId action"),
3237        }
3238    }
3239
3240    #[test]
3241    fn test_move_tab_without_tab_id() {
3242        let cli_action = CliAction::MoveTab {
3243            direction: Direction::Right,
3244            tab_id: None,
3245        };
3246        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3247        assert!(result.is_ok());
3248        let actions = result.unwrap();
3249        assert_eq!(actions.len(), 1);
3250        match &actions[0] {
3251            Action::MoveTab { direction } => {
3252                assert!(matches!(direction, Direction::Right));
3253            },
3254            _ => panic!("Expected MoveTab action"),
3255        }
3256    }
3257
3258    // 28. ANSI flag tests
3259
3260    #[test]
3261    fn test_edit_scrollback_with_ansi_flag() {
3262        let cli_action = CliAction::EditScrollback {
3263            pane_id: None,
3264            ansi: true,
3265        };
3266        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3267        assert!(result.is_ok());
3268        let actions = result.unwrap();
3269        assert_eq!(actions.len(), 1);
3270        assert!(matches!(actions[0], Action::EditScrollback { ansi: true }));
3271    }
3272
3273    #[test]
3274    fn test_edit_scrollback_with_pane_id_and_ansi() {
3275        let cli_action = CliAction::EditScrollback {
3276            pane_id: Some("terminal_15".to_string()),
3277            ansi: true,
3278        };
3279        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3280        assert!(result.is_ok());
3281        let actions = result.unwrap();
3282        assert_eq!(actions.len(), 1);
3283        match &actions[0] {
3284            Action::EditScrollbackByPaneId { pane_id, ansi } => {
3285                assert_eq!(*pane_id, PaneId::Terminal(15));
3286                assert!(*ansi);
3287            },
3288            _ => panic!("Expected EditScrollbackByPaneId action"),
3289        }
3290    }
3291
3292    #[test]
3293    fn test_dump_screen_with_ansi_flag() {
3294        let cli_action = CliAction::DumpScreen {
3295            path: Some(PathBuf::from("/tmp/test")),
3296            full: true,
3297            pane_id: None,
3298            ansi: true,
3299        };
3300        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3301        assert!(result.is_ok());
3302        let actions = result.unwrap();
3303        assert_eq!(actions.len(), 1);
3304        match &actions[0] {
3305            Action::DumpScreen {
3306                ansi,
3307                include_scrollback,
3308                ..
3309            } => {
3310                assert!(*ansi);
3311                assert!(*include_scrollback);
3312            },
3313            _ => panic!("Expected DumpScreen action"),
3314        }
3315    }
3316
3317    #[test]
3318    fn test_dump_screen_with_pane_id_and_ansi() {
3319        let cli_action = CliAction::DumpScreen {
3320            path: None,
3321            full: false,
3322            pane_id: Some("terminal_5".to_string()),
3323            ansi: true,
3324        };
3325        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3326        assert!(result.is_ok());
3327        let actions = result.unwrap();
3328        assert_eq!(actions.len(), 1);
3329        match &actions[0] {
3330            Action::DumpScreen { pane_id, ansi, .. } => {
3331                assert_eq!(*pane_id, Some(PaneId::Terminal(5)));
3332                assert!(*ansi);
3333            },
3334            _ => panic!("Expected DumpScreen action"),
3335        }
3336    }
3337
3338    #[test]
3339    fn test_focus_pane_id() {
3340        let cli_action = CliAction::FocusPaneId {
3341            pane_id: "terminal_7".to_string(),
3342        };
3343        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3344        assert!(result.is_ok());
3345        let actions = result.unwrap();
3346        assert_eq!(actions.len(), 1);
3347        match &actions[0] {
3348            Action::FocusPaneByPaneId { pane_id } => {
3349                assert!(matches!(pane_id, PaneId::Terminal(7)));
3350            },
3351            _ => panic!("Expected FocusPaneByPaneId action"),
3352        }
3353    }
3354
3355    #[test]
3356    fn test_focus_pane_id_bare_int() {
3357        let cli_action = CliAction::FocusPaneId {
3358            pane_id: "3".to_string(),
3359        };
3360        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3361        assert!(result.is_ok());
3362        let actions = result.unwrap();
3363        assert_eq!(actions.len(), 1);
3364        match &actions[0] {
3365            Action::FocusPaneByPaneId { pane_id } => {
3366                assert!(matches!(pane_id, PaneId::Terminal(3)));
3367            },
3368            _ => panic!("Expected FocusPaneByPaneId action"),
3369        }
3370    }
3371
3372    #[test]
3373    fn test_focus_pane_id_plugin() {
3374        let cli_action = CliAction::FocusPaneId {
3375            pane_id: "plugin_2".to_string(),
3376        };
3377        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3378        assert!(result.is_ok());
3379        let actions = result.unwrap();
3380        assert_eq!(actions.len(), 1);
3381        match &actions[0] {
3382            Action::FocusPaneByPaneId { pane_id } => {
3383                assert!(matches!(pane_id, PaneId::Plugin(2)));
3384            },
3385            _ => panic!("Expected FocusPaneByPaneId action"),
3386        }
3387    }
3388
3389    #[test]
3390    fn test_focus_pane_id_malformed() {
3391        let cli_action = CliAction::FocusPaneId {
3392            pane_id: "invalid_id".to_string(),
3393        };
3394        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3395        assert!(result.is_err());
3396    }
3397
3398    #[test]
3399    fn test_new_tab_with_layout_string() {
3400        let cli_action = CliAction::NewTab {
3401            name: None,
3402            layout: None,
3403            layout_string: Some("layout {\n    pane\n    pane\n}\n".into()),
3404            layout_dir: None,
3405            cwd: None,
3406            initial_command: vec![],
3407            initial_plugin: None,
3408            close_on_exit: Default::default(),
3409            start_suspended: Default::default(),
3410            block_until_exit: false,
3411            block_until_exit_success: false,
3412            block_until_exit_failure: false,
3413        };
3414        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3415        assert!(result.is_ok());
3416        let actions = result.unwrap();
3417        assert_eq!(actions.len(), 1);
3418        match &actions[0] {
3419            Action::NewTab {
3420                tiled_layout,
3421                floating_layouts,
3422                ..
3423            } => {
3424                assert!(tiled_layout.is_some());
3425                let layout = tiled_layout.as_ref().unwrap();
3426                // layout { pane; pane } produces a layout with 2 children
3427                assert_eq!(layout.children.len(), 2);
3428                assert!(floating_layouts.is_empty());
3429            },
3430            _ => panic!("Expected NewTab action"),
3431        }
3432    }
3433
3434    #[test]
3435    fn test_new_tab_with_invalid_layout_string() {
3436        let cli_action = CliAction::NewTab {
3437            name: None,
3438            layout: None,
3439            layout_string: Some("invalid { kdl".into()),
3440            layout_dir: None,
3441            cwd: None,
3442            initial_command: vec![],
3443            initial_plugin: None,
3444            close_on_exit: Default::default(),
3445            start_suspended: Default::default(),
3446            block_until_exit: false,
3447            block_until_exit_success: false,
3448            block_until_exit_failure: false,
3449        };
3450        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3451        assert!(result.is_err());
3452    }
3453
3454    #[test]
3455    fn test_override_layout_with_layout_string() {
3456        let cli_action = CliAction::OverrideLayout {
3457            layout: None,
3458            layout_string: Some("layout {\n    pane\n    pane\n}\n".into()),
3459            layout_dir: None,
3460            retain_existing_terminal_panes: false,
3461            retain_existing_plugin_panes: false,
3462            apply_only_to_active_tab: false,
3463        };
3464        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3465        assert!(result.is_ok());
3466        let actions = result.unwrap();
3467        assert_eq!(actions.len(), 1);
3468        match &actions[0] {
3469            Action::OverrideLayout { tabs, .. } => {
3470                assert!(!tabs.is_empty());
3471            },
3472            _ => panic!("Expected OverrideLayout action"),
3473        }
3474    }
3475
3476    #[test]
3477    fn test_switch_session_with_layout_string() {
3478        let cli_action = CliAction::SwitchSession {
3479            name: "test-session".into(),
3480            tab_position: None,
3481            pane_id: None,
3482            layout: None,
3483            layout_string: Some("layout {\n    pane\n}\n".into()),
3484            layout_dir: None,
3485            cwd: None,
3486        };
3487        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3488        assert!(result.is_ok());
3489        let actions = result.unwrap();
3490        assert_eq!(actions.len(), 1);
3491        match &actions[0] {
3492            Action::SwitchSession { layout, .. } => {
3493                assert!(matches!(
3494                    layout,
3495                    Some(crate::data::LayoutInfo::Stringified(_))
3496                ));
3497            },
3498            _ => panic!("Expected SwitchSession action"),
3499        }
3500    }
3501
3502    #[test]
3503    fn test_switch_session_with_invalid_layout_string() {
3504        let cli_action = CliAction::SwitchSession {
3505            name: "test-session".into(),
3506            tab_position: None,
3507            pane_id: None,
3508            layout: None,
3509            layout_string: Some("invalid { kdl".into()),
3510            layout_dir: None,
3511            cwd: None,
3512        };
3513        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3514        assert!(result.is_err());
3515    }
3516
3517    // Tab-targeting for pane creation commands
3518
3519    #[test]
3520    fn test_new_pane_tiled_with_tab_id() {
3521        let cli_action = CliAction::NewPane {
3522            direction: Some(Direction::Right),
3523            command: vec![],
3524            plugin: None,
3525            cwd: None,
3526            floating: false,
3527            in_place: false,
3528            close_replaced_pane: false,
3529            name: None,
3530            close_on_exit: false,
3531            start_suspended: false,
3532            configuration: None,
3533            skip_plugin_cache: false,
3534            x: None,
3535            y: None,
3536            width: None,
3537            height: None,
3538            pinned: None,
3539            stacked: false,
3540            blocking: false,
3541            block_until_exit_success: false,
3542            block_until_exit_failure: false,
3543            block_until_exit: false,
3544            unblock_condition: None,
3545            near_current_pane: false,
3546            borderless: None,
3547            tab_id: Some(3),
3548        };
3549        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3550        assert!(result.is_ok());
3551        let actions = result.unwrap();
3552        assert_eq!(actions.len(), 1);
3553        match &actions[0] {
3554            Action::NewTiledPane { tab_id, .. } => {
3555                assert_eq!(*tab_id, Some(3));
3556            },
3557            _ => panic!("Expected NewTiledPane action"),
3558        }
3559    }
3560
3561    #[test]
3562    fn test_new_pane_tiled_without_tab_id() {
3563        let cli_action = CliAction::NewPane {
3564            direction: None,
3565            command: vec![],
3566            plugin: None,
3567            cwd: None,
3568            floating: false,
3569            in_place: false,
3570            close_replaced_pane: false,
3571            name: None,
3572            close_on_exit: false,
3573            start_suspended: false,
3574            configuration: None,
3575            skip_plugin_cache: false,
3576            x: None,
3577            y: None,
3578            width: None,
3579            height: None,
3580            pinned: None,
3581            stacked: false,
3582            blocking: false,
3583            block_until_exit_success: false,
3584            block_until_exit_failure: false,
3585            block_until_exit: false,
3586            unblock_condition: None,
3587            near_current_pane: false,
3588            borderless: None,
3589            tab_id: None,
3590        };
3591        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3592        assert!(result.is_ok());
3593        let actions = result.unwrap();
3594        assert_eq!(actions.len(), 1);
3595        match &actions[0] {
3596            Action::NewTiledPane { tab_id, .. } => {
3597                assert_eq!(*tab_id, None);
3598            },
3599            _ => panic!("Expected NewTiledPane action"),
3600        }
3601    }
3602
3603    #[test]
3604    fn test_new_pane_floating_with_tab_id() {
3605        let cli_action = CliAction::NewPane {
3606            direction: None,
3607            command: vec![],
3608            plugin: None,
3609            cwd: None,
3610            floating: true,
3611            in_place: false,
3612            close_replaced_pane: false,
3613            name: None,
3614            close_on_exit: false,
3615            start_suspended: false,
3616            configuration: None,
3617            skip_plugin_cache: false,
3618            x: None,
3619            y: None,
3620            width: None,
3621            height: None,
3622            pinned: None,
3623            stacked: false,
3624            blocking: false,
3625            block_until_exit_success: false,
3626            block_until_exit_failure: false,
3627            block_until_exit: false,
3628            unblock_condition: None,
3629            near_current_pane: false,
3630            borderless: None,
3631            tab_id: Some(5),
3632        };
3633        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3634        assert!(result.is_ok());
3635        let actions = result.unwrap();
3636        assert_eq!(actions.len(), 1);
3637        match &actions[0] {
3638            Action::NewFloatingPane { tab_id, .. } => {
3639                assert_eq!(*tab_id, Some(5));
3640            },
3641            _ => panic!("Expected NewFloatingPane action"),
3642        }
3643    }
3644
3645    #[test]
3646    fn test_new_pane_stacked_with_tab_id() {
3647        let cli_action = CliAction::NewPane {
3648            direction: None,
3649            command: vec!["ls".into()],
3650            plugin: None,
3651            cwd: None,
3652            floating: false,
3653            in_place: false,
3654            close_replaced_pane: false,
3655            name: None,
3656            close_on_exit: false,
3657            start_suspended: false,
3658            configuration: None,
3659            skip_plugin_cache: false,
3660            x: None,
3661            y: None,
3662            width: None,
3663            height: None,
3664            pinned: None,
3665            stacked: true,
3666            blocking: false,
3667            block_until_exit_success: false,
3668            block_until_exit_failure: false,
3669            block_until_exit: false,
3670            unblock_condition: None,
3671            near_current_pane: false,
3672            borderless: None,
3673            tab_id: Some(1),
3674        };
3675        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3676        assert!(result.is_ok());
3677        let actions = result.unwrap();
3678        assert_eq!(actions.len(), 1);
3679        match &actions[0] {
3680            Action::NewStackedPane { tab_id, .. } => {
3681                assert_eq!(*tab_id, Some(1));
3682            },
3683            _ => panic!("Expected NewStackedPane action"),
3684        }
3685    }
3686
3687    #[test]
3688    fn test_new_pane_blocking_with_tab_id() {
3689        let cli_action = CliAction::NewPane {
3690            direction: None,
3691            command: vec!["ls".into()],
3692            plugin: None,
3693            cwd: None,
3694            floating: false,
3695            in_place: false,
3696            close_replaced_pane: false,
3697            name: None,
3698            close_on_exit: false,
3699            start_suspended: false,
3700            configuration: None,
3701            skip_plugin_cache: false,
3702            x: None,
3703            y: None,
3704            width: None,
3705            height: None,
3706            pinned: None,
3707            stacked: false,
3708            blocking: true,
3709            block_until_exit_success: false,
3710            block_until_exit_failure: false,
3711            block_until_exit: false,
3712            unblock_condition: None,
3713            near_current_pane: false,
3714            borderless: None,
3715            tab_id: Some(2),
3716        };
3717        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3718        assert!(result.is_ok());
3719        let actions = result.unwrap();
3720        assert_eq!(actions.len(), 1);
3721        match &actions[0] {
3722            Action::NewBlockingPane { tab_id, .. } => {
3723                assert_eq!(*tab_id, Some(2));
3724            },
3725            _ => panic!("Expected NewBlockingPane action"),
3726        }
3727    }
3728
3729    #[test]
3730    fn test_edit_with_tab_id() {
3731        let cli_action = CliAction::Edit {
3732            file: PathBuf::from("/tmp/test.rs"),
3733            direction: None,
3734            line_number: None,
3735            floating: false,
3736            in_place: false,
3737            close_replaced_pane: false,
3738            cwd: None,
3739            x: None,
3740            y: None,
3741            width: None,
3742            height: None,
3743            pinned: None,
3744            near_current_pane: false,
3745            borderless: None,
3746            tab_id: Some(4),
3747        };
3748        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3749        assert!(result.is_ok());
3750        let actions = result.unwrap();
3751        assert_eq!(actions.len(), 1);
3752        match &actions[0] {
3753            Action::EditFile { tab_id, .. } => {
3754                assert_eq!(*tab_id, Some(4));
3755            },
3756            _ => panic!("Expected EditFile action"),
3757        }
3758    }
3759
3760    #[test]
3761    fn test_edit_without_tab_id() {
3762        let cli_action = CliAction::Edit {
3763            file: PathBuf::from("/tmp/test.rs"),
3764            direction: None,
3765            line_number: None,
3766            floating: false,
3767            in_place: false,
3768            close_replaced_pane: false,
3769            cwd: None,
3770            x: None,
3771            y: None,
3772            width: None,
3773            height: None,
3774            pinned: None,
3775            near_current_pane: false,
3776            borderless: None,
3777            tab_id: None,
3778        };
3779        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3780        assert!(result.is_ok());
3781        let actions = result.unwrap();
3782        assert_eq!(actions.len(), 1);
3783        match &actions[0] {
3784            Action::EditFile { tab_id, .. } => {
3785                assert_eq!(*tab_id, None);
3786            },
3787            _ => panic!("Expected EditFile action"),
3788        }
3789    }
3790
3791    #[test]
3792    fn test_new_pane_plugin_tiled_with_tab_id() {
3793        let cli_action = CliAction::NewPane {
3794            direction: None,
3795            command: vec![],
3796            plugin: Some("zellij:strider".into()),
3797            cwd: None,
3798            floating: false,
3799            in_place: false,
3800            close_replaced_pane: false,
3801            name: None,
3802            close_on_exit: false,
3803            start_suspended: false,
3804            configuration: None,
3805            skip_plugin_cache: false,
3806            x: None,
3807            y: None,
3808            width: None,
3809            height: None,
3810            pinned: None,
3811            stacked: false,
3812            blocking: false,
3813            block_until_exit_success: false,
3814            block_until_exit_failure: false,
3815            block_until_exit: false,
3816            unblock_condition: None,
3817            near_current_pane: false,
3818            borderless: None,
3819            tab_id: Some(2),
3820        };
3821        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3822        assert!(result.is_ok());
3823        let actions = result.unwrap();
3824        assert_eq!(actions.len(), 1);
3825        match &actions[0] {
3826            Action::NewTiledPluginPane { tab_id, .. } => {
3827                assert_eq!(*tab_id, Some(2));
3828            },
3829            _ => panic!("Expected NewTiledPluginPane action"),
3830        }
3831    }
3832
3833    #[test]
3834    fn test_new_pane_plugin_floating_with_tab_id() {
3835        let cli_action = CliAction::NewPane {
3836            direction: None,
3837            command: vec![],
3838            plugin: Some("zellij:strider".into()),
3839            cwd: None,
3840            floating: true,
3841            in_place: false,
3842            close_replaced_pane: false,
3843            name: None,
3844            close_on_exit: false,
3845            start_suspended: false,
3846            configuration: None,
3847            skip_plugin_cache: false,
3848            x: None,
3849            y: None,
3850            width: None,
3851            height: None,
3852            pinned: None,
3853            stacked: false,
3854            blocking: false,
3855            block_until_exit_success: false,
3856            block_until_exit_failure: false,
3857            block_until_exit: false,
3858            unblock_condition: None,
3859            near_current_pane: false,
3860            borderless: None,
3861            tab_id: Some(1),
3862        };
3863        let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None);
3864        assert!(result.is_ok());
3865        let actions = result.unwrap();
3866        assert_eq!(actions.len(), 1);
3867        match &actions[0] {
3868            Action::NewFloatingPluginPane { tab_id, .. } => {
3869                assert_eq!(*tab_id, Some(1));
3870            },
3871            _ => panic!("Expected NewFloatingPluginPane action"),
3872        }
3873    }
3874}