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