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