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