use galaxy_core::ui::appearance::Appearance; use galaxyui::{ r#async::SpawnedFutureHandle, AppContext, EntityId, SingletonEntity as _, ViewContext, ViewHandle, }; use parking_lot::FairMutex; use crate::terminal::{ model::{ ansi::SystemDetails, block::BlockId, session::SessionId, terminal_model::{SubshellInitializationInfo, TmuxInstallationState}, }, settings::TerminalSettings, shell::ShellType, ssh::{error::SshErrorBlock, install_tmux::SshInstallTmuxBlock, warpify::SshWarpifyBlock}, TerminalModel, TerminalView, }; use std::{collections::HashMap, sync::Arc}; use super::success_block::WarpifySuccessBlock; /// A unique identifier for a subshell separator. pub type SeparatorId = usize; /// These are elements in the BlockList which are similar to inline banners but are smaller, and /// only meant to render in compact mode when their in-padding flag couterparts don't have enough /// space in the padding to render. #[derive(Default)] struct SubshellSeparatorState { /// The ID for the next separator to be created. next_separator_id: SeparatorId, /// These are for rendering above the first block of a subshell session. separators: HashMap, } impl SubshellSeparatorState { /// Returns the ID to assign to the next separator fn next_separator_id(&mut self) -> SeparatorId { let next_id = self.next_separator_id; self.next_separator_id += 1; next_id } } #[derive(Debug)] pub enum SshBlockState { Warpifying { handle: ViewHandle, }, WarpifySuccess { handle: ViewHandle, }, InstallTmux { handle: ViewHandle, }, Error { handle: ViewHandle, }, } impl SshBlockState { pub fn should_prevent_input(&self) -> bool { !matches!(self, SshBlockState::InstallTmux { .. }) } pub fn get_block_view_id(&self) -> EntityId { match self { SshBlockState::Warpifying { handle, .. } => handle.id(), SshBlockState::WarpifySuccess { handle, .. } => handle.id(), SshBlockState::InstallTmux { handle, .. } => handle.id(), SshBlockState::Error { handle } => handle.id(), } } /// Returns `true` if the script was previously visible and is now collapsed. pub fn collapse_script(&mut self, ctx: &mut ViewContext) -> bool { match self { SshBlockState::InstallTmux { handle, .. } => { handle.update(ctx, |block, ctx| block.collapse_script(ctx)) } SshBlockState::Warpifying { .. } => false, SshBlockState::WarpifySuccess { .. } => false, SshBlockState::Error { .. } => false, } } pub fn focus(&mut self, ctx: &mut ViewContext) { match self { SshBlockState::Warpifying { handle } => { handle.update(ctx, |block, ctx| block.focus(ctx)); } SshBlockState::WarpifySuccess { .. } => {} SshBlockState::InstallTmux { handle } => { handle.update(ctx, |block, ctx| block.focus(ctx)); } SshBlockState::Error { handle } => { handle.update(ctx, |block, ctx| block.focus(ctx)); } } } pub fn get_system_details(&self, app: &AppContext) -> Option { match self { SshBlockState::InstallTmux { handle, .. } => { Some(handle.read(app, |view, _| view.system_details())) } SshBlockState::Warpifying { .. } => None, SshBlockState::WarpifySuccess { .. } => None, SshBlockState::Error { .. } => None, } } pub fn on_warpified_session_complete( &self, ctx: &mut ViewContext, ) -> Option { match self { SshBlockState::InstallTmux { .. } | SshBlockState::Warpifying { .. } => { let block_id = self.get_block_view_id(); return Some(block_id); } SshBlockState::WarpifySuccess { handle } => { handle.update(ctx, |block, ctx| { block.on_warpified_session_complete(ctx); }); } SshBlockState::Error { .. } => {} } None } } /// Temporary state used to trigger Warpification. #[derive(Default)] struct WarpifyTriggerState { block_id: Option, /// Lets us abort an attempt to auto warpify if the subshell command /// hasn't completed. auto_warpify_abort_handle: Option, /// The subshell banner waits 1s before showing. This is to see that the command stays running /// for a while without exiting. We store the abort handle here so that the /// TerminalEvent::BlockCompleted event can abort the banner. subshell_banner_abort_handle: Option, /// The command which may trigger ssh Warpification pending_command: Option, /// The Host which may trigger ssh Warpification pending_warpify_ssh_host: Option, /// Which, if any, SSH block is currently added to the blocklist. ssh_block_state: Option, ssh_warpify_timeout_handle: Option, shell_type: Option, is_shell_detection_in_progress: bool, tmux_installation: TmuxInstallationState, } #[derive(Default)] pub struct WarpifyState { session_id: Option, pending_state: Option, /// Stores the metadata needed to render any separators above the first block of a subshell. subshell_separator_state: SubshellSeparatorState, /// A unique-enough ID that is used to validate that a timeout is still valid. timeout_id: u8, } impl WarpifyState { pub fn delete_state(&mut self) { self.pending_state.take(); } pub fn is_shell_detection_in_progress(&self) -> bool { self.pending_state .as_ref() .map(|state| state.is_shell_detection_in_progress) .unwrap_or_default() } pub fn set_shell_detection_in_progress(&mut self) { if let Some(ref mut pending_state) = self.pending_state.as_mut() { pending_state.is_shell_detection_in_progress = true; } } pub fn set_shell_type(&mut self, shell_type: &ShellType) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.shell_type = Some(shell_type.to_owned()); pending_state.is_shell_detection_in_progress = false; } pub fn get_shell_type(&self) -> Option { self.pending_state .as_ref() .and_then(|state| state.shell_type) } pub fn add_subshell_separator( &mut self, subshell_info: &SubshellInitializationInfo, terminal_model: Arc>, ctx: &mut ViewContext, ) { let Some(command) = subshell_info.spawning_command.split_whitespace().next() else { return; }; let separator_id = self.subshell_separator_state.next_separator_id(); let appearance = Appearance::as_ref(ctx); let terminal_spacing = TerminalSettings::as_ref(ctx).terminal_spacing(appearance.line_height_ratio(), ctx); let height = terminal_spacing.subshell_separator_height; self.subshell_separator_state .separators .insert(separator_id, command.to_owned()); terminal_model .lock() .block_list_mut() .append_subshell_separator(separator_id, height); ctx.notify(); } pub fn get_subshell_separators(&self) -> &HashMap { &self.subshell_separator_state.separators } pub fn add_subshell_banner_abort_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.subshell_banner_abort_handle = Some(spawned_future_handle); } pub fn take_subshell_banner_abort_handle(&mut self) -> Option { self.pending_state .as_mut() .and_then(|state| state.subshell_banner_abort_handle.take()) } pub fn add_auto_warpify_abort_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.auto_warpify_abort_handle = Some(spawned_future_handle); } pub fn abort_auto_warpify(&mut self) { if let Some(abort_handle) = self .pending_state .as_mut() .and_then(|state| state.auto_warpify_abort_handle.take()) { abort_handle.abort(); }; } pub fn add_ssh_warpify_timeout_handle(&mut self, spawned_future_handle: SpawnedFutureHandle) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.ssh_warpify_timeout_handle = Some(spawned_future_handle); } pub fn abort_ssh_warpify_timeout(&mut self) { self.replace_timeout_id(); if let Some(handle) = self .pending_state .as_mut() .and_then(|state| state.ssh_warpify_timeout_handle.take()) { handle.abort(); }; } pub fn collapse_ssh_block(&mut self, ctx: &mut ViewContext) -> bool { if let Some(ref mut pending_state) = self.pending_state.as_mut() { if let Some(ref mut ssh_block_state) = &mut pending_state.ssh_block_state { return ssh_block_state.collapse_script(ctx); } } false } pub fn focus(&mut self, ctx: &mut ViewContext) { if let Some(ref mut pending_state) = self.pending_state.as_mut() { if let Some(ref mut ssh_block_state) = &mut pending_state.ssh_block_state { ssh_block_state.focus(ctx); } } } pub fn clear_ssh_block_state(&mut self) { if let Some(ref mut pending_state) = self.pending_state.as_mut() { pending_state.ssh_block_state = None; } } pub fn set_ssh_block_state(&mut self, ssh_block_state: SshBlockState) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.ssh_block_state = Some(ssh_block_state); } pub fn ssh_block_state(&self) -> Option<&SshBlockState> { self.pending_state .as_ref() .and_then(|state| state.ssh_block_state.as_ref()) } pub fn get_pending_ssh_host(&self) -> Option { self.pending_state .as_ref() .and_then(|state: &WarpifyTriggerState| state.pending_warpify_ssh_host.clone()) } pub fn get_pending_ssh_command(&self) -> Option { self.pending_state .as_ref() .and_then(|state: &WarpifyTriggerState| state.pending_command.clone()) } pub fn take_pending_ssh_host(&mut self) -> Option { self.pending_state .as_mut() .and_then(|state: &mut WarpifyTriggerState| state.pending_warpify_ssh_host.take()) } pub fn clear_pending_ssh_host(&mut self) { if let Some(ref mut pending_state) = self.pending_state.as_mut() { pending_state.pending_warpify_ssh_host = None; } } pub fn set_pending_ssh_host(&mut self, command: String, ssh_host: Option) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.pending_command = Some(command); pending_state.pending_warpify_ssh_host = ssh_host; } pub fn set_tmux_installation_state(&mut self, tmux_installation: TmuxInstallationState) { if let Some(ref mut pending_state) = self.pending_state.as_mut() { pending_state.tmux_installation = tmux_installation; } } pub fn tmux_installation(&self) -> Option { self.pending_state .as_ref() .map(|state| state.tmux_installation) } pub fn set_block_id(&mut self, block_id: BlockId) { let pending_state = self.pending_state.get_or_insert_with(Default::default); pending_state.block_id = Some(block_id); } pub fn block_id(&self) -> Option { self.pending_state .as_ref() .and_then(|state| state.block_id.clone()) } pub fn timeout_id(&self) -> u8 { self.timeout_id } /// Generates a new timeout ID. This is used to validate that a timeout is still valid. /// Call this to get a new timeout ID before starting a new timeout, or to invalidate /// an existing timeout. pub fn replace_timeout_id(&mut self) -> u8 { self.timeout_id = self.timeout_id.wrapping_add(1); self.timeout_id } /// The terminal view should prevent typing pub fn should_prevent_input(&self) -> bool { let Some(state) = self.ssh_block_state() else { return false; }; state.should_prevent_input() } /// Called once whenever we get a local block completed, as opposed to a remote ssh block /// and we have a Warpify Success block. fn on_warpified_session_complete( &mut self, state: WarpifyTriggerState, ctx: &mut ViewContext, ) -> Option { self.clear_ssh_block_state(); ctx.notify(); let Some(block) = &state.ssh_block_state else { return None; }; block.on_warpified_session_complete(ctx) } pub fn on_warpify_start(&mut self, active_session_id: Option) { self.session_id = active_session_id; } /// Called whenever a block is completed, to determine whether a Warpified session /// has been completed. pub fn get_completed_warpify_session_id( &mut self, active_session_id: Option, ctx: &mut ViewContext, ) -> Option { if self.session_id.is_none() || active_session_id == self.session_id { return None; } if let Some(state) = self.pending_state.take() { return self.on_warpified_session_complete(state, ctx); }; None } }