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