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