use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; use std::ffi::OsString; use std::path::PathBuf; use std::rc::Rc; use std::sync::mpsc::{SendError, SyncSender}; use std::sync::Arc; use std::thread::JoinHandle; use anyhow::Context as _; use async_broadcast::InactiveReceiver; use galaxy_core::SessionId; use galaxyui::r#async::executor::Background; use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity, ViewHandle}; #[cfg(unix)] use nix::sys::termios::LocalFlags; use parking_lot::{FairMutex, Mutex}; use pathfinder_geometry::vector::Vector2F; use settings::Setting as _; use super::event_loop::EventLoop; use super::shell::{ShellStarter, ShellStarterSource}; #[cfg(unix)] use super::terminal_attributes::TerminalAttributesPoller; use super::{mio_channel, recorder}; use crate::ai::aws_credentials::AwsCredentialRefresher as _; use crate::ai::blocklist::SerializedBlockListItem; use crate::auth::auth_state::AuthState; use crate::auth::AuthStateProvider; use crate::banner::BannerState; use crate::context_chips::prompt::Prompt; use crate::context_chips::ContextChipKind; use crate::features::FeatureFlag; use crate::persistence::ModelEvent; use crate::send_telemetry_on_executor; use crate::server::telemetry::TelemetryEvent; use crate::settings::{DebugSettings, PrivacySettings, SshSettings}; use crate::terminal::available_shells::{AvailableShell, AvailableShells}; use crate::terminal::color::List as ColorList; use crate::terminal::event_listener::ChannelEventListener; #[cfg(unix)] use crate::terminal::local_tty::terminal_attributes::Event as TerminalAttributesPollerEvent; use crate::terminal::local_tty::{Pty, PtyOptions}; use crate::terminal::model::session::Sessions; #[cfg(unix)] use crate::terminal::model::terminal_model::BlockIndex; use crate::terminal::model::terminal_model::ExitReason; #[cfg(unix)] use crate::terminal::model_events::ModelEvent as TerminalModelEvent; use crate::terminal::model_events::ModelEventDispatcher; use crate::terminal::session_settings::{SessionSettings, ToolbarChipSelection}; use crate::terminal::shared_session::sharer::network::Network; use crate::terminal::shared_session::{IsSharedSessionCreator, SharedSessionStatus}; use crate::terminal::shell::ShellName; use crate::terminal::warpify::settings::WarpifySettings; use crate::terminal::writeable_pty::pty_controller::{EventLoopSendError, EventLoopSender}; use crate::terminal::writeable_pty::terminal_manager_util::{ init_pty_controller_model, init_remote_server_controller, wire_up_pty_controller_with_surface, }; use crate::terminal::writeable_pty::{self, Message, PtyIntentEvent, TerminalSurface}; use crate::terminal::{ terminal_manager, ShellLaunchData, ShellLaunchState, SizeInfo, TerminalManager as TerminalManagerTrait, TerminalModel, PTY_READS_BROADCAST_CHANNEL_SIZE, }; type PtyController = writeable_pty::PtyController>; type RemoteServerController = writeable_pty::remote_server_controller::RemoteServerController>; /// Owns a local terminal session: the terminal model, PTY event loop, PTY /// controller, and a terminal surface. /// /// Holds onto data that needs to live as long as the session does (e.g. the /// event loop join handle). pub struct TerminalManager { event_loop_tx: Arc>>, /// This is an `Option` so that we can take ownership of the inner /// `JoinHandle` in `TerminalManager::drop`. event_loop_handle: Option>, pub(super) model: Arc>, pub(super) view: ViewHandle, /// The manager is responsible for managing the lifetime /// of the terminal attributes poller. None if the event loop has not yet started. #[cfg(unix)] #[allow(dead_code)] terminal_attributes_poller: Option>, /// The manager is responsible for managing the lifetime /// of the PTY controller. pty_controller: ModelHandle, /// The manager is responsible for managing the lifetime of the remote server controller. remote_server_controller: ModelHandle, /// The process ID of the PTY. Purely used for integration tests. None if the PTY has not yet /// been started. #[cfg(feature = "integration_tests")] pub(super) pid: Option, /// An inactive receiver for PTY reads that we can upgrade to an active /// receiver as needed. We prefer to not create active receivers eagerly /// to avoid unnecessary allocations of data coming from the PTY (high throughput). /// Note that we need to hold onto the inactive receiver so that the channel isn't closed prematurely. inactive_pty_reads_rx: InactiveReceiver>>, /// The sharer side of the session sharing protocol. [`Some`] only when a /// shared session connection is ongoing. pub(super) session_sharer: Rc>>>, } /// Shared inputs needed to construct a terminal surface for a local PTY. pub struct TerminalSurfaceInit { pub wakeups_rx: async_channel::Receiver<()>, pub model_events: ModelHandle, pub model: Arc>, pub sessions: ModelHandle, pub size_info: SizeInfo, pub colors: ColorList, pub inactive_pty_reads_rx: InactiveReceiver>>, } /// A newly constructed terminal surface and its manager post-wiring callback. pub struct TerminalSurfaceResult { pub surface: ViewHandle, pub post_wire: PostWire, } /// One-shot resources consumed when the shell is determined and the PTY starts. struct ShellStartupResources { event_loop_rx: mio_channel::Receiver, channel_event_proxy: ChannelEventListener, #[cfg(unix)] model_events: ModelHandle, } /// Handles created for a local terminal manager and its surface. pub struct TerminalManagerInit { pub manager: ModelHandle>, pub surface: ViewHandle, } /// Adapts a TUI-owned surface manager to Warp's type-erased manager contract. struct TuiTerminalManager(TerminalManager); impl TerminalManagerTrait for TuiTerminalManager { fn model(&self) -> Arc> { self.0.model() } fn as_any(&self) -> &dyn Any { &self.0 } fn as_any_mut(&mut self) -> &mut dyn Any { &mut self.0 } } impl Drop for TerminalManager { fn drop(&mut self) { self.shutdown_event_loop(); } } impl TerminalManager { /// Creates a local terminal manager model and terminal surface. #[allow(clippy::too_many_arguments)] pub(crate) fn create_model( startup_directory: Option, env_vars: HashMap, is_shared_session_creator: IsSharedSessionCreator, all_restored_blocks: Option<&Vec>, user_default_shell_unsupported_banner_model_handle: ModelHandle, initial_size: Vector2F, model_event_sender: Option>, chosen_shell: Option, ctx: &mut AppContext, create_surface: impl FnOnce( TerminalSurfaceInit, &mut AppContext, ) -> TerminalSurfaceResult, ) -> TerminalManagerInit where S: TerminalSurface, ::Event: PtyIntentEvent, Self: TerminalManagerTrait, PostWire: FnOnce(&mut Self, &ViewHandle, &mut AppContext), { Self::create_model_with_manager( startup_directory, env_vars, is_shared_session_creator, all_restored_blocks, user_default_shell_unsupported_banner_model_handle, initial_size, model_event_sender, chosen_shell, ctx, create_surface, |manager| Box::new(manager), ) } /// Creates a local terminal manager for a TUI-owned terminal surface. #[allow(clippy::too_many_arguments)] pub fn create_tui_model( startup_directory: Option, env_vars: HashMap, is_shared_session_creator: IsSharedSessionCreator, all_restored_blocks: Option<&Vec>, user_default_shell_unsupported_banner_model_handle: ModelHandle, initial_size: Vector2F, model_event_sender: Option>, chosen_shell: Option, ctx: &mut AppContext, create_surface: impl FnOnce( TerminalSurfaceInit, &mut AppContext, ) -> TerminalSurfaceResult, ) -> TerminalManagerInit where S: TerminalSurface, ::Event: PtyIntentEvent, PostWire: FnOnce(&mut Self, &ViewHandle, &mut AppContext), { Self::create_model_with_manager( startup_directory, env_vars, is_shared_session_creator, all_restored_blocks, user_default_shell_unsupported_banner_model_handle, initial_size, model_event_sender, chosen_shell, ctx, create_surface, |manager| Box::new(TuiTerminalManager(manager)), ) } /// Creates a manager using the supplied type-erasure adapter. #[allow(clippy::too_many_arguments)] fn create_model_with_manager( startup_directory: Option, env_vars: HashMap, is_shared_session_creator: IsSharedSessionCreator, all_restored_blocks: Option<&Vec>, user_default_shell_unsupported_banner_model_handle: ModelHandle, initial_size: Vector2F, model_event_sender: Option>, chosen_shell: Option, ctx: &mut AppContext, create_surface: impl FnOnce( TerminalSurfaceInit, &mut AppContext, ) -> TerminalSurfaceResult, box_manager: BoxManager, ) -> TerminalManagerInit where S: TerminalSurface, ::Event: PtyIntentEvent, PostWire: FnOnce(&mut Self, &ViewHandle, &mut AppContext), BoxManager: FnOnce(Self) -> Box + 'static, { let (wakeups_tx, wakeups_rx) = async_channel::unbounded(); let (events_tx, events_rx) = async_channel::unbounded(); let (executor_command_tx, executor_command_rx) = async_channel::unbounded(); let (event_loop_tx, event_loop_rx) = mio_channel::channel(); // Create the broadcast channel to receive data from the PTY, but deactivate it immediately. // We only want to create active receivers as necessary. let (pty_reads_tx, pty_reads_rx) = async_broadcast::broadcast(PTY_READS_BROADCAST_CHANNEL_SIZE); let inactive_pty_reads_rx = pty_reads_rx.deactivate(); let channel_event_proxy = ChannelEventListener::new(wakeups_tx, events_tx, pty_reads_tx); // Initialize the sessions model. let sessions = ctx.add_model(|ctx| Sessions::new(executor_command_tx.clone(), ctx)); let model_events = ctx.add_model(|ctx| ModelEventDispatcher::new(events_rx, sessions.clone(), ctx)); // Have ApiKeyManager subscribe to block completion events for AWS credential refresh ai::api_keys::ApiKeyManager::handle(ctx).update(ctx, |manager, ctx| { manager.register_model_event_dispatcher(&model_events, ctx); }); let preferred_shell = chosen_shell.unwrap_or_else(|| { AvailableShells::handle(ctx) .read(ctx, |shells, ctx| shells.get_user_preferred_shell(ctx)) }); let wsl_name_or_shell_starter = ShellStarter::init(preferred_shell.clone()); // Create the terminal model with all restored blocks log::info!( "Creating terminal model with {} restored blocks", all_restored_blocks .as_ref() .map(|blocks| blocks.len()) .unwrap_or(0) ); let model = terminal_manager::create_terminal_model( startup_directory.clone(), all_restored_blocks, initial_size, channel_event_proxy.clone(), ShellLaunchState::DeterminingShell { available_shell: Some(preferred_shell), display_name: wsl_name_or_shell_starter .as_ref() .map(|wsl_name_or_shell_starter| wsl_name_or_shell_starter.name()) .unwrap_or(ShellName::LessDescriptive("Shell".to_owned())), }, ctx, ); let colors = model.colors(); let model = Arc::new(FairMutex::new(model)); // This is purely for measuring throughput on WarpDev. if FeatureFlag::RecordPtyThroughput.is_enabled() { let auth_state = AuthStateProvider::as_ref(ctx).get(); recorder::record_pty_throughput( inactive_pty_reads_rx.clone().activate(), model.clone(), auth_state.clone(), ctx.background_executor().to_owned(), ); } // If this session should be a shared-session creator, configure its initial // shared-session state before the surface is constructed, so that bootstrap // events can observe the correct pending status and source type. match is_shared_session_creator { IsSharedSessionCreator::Yes { source } if FeatureFlag::CreatingSharedSessions.is_enabled() => { model.lock().set_shared_session_status( SharedSessionStatus::SharePendingPreBootstrap { source }, ); log::info!("Configured terminal to start sharing after bootstrap"); } IsSharedSessionCreator::Yes { .. } => { log::warn!( "Session sharing was requested, but CreatingSharedSessions is disabled; \ skipping shared-session startup" ); } IsSharedSessionCreator::No => {} } // Initialize the PtyController. let pty_controller = init_pty_controller_model( event_loop_tx.clone(), executor_command_rx, model_events.clone(), sessions.clone(), model.clone(), ctx, ); // Initialize the RemoteServerController. let remote_server_controller = init_remote_server_controller(&pty_controller, &model_events, ctx); let size_info = model.lock().block_list().size().to_owned(); let TerminalSurfaceResult { surface, post_wire } = create_surface( TerminalSurfaceInit { wakeups_rx, model_events: model_events.clone(), model: model.clone(), sessions: sessions.clone(), size_info, colors, inactive_pty_reads_rx: inactive_pty_reads_rx.clone(), }, ctx, ); wire_up_pty_controller_with_surface( &pty_controller, &surface, model.clone(), sessions.clone(), model_event_sender, ctx, ); let mut terminal_manager = Self { event_loop_tx: Arc::new(Mutex::new(event_loop_tx)), model, event_loop_handle: None, view: surface.clone(), #[cfg(unix)] terminal_attributes_poller: None, pty_controller, remote_server_controller, #[cfg(feature = "integration_tests")] pid: None, inactive_pty_reads_rx, session_sharer: Rc::new(RefCell::new(None)), }; // Run surface-specific wiring after the manager exists, because this // step may need manager-owned controllers and retained handles. post_wire(&mut terminal_manager, &surface, ctx); let terminal_surface = surface.clone(); let shell_startup_resources = ShellStartupResources { event_loop_rx, channel_event_proxy, #[cfg(unix)] model_events, }; let terminal_manager_model = ctx.add_model(|ctx| { let terminal_manager = box_manager(terminal_manager); ctx.spawn( async move { match wsl_name_or_shell_starter { Some(starter_source) => starter_source.to_shell_starter_source().await, None => None, } }, move |terminal_manager: &mut Box, shell_starter_source, ctx| { let Some(terminal_manager) = TerminalManagerTrait::as_any_mut(terminal_manager.as_mut()) .downcast_mut::() else { return; }; on_shell_determined( terminal_manager, startup_directory, env_vars, user_default_shell_unsupported_banner_model_handle, shell_startup_resources, shell_starter_source, ctx, ) }, ); terminal_manager }); TerminalManagerInit { manager: terminal_manager_model, surface: terminal_surface, } } /// Returns the terminal model owned by this manager. pub(crate) fn model(&self) -> Arc> { self.model.clone() } /// Returns the remote server controller owned by this manager. pub(super) fn remote_server_controller(&self) -> ModelHandle { self.remote_server_controller.clone() } /// Sends a shutdown message to the PTY event loop and waits for it to /// process that event. pub(super) fn shutdown_event_loop(&mut self) { let shutdown_res = self.event_loop_tx.lock().send(Message::Shutdown); // Happens normally if the event loop has already been terminated (so the channel is now gone). if let Err(e) = shutdown_res { log::info!("Failed to send Shutdown {e:?}"); } if let Some(join_handle) = self.event_loop_handle.take() { if let Err(e) = join_handle.join() { log::error!("Failed to join event loop handle {e:?}"); } } else { log::error!("No event loop handle to join when dropping terminal manager.") } self.inactive_pty_reads_rx.close(); } } /// Callback invoked upon determining the shell to be spawned when starting the event loop. #[allow(clippy::too_many_arguments)] fn on_shell_determined( manager: &mut TerminalManager, startup_directory: Option, env_vars: HashMap, user_default_shell_unsupported_banner_model_handle: ModelHandle, shell_startup_resources: ShellStartupResources, shell_starter_source: Option, ctx: &mut ModelContext>, ) where ::Event: PtyIntentEvent, { // This is executed as a callback and the window could be closed in the interim. if !ctx.is_window_open(manager.view.window_id(ctx)) { log::warn!("Window was closed before shell was determined, aborting shell startup."); return; } log::debug!("Using shell starter source {shell_starter_source:?}"); let bg_executor = ctx.background_executor(); let auth_state = AuthStateProvider::as_ref(ctx).get(); let is_fallback_shell = matches!( shell_starter_source, Some(ShellStarterSource::Fallback { .. }) ); let shell_starter = shell_starter_source .map(|source| get_shell_starter_internal(source, bg_executor, auth_state)); let shell_starter = match shell_starter { Some(shell_starter) => shell_starter, None => { log::error!("Could not compute fallback shell"); manager.view.update(ctx, |surface, ctx| { surface.on_pty_spawn_failed( anyhow::Error::msg("Could not find a fallback shell. If you have PowerShell or WSL installed, please file an issue."), ctx, ); }); manager.model().lock().exit(ExitReason::ShellNotFound); return; } }; // In WSL, default to the WSL home directory, not the native Windows home directory. let startup_directory = if let (ShellStarter::Wsl(wsl_shell_starter), None) = (&shell_starter, &startup_directory) { wsl_shell_starter.home_directory() } else { startup_directory }; // Show a "shell unsupported" banner, if applicable. if is_fallback_shell && user_default_shell_unsupported_banner_model_handle.as_ref(ctx) == &BannerState::NotDismissed { user_default_shell_unsupported_banner_model_handle.update(ctx, |model, ctx| { *model = BannerState::Open; ctx.notify(); }) } manager .model() .lock() .set_login_shell_spawned(shell_starter.shell_type()); let shell_launch_data = match &shell_starter { ShellStarter::Direct(shell_starter) => ShellLaunchData::Executable { executable_path: shell_starter.logical_shell_path().to_owned(), shell_type: shell_starter.shell_type(), }, ShellStarter::DockerSandbox(docker_starter) => ShellLaunchData::Executable { executable_path: docker_starter.logical_shell_path().to_owned(), shell_type: docker_starter.shell_type(), }, ShellStarter::Wsl(shell_starter) => ShellLaunchData::WSL { distro: shell_starter.distribution().to_owned(), }, ShellStarter::MSYS2(shell_starter) => ShellLaunchData::MSYS2 { executable_path: shell_starter.logical_shell_path().to_owned(), shell_type: shell_starter.shell_type(), }, }; // This needs to be done before bootstrapping starts (i.e. before spawning the event loop below). manager .model() .lock() .set_pending_shell_launch_data(shell_launch_data.clone()); // Register the session ID that was generated during shell starter construction. // For bash, fish, and PowerShell, the session ID is already baked into the command // args. For zsh and MSYS2, enqueue_init_script injects this same ID. let generated_session_id = match &shell_starter { ShellStarter::Direct(starter) | ShellStarter::MSYS2(starter) => starter.session_id(), ShellStarter::DockerSandbox(starter) => starter.session_id(), ShellStarter::Wsl(starter) => starter.session_id(), }; manager .model() .lock() .register_session_id(generated_session_id); // Enqueue the init shell script (for shells that need it), then create // the PTY and start its corresponding event loop. let ShellStartupResources { event_loop_rx, channel_event_proxy, #[cfg(unix)] model_events, } = shell_startup_resources; let model = manager.model(); #[cfg(windows)] let event_loop_tx = manager.event_loop_tx.lock().clone(); let pty = match manager .enqueue_init_script(&shell_starter, generated_session_id) .context("Failed to write shell init script to the pty") .and_then(|_| { TerminalManager::::create_pty( startup_directory, shell_starter, env_vars, model.clone(), #[cfg(windows)] event_loop_tx, ctx, ) }) { Ok(pty) => pty, Err(err) => { log::error!("Failed to spawn pty: {err:#}"); manager.view.update(ctx, |surface, ctx| { surface.on_pty_spawn_failed(err, ctx); }); manager.model().lock().exit(ExitReason::PtySpawnFailed); return; } }; #[cfg(feature = "integration_tests")] let pid = pty.get_pid(); #[cfg(unix)] let fd = pty.get_fd(); // Create the channel above and pass the receving side to the event loop. let event_loop_handle = TerminalManager::::start_pty_event_loop( pty, event_loop_rx, model.clone(), channel_event_proxy, ); manager.event_loop_handle = Some(event_loop_handle); #[cfg(feature = "integration_tests")] { manager.pid = Some(pid); } manager.view.update(ctx, |surface, ctx| { surface.on_shell_determined(ctx); surface.on_active_shell_launch_data_updated(Some(shell_launch_data), ctx); }); // Initialize the terminal attributes poller. // TODO(CORE-2297): Implement TerminalPoller on Windows. #[cfg(unix)] { let terminal_attributes_poller = ctx.add_model(|_| TerminalAttributesPoller::new(fd)); wire_up_terminal_attribute_poller_with_surface( &terminal_attributes_poller, &manager.view, &model_events, model.clone(), ctx, ); manager.terminal_attributes_poller = Some(terminal_attributes_poller); } } impl TerminalManager { /// Sends bindkey to notify shell process to switch to PS1 logic for prompt /// with the combined prompt/command grid (we restore the saved PS1 value). pub fn send_switch_to_ps1_bindkey(&self, app_ctx: &mut AppContext) { self.pty_controller.update(app_ctx, |pty_controller, ctx| { pty_controller.send_switch_to_ps1_bindkey(ctx); }); } /// Sends bindkey to notify shell process to switch to Warp prompt logic for prompt /// with the combined prompt/command grid (we unset the PS1, but save the value for potential /// future restoration). pub fn send_switch_to_warp_prompt_bindkey(&self, app_ctx: &mut AppContext) { self.pty_controller.update(app_ctx, |pty_controller, ctx| { pty_controller.send_switch_to_warp_prompt_bindkey(ctx); }); } fn enqueue_init_script( &self, shell_starter: &ShellStarter, session_id: SessionId, ) -> Result<(), SendError> { let shell_type = shell_starter.shell_type(); if shell_type == crate::terminal::shell::ShellType::Zsh // For more on why this is necessary on Git Bash, see https://linear.app/warpdotdev/issue/CORE-3202. || shell_starter.is_msys2() { let init_shell_script = crate::terminal::bootstrap::init_shell_script_for_shell( shell_type, &crate::ASSETS, session_id, ); let tx = self.event_loop_tx.lock(); tx.send(Message::Input(init_shell_script.into_bytes().into()))?; tx.send(Message::Input(shell_type.execute_command_bytes().into())) } else { Ok(()) } } fn create_pty( startup_directory: Option, shell_starter: ShellStarter, env_vars: HashMap, model: Arc>, #[cfg(windows)] event_loop_tx: mio_channel::Sender, ctx: &mut AppContext, ) -> anyhow::Result { let is_shell_debug_mode_enabled = *DebugSettings::as_ref(ctx) .is_shell_debug_mode_enabled .value(); let is_honor_ps1_enabled = *SessionSettings::as_ref(ctx).honor_ps1; let is_crash_reporting_enabled = PrivacySettings::as_ref(ctx).is_crash_reporting_enabled; // Determine whether the Node.js Version chip is enabled anywhere it could be // shown (the Warp prompt, the agent footer, or the CLI agent footer). When it // is not, the shell bootstrap skips the expensive per-prompt `node --version` // detection. The chip value is fed by the same precmd payload regardless of // where it is displayed, so we must check all three locations. let node_version_chip_enabled = { let in_prompt = !is_honor_ps1_enabled && Prompt::as_ref(ctx) .chip_kinds() .contains(&ContextChipKind::NodeVersion); let settings = SessionSettings::as_ref(ctx); in_prompt || settings .agent_footer_chip_selection .all_chips() .contains(&ContextChipKind::NodeVersion) || settings .cli_agent_footer_chip_selection .all_chips() .contains(&ContextChipKind::NodeVersion) }; // `enable_ssh_warpification` is the single source of truth for whether the SSH // wrapper is active. The bootstrap scripts check `WARP_USE_SSH_WRAPPER` (derived // from this value) before invoking `warp_ssh_helper`, which spawns the ControlMaster // and opens agent-protocol channels. let enable_ssh_wrapper = *WarpifySettings::as_ref(ctx) .enable_ssh_warpification .value(); // Only meaningful when the legacy ControlMaster wrapper is active. let reuse_ssh_control_master = enable_ssh_wrapper && *SshSettings::as_ref(ctx) .reuse_existing_control_master .value(); let size: SizeInfo = model.lock().block_list().size().to_owned(); let options = PtyOptions { size, window_id: None, shell_starter, start_dir: startup_directory, env_vars, enable_ssh_wrapper, reuse_ssh_control_master, shell_debug_mode: is_shell_debug_mode_enabled, honor_ps1: is_honor_ps1_enabled, node_version_chip_enabled, close_fds: true, }; Pty::new( options, is_crash_reporting_enabled, #[cfg(windows)] event_loop_tx, ctx, ) } /// Start's the PTY event loop, returning a sender for the event loop and the event loop's join handle. fn start_pty_event_loop( pty: Pty, rx: mio_channel::Receiver, model: Arc>, channel_event_proxy: ChannelEventListener, ) -> JoinHandle<()> { // Create the event loop and get a handle to the injector. let event_loop = EventLoop::new(model, channel_event_proxy, pty, rx); // Spawn the event loop on a separate thread to interact with the PTY and write the data back // to the terminal. event_loop.spawn() } } /// Wires the Unix terminal-attributes poller to a terminal surface. /// /// The poller is driven off `ModelEventDispatcher` block start/complete events /// rather than surface-specific view events, and termios results are routed back /// through the [`TerminalSurface`] Unix hooks so the manager stays surface-agnostic. /// /// NOTE: we cannot simply use the strong references (the handle arguments to this /// wire_up fn) in the subscription callbacks because that will create a reference /// cycle. Instead, we use weak handles and upgrade them lazily. #[cfg(unix)] fn wire_up_terminal_attribute_poller_with_surface( terminal_attributes_poller: &ModelHandle, surface: &ViewHandle, model_events: &ModelHandle, model: Arc>, ctx: &mut ModelContext>, ) where ::Event: PtyIntentEvent, { let poller_weak_handle = terminal_attributes_poller.downgrade(); let poller_weak_handle_for_termios = poller_weak_handle.clone(); let surface_weak_handle = surface.downgrade(); let surface_weak_handle_for_termios = surface_weak_handle.clone(); // Tracks the index of the started block across the model-event <-> poller interactions. let block_index: Rc>> = Rc::new(RefCell::new(None)); let block_index_for_termios = block_index.clone(); // On block start, record the active block index and ask the surface whether // password-prompt polling is useful before starting the poller. On block // completion, stop polling and let the surface react to the completed block. ctx.subscribe_to_model(model_events, move |_manager, _dispatcher, event, ctx| { let Some(poller) = poller_weak_handle.upgrade(ctx) else { return; }; match event { TerminalModelEvent::AfterBlockStarted { command, is_for_in_band_command: false, .. } => { let should_poll = surface_weak_handle.upgrade(ctx).is_some_and(|surface| { surface.read(ctx, |surface, ctx| { surface.should_start_password_prompt_polling(command, ctx) }) }); if should_poll { *block_index.borrow_mut() = Some(model.lock().block_list().active_block_index()); poller.update(ctx, |poller, ctx| { poller.start_polling(ctx); }); } else { *block_index.borrow_mut() = None; } } TerminalModelEvent::AfterBlockCompleted(completed) => { if let Some(surface) = surface_weak_handle.upgrade(ctx) { let should_stop = surface.read(ctx, |surface, _ctx| { surface.should_stop_password_prompt_polling(completed) }); if should_stop { poller.update(ctx, |poller, _ctx| { poller.stop_polling(); }); surface.update(ctx, |surface, ctx| { surface.on_polled_block_completed(completed, ctx); }); } } } _ => {} } }); // When the poller detects termios consistent with a password prompt, route the // result through the surface and stop after the first detection to preserve // today's one-notification-per-command behavior. ctx.subscribe_to_model( terminal_attributes_poller, move |_manager, _poller, event, ctx| { let TerminalAttributesPollerEvent::TermiosQueryFinished { termios } = event; // A PTY likely has a password prompt if ECHO is disabled but ICANON is // still enabled. Apps like neovim disable both in raw mode, so requiring // ICANON avoids false positives. let might_be_password_prompt = !termios.local_flags.contains(LocalFlags::ECHO) && termios.local_flags.contains(LocalFlags::ICANON); if !might_be_password_prompt { return; } let Some(surface) = surface_weak_handle_for_termios.upgrade(ctx) else { return; }; let block_index = block_index_for_termios.borrow_mut().take(); surface.update(ctx, |surface, ctx| { surface.on_possible_password_prompt(block_index, ctx); }); if let Some(poller) = poller_weak_handle_for_termios.upgrade(ctx) { poller.update(ctx, |poller, _ctx| { poller.stop_polling(); }); } }, ); } pub fn get_shell_starter( chosen_shell: Option, auth_state: &AuthState, ctx: &mut AppContext, ) -> Option { let preferred_shell = chosen_shell.unwrap_or_else(|| { AvailableShells::handle(ctx).read(ctx, |shells, ctx| shells.get_user_preferred_shell(ctx)) }); let shell_starter_or_wsl_name = ShellStarter::init(preferred_shell); // TODO(alokedesai): Further refactor this function to make it clear that it's expensive. shell_starter_or_wsl_name .and_then(|starter| { galaxyui::r#async::block_on(async { starter.to_shell_starter_source().await }) }) .map(|starter_source| { get_shell_starter_internal( starter_source, ctx.background_executor().clone(), auth_state, ) }) } fn get_shell_starter_internal( shell_starter_source: ShellStarterSource, background_executor: Arc, auth_state: &AuthState, ) -> ShellStarter { match shell_starter_source { ShellStarterSource::Override(shell_starter) => shell_starter, ShellStarterSource::Environment(starter) | ShellStarterSource::UserDefault(starter) => { ShellStarter::Direct(starter) } ShellStarterSource::Fallback { unsupported_shell, starter, } => { if let Some(unsupported_shell) = unsupported_shell { send_telemetry_on_executor!( auth_state, TelemetryEvent::UnsupportedShell { shell: unsupported_shell }, background_executor ); } ShellStarter::Direct(starter) } } } impl EventLoopSender for mio_channel::Sender { fn send(&self, message: Message) -> Result<(), EventLoopSendError> { self.send(message).map_err(|error| match error { SendError(_) => EventLoopSendError::Disconnected, }) } }