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