use std::path::PathBuf; use crate::app_state::{ AppState, LeafContents, PaneNodeSnapshot, SplitDirection as StateSplitDirection, TabSnapshot, WindowSnapshot, }; use crate::themes::theme::AnsiColorIdentifier; use serde::{Deserialize, Deserializer, Serialize}; #[cfg(test)] #[path = "launch_config_tests.rs"] mod tests; #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct LaunchConfig { pub name: String, #[serde(skip_serializing_if = "Option::is_none", default)] pub active_window_index: Option, pub windows: Vec, } impl LaunchConfig { pub fn from_snapshot(name: String, app_state: &AppState) -> Self { Self { name, active_window_index: app_state.active_window_index, windows: app_state .windows .iter() .filter_map(|window| (!window.quake_mode).then_some(window.clone().into())) .collect::>(), } } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct WindowTemplate { #[serde(skip_serializing_if = "Option::is_none", default)] pub active_tab_index: Option, pub tabs: Vec, } impl From for WindowTemplate { fn from(snapshot: WindowSnapshot) -> Self { let mut active_tab_index = None; let mut num_valid_tabs = 0; let tabs = snapshot .tabs .into_iter() .enumerate() .filter_map(|(i, tab)| { let tab = tab.try_into().ok()?; if i == snapshot.active_tab_index { active_tab_index = Some(num_valid_tabs); } num_valid_tabs += 1; Some(tab) }) .collect::>(); Self { active_tab_index, tabs, } } } fn is_falsey(val: &Option) -> bool { val.is_none_or(|v| !v) } /// The mode a leaf pane opens in. /// /// Used by tab configs to distinguish terminal, agent, and cloud panes. /// Launch configs always produce `Terminal` (the default). #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum PaneMode { /// A standard terminal shell session. #[default] Terminal, /// A terminal that immediately enters Agent Mode. Agent, /// A cloud-mode (ambient agent) pane with no local shell. Cloud, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(untagged, rename_all = "lowercase")] pub enum PaneTemplateType { PaneTemplate { #[serde(deserialize_with = "deserialize_path")] cwd: PathBuf, #[serde(skip_serializing_if = "Vec::is_empty", default)] commands: Vec, #[serde(skip_serializing_if = "is_falsey", default)] is_focused: Option, #[serde(default)] pane_mode: PaneMode, /// Optional shell override for this pane (e.g. `"pwsh"`, `"zsh"`). /// Sourced from the `shell` field of a tab config pane node. #[serde(skip_serializing_if = "Option::is_none", default)] shell: Option, }, PaneBranchTemplate { split_direction: SplitDirection, panes: Vec, }, } impl TryFrom for PaneTemplateType { type Error = (); #[allow(clippy::unwrap_in_result)] fn try_from(snapshot: PaneNodeSnapshot) -> Result { match snapshot { PaneNodeSnapshot::Branch(branch) => { let panes = branch .children .iter() .filter_map(|(_, snapshot)| snapshot.clone().try_into().ok()) .collect::>(); match panes.len() { 0 => Err(()), 1 => Ok(panes .into_iter() .next() .expect("Checked that panes has 1 element")), _ => Ok(Self::PaneBranchTemplate { split_direction: branch.direction.into(), panes, }), } } PaneNodeSnapshot::Leaf(leaf) => match leaf.contents { LeafContents::Terminal(terminal) => Ok(Self::PaneTemplate { cwd: PathBuf::from(terminal.cwd.unwrap_or_default()), commands: Vec::new(), is_focused: Some(leaf.is_focused), pane_mode: PaneMode::Terminal, shell: None, }), // Currently, notebook panes cannot be saved in launch configurations. LeafContents::Notebook(_) | LeafContents::EnvVarCollection(_) | LeafContents::Code(_) | LeafContents::Workflow(_) | LeafContents::Settings(_) | LeafContents::AIFact(_) | LeafContents::CodeReview(_) | LeafContents::ExecutionProfileEditor | LeafContents::GetStarted | LeafContents::NetworkLog | LeafContents::Welcome { .. } | LeafContents::AIDocument(_) | LeafContents::EnvironmentManagement(_) | LeafContents::AmbientAgent(_) => { // TODO: Handle AIDocument in launch config Err(()) } }, } } } /// Deserializes a string that semantically represents a path, expanding ~ as /// needed. fn deserialize_path<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let raw_path = String::deserialize(deserializer)?; Ok(PathBuf::from(shellexpand::tilde(&raw_path).into_owned())) } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct TabTemplate { #[serde(skip_serializing_if = "Option::is_none", default)] pub title: Option, pub layout: PaneTemplateType, #[serde(skip_serializing_if = "Option::is_none", default)] pub color: Option, } impl TryFrom for TabTemplate { type Error = (); fn try_from(snapshot: TabSnapshot) -> Result { let color = snapshot.color(); Ok(Self { title: snapshot.custom_title, layout: snapshot.root.try_into()?, color, }) } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum SplitDirection { Vertical, Horizontal, } impl From for SplitDirection { fn from(snapshot: StateSplitDirection) -> Self { match snapshot { StateSplitDirection::Horizontal => Self::Horizontal, StateSplitDirection::Vertical => Self::Vertical, } } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct CommandTemplate { pub exec: String, } impl From<&str> for CommandTemplate { fn from(s: &str) -> CommandTemplate { CommandTemplate { exec: s.to_string(), } } } // TODO add extra elements to the mock (split panes, multiple tabs, multiple windows) pub fn make_mock_single_window_launch_config() -> LaunchConfig { LaunchConfig { name: "Mocked Config".to_string(), active_window_index: Some(0), windows: vec![WindowTemplate { active_tab_index: Some(0), tabs: vec![ TabTemplate { title: Some("First Tab".to_string()), layout: PaneTemplateType::PaneTemplate { is_focused: Some(true), cwd: PathBuf::from("/some/path"), commands: vec!["echo test_command".into()], pane_mode: PaneMode::Terminal, shell: None, }, color: None, }, TabTemplate { title: Some("Second Tab".to_string()), layout: PaneTemplateType::PaneTemplate { is_focused: Some(true), cwd: PathBuf::from("/some/path"), commands: vec!["echo test_command_on_another_tab".into()], pane_mode: PaneMode::Terminal, shell: None, }, color: None, }, ], }], } }