//! Primary terminal IO ("termio") state. This maintains the terminal state, //! pty, subprocess, etc. This is flexible enough to be used in environments //! that don't have a pty and simply provides the input/output using raw //! bytes (`termio/Termio.zig`). //! //! # Ownership //! //! Zig has one `Termio` struct touched by the IO thread (messages), the pty //! read thread (`processOutput`) and the surface thread (via the mailbox). In //! Rust it is split along the thread boundaries: //! //! * [`Termio`] is owned by the IO thread (`Thread::thread_main` takes it by //! value) and holds the backend, derived config and cached size. //! * [`Output`] (an `Arc`, reachable through [`Termio::output`]) is the part //! the pty read thread needs: the shared [`SharedState`], the VT stream and //! the renderer/termio notification handles. `Termio::process_output` is //! just `output.process_output`. //! * The `Terminal` itself lives in [`SharedState`] //! (`Arc>`), shared with the renderer like Zig's //! `renderer.State`. //! //! Lock order is always terminal mutex first, then the stream mutex inside //! [`Output`]. use std::{ io::Read, sync::{Arc, Mutex, MutexGuard, PoisonError}, time::{Duration, Instant}, }; use ghostty_config::{ conditional, config::Config, io::ReadableIO, types_a::{Color, CursorStyle, TerminalColor}, types_b::{ClipboardAccess, GraphemeWidthMethod, OSCColorReportFormat}, }; use ghostty_datastruct::blocking_queue::Timeout; use ghostty_renderer::{message::Message as RendererMessage, size::Size, state::InspectorHook}; use ghostty_terminal::{ kitty::graphics_storage::TerminalInfo, point::{Coordinate, Point}, screen::Scroll, screen_set::Key as ScreenKey, terminal::{Colors, Options as TerminalOptions, ScrollViewport, Terminal}, }; use ghostty_vt::{ color::{DynamicPalette, DynamicRgb, Palette}, csi, kitty::graphics_command::Delete as KittyDelete, modes::{Mode, ModePacked}, }; use crate::{ backend::{self, Backend}, exec, mailbox::Mailbox, message::{Message, SizeReport}, options::Options, sinks::{RendererMailbox, SurfaceMailbox}, state::{SharedState, SharedStateExt, State}, stream_handler::{HandlerOptions, HandlerState, TerminalStream}, wakeup::Wakeup, }; /// Errors from termio initialization and thread setup. #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Exec(#[from] exec::Error), #[error("terminal error: {0:?}")] Terminal(ghostty_terminal::terminal::Error), #[error("a configured `input` path was not found")] InputNotFound, #[error("a configured `input` could not be read or written")] InputFailed, #[error("io error: {0}")] Io(#[from] std::io::Error), } impl From for Error { fn from(e: ghostty_terminal::terminal::Error) -> Self { Error::Terminal(e) } } /// Mutex state argument for [`Termio::queue_message`] callers that hold the /// terminal lock (`MutexState`). In Rust the lock is a guard, so this is /// expressed with `Mailbox::send_locked` instead; the enum is kept for API /// parity. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MutexState { Locked, Unlocked, } /// The configuration for this IO that is derived from the main /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. #[derive(Debug, Clone)] pub struct DerivedConfig { pub palette: Palette, pub image_storage_limit: usize, pub cursor_style: CursorStyle, pub cursor_blink: Option, pub cursor_color: Option, pub foreground: Color, pub background: Color, pub osc_color_report_format: OSCColorReportFormat, pub clipboard_write: ClipboardAccess, pub enquiry_response: String, pub conditional_state: conditional::State, } impl DerivedConfig { pub fn new(config: &Config) -> DerivedConfig { let palette: Palette = 'palette: { if config.palette_generate && config.palette.mask.count() > 0 { // If the user didn't set any values manually, then // we're using the default palette and we don't need // to apply the generation code to it. (Handled by the // count check above.) break 'palette ghostty_vt::color::generate_256_color( config.palette.value, config.palette.mask, config.background.to_terminal_rgb(), config.foreground.to_terminal_rgb(), config.palette_harmonious, ); } config.palette.value }; DerivedConfig { palette, image_storage_limit: config.image_storage_limit as usize, cursor_style: config.cursor_style, cursor_blink: config.cursor_style_blink, cursor_color: config.cursor_color, foreground: config.foreground, background: config.background, osc_color_report_format: config.osc_color_report_format, clipboard_write: config.clipboard_write, enquiry_response: config.enquiry_response.clone(), conditional_state: config.conditional_state, } } } /// The state we need to keep around only until we enter the IO /// thread. Then we can throw it all away. struct ThreadEnterState { /// Initial input to send to the subprocess after starting. input: Vec, } impl ThreadEnterState { fn create(config: &Config) -> Option { // If we have no input then we have no thread enter state if config.input.list.is_empty() { return None; } Some(ThreadEnterState { input: config.input.clone_parsed().list, }) } /// Prepare the inputs for use: raw strings are used as is, files are read /// (up to 10 MiB each). fn prepare_input(&self) -> Result>, Error> { let mut out = Vec::with_capacity(self.input.len()); for item in &self.input { match item { ReadableIO::Raw(v) => out.push(v.clone().into_bytes()), ReadableIO::Path(path) => { let f = std::fs::File::open(path).map_err(|err| { log::warn!("failed to open input file={path} err={err}"); Error::InputNotFound })?; let mut buf = Vec::new(); // 10 MiB max let limit = 10 * 1024 * 1024; f.take(limit + 1).read_to_end(&mut buf).map_err(|err| { log::warn!("failed to read input file err={err}"); Error::InputFailed })?; if buf.len() as u64 > limit { log::warn!("failed to read input file err=StreamTooLong"); return Err(Error::InputFailed); } out.push(buf); } } } Ok(out) } } /// What the stream needs mutual exclusion for (under the terminal lock). struct OutputInner { stream: TerminalStream, /// Last time the cursor was reset. This is used to prevent message /// flooding with cursor resets. last_cursor_reset: Option, } /// The part of termio used from the pty read thread: parses the pty output /// and applies it to the shared terminal. pub struct Output { state: SharedState, inner: Mutex, renderer_mailbox: Arc, mailbox: Mailbox, surface_mailbox: Arc, } impl Output { fn inner(&self) -> MutexGuard<'_, OutputInner> { self.inner.lock().unwrap_or_else(PoisonError::into_inner) } /// Process output from the pty. This is the manual API that users can /// call with pty data but it is also called by the read thread when using /// an exec subprocess. pub fn process_output(&self, buf: &[u8]) { // We are modifying terminal state from here on out and we need // the lock to grab our read data. let outbox = { let mut term = self.state.lock_terminal(); let inspector = term.state().inspector.clone(); let mut inner = self.inner(); self.process_output_locked(&mut term, &mut inner, buf, inspector.as_deref()); // If our stream handling caused messages to be sent to the // mailbox thread, then we need to wake it up so that it // processes them. if inner.stream.state.take_termio_messaged() { self.mailbox.notify(); } inner.stream.state.take_outbox() }; // Anything that didn't fit in a full mailbox is delivered now that // the locks are released. if !outbox.is_empty() { outbox.deliver(&self.mailbox, &*self.surface_mailbox); } } /// Process output from the pty but the lock is already held. fn process_output_locked( &self, term: &mut Terminal, inner: &mut OutputInner, buf: &[u8], inspector: Option<&dyn InspectorHook>, ) { // Schedule a render. We can call this first because we have the lock. inner.stream.state.queue_render(); // Whenever a character is typed, we ensure the cursor is in the // non-blink state so it is rendered if visible. If we're under // HEAVY read load, we don't want to send a ton of these so we // use a timer under the covers let now = Instant::now(); let reset = match inner.last_cursor_reset { Some(last) => now.duration_since(last) > Duration::from_millis(500), None => true, }; if reset { inner.last_cursor_reset = Some(now); self.renderer_mailbox .push(RendererMessage::ResetCursorBlink, Timeout::Instant); } // If we have an inspector, we enter SLOW MODE because we need to // process a byte at a time alternating between the inspector handler // and the termio handler. This is very slow compared to our // optimizations below but at least users only pay for it if they're // using the inspector. if let Some(insp) = inspector { for &byte in buf { insp.record_pty_read(term, &[byte]); if let Err(err) = inner.stream.process_byte(term, byte) { log::error!("error processing terminal data: {err:?}"); } } } else if let Err(err) = inner.stream.process(term, buf) { log::error!("error processing terminal data: {err:?}"); } } } /// See the module docs. pub struct Termio { /// This is the implementation responsible for io. backend: Backend, /// The derived configuration for this termio implementation. config: DerivedConfig, /// The shared render state (contains the terminal). renderer_state: SharedState, /// The part shared with the pty read thread. output: Arc, /// A handle to wake up the renderer. This hints to the renderer that that /// a repaint should happen. renderer_wakeup: Wakeup, /// The mailbox for notifying the renderer of things. renderer_mailbox: Arc, /// The mailbox for communicating with the surface. surface_mailbox: Arc, /// The cached size info size: Size, /// The mailbox implementation to use. mailbox: Mailbox, /// State we have for thread enter. This may be None if we don't need /// to keep track of any state or if its already been freed. thread_enter_state: Option, } /// ThreadData is the data created and stored in the termio thread /// when the thread is started and destroyed when the thread is /// stopped. /// /// All of the fields in this struct should only be read/written by /// the termio thread. As such, a lock is not necessary. pub struct ThreadData { /// The shared render state pub renderer_state: SharedState, /// Mailboxes for different threads pub surface_mailbox: Arc, /// Data associated with the backend implementation (i.e. pty/exec state) pub backend: backend::ThreadData, pub mailbox: Mailbox, } impl Termio { /// Initialize the termio state. /// /// This does NOT start the child process: that happens in /// [`Termio::thread_enter`] on the IO thread. pub fn init(opts: Options<'_>) -> Result { let Options { size, full_config, config, mut backend, mailbox, renderer_wakeup, renderer_mailbox, surface_mailbox, } = opts; // The default terminal modes based on our config. let default_modes: ModePacked = { let mut modes = ModePacked::default(); // Setup our initial grapheme cluster support if enabled. match full_config.grapheme_width_method { GraphemeWidthMethod::Unicode => modes.set(Mode::GraphemeCluster, true), GraphemeWidthMethod::Legacy => {} } // Set default cursor blink settings modes.set(Mode::CursorBlinking, config.cursor_blink.unwrap_or(true)); modes }; // Create our terminal let grid_size = size.grid(); let mut term = Terminal::new(TerminalOptions { cols: grid_size.columns, rows: grid_size.rows, max_scrollback: full_config.scrollback_limit, default_modes, colors: Colors { background: DynamicRgb::init(config.background.to_terminal_rgb()), foreground: DynamicRgb::init(config.foreground.to_terminal_rgb()), cursor: config .cursor_color .and_then(TerminalColor::to_terminal_rgb) .map_or(DynamicRgb::UNSET, DynamicRgb::init), palette: DynamicPalette::init(config.palette), }, })?; // Set the image size limits set_image_limits(&mut term, config.image_storage_limit); // Set our default cursor style term.screen_mut().cursor.cursor_style = config.cursor_style; // Setup our terminal size in pixels for certain requests. term.width_px = u32::from(term.cols) * size.cell.width; term.height_px = u32::from(term.rows) * size.cell.height; // Setup our backend. backend.init_terminal(&mut term); // Create our stream handler. let handler = HandlerState::new(HandlerOptions { size, termio_mailbox: mailbox.clone(), surface_mailbox: Arc::clone(&surface_mailbox), renderer_wakeup: renderer_wakeup.clone(), default_cursor_style: config.cursor_style, default_cursor_blink: config.cursor_blink, enquiry_response: config.enquiry_response.clone().into_bytes(), osc_color_report_format: config.osc_color_report_format, clipboard_write: config.clipboard_write, }); let thread_enter_state = ThreadEnterState::create(full_config); let renderer_state = State::new(term).into_shared(); let output = Arc::new(Output { state: Arc::clone(&renderer_state), inner: Mutex::new(OutputInner { stream: TerminalStream::new(handler), last_cursor_reset: None, }), renderer_mailbox: Arc::clone(&renderer_mailbox), mailbox: mailbox.clone(), surface_mailbox: Arc::clone(&surface_mailbox), }); Ok(Termio { backend, config, renderer_state, output, renderer_wakeup, renderer_mailbox, surface_mailbox, size, mailbox, thread_enter_state, }) } /// The shared render state (it holds the terminal). Hand this to the /// renderer. pub fn renderer_state(&self) -> &SharedState { &self.renderer_state } /// The termio mailbox (clone it to send messages to the IO thread). pub fn mailbox(&self) -> &Mailbox { &self.mailbox } /// The part of termio shared with the pty read thread; also the manual /// API to feed output (`process_output`). pub fn output(&self) -> &Arc { &self.output } /// The surface mailbox. pub fn surface_mailbox(&self) -> &Arc { &self.surface_mailbox } /// The derived config currently in effect. pub fn config(&self) -> &DerivedConfig { &self.config } /// The current size. pub fn size(&self) -> Size { self.size } /// The renderer wakeup handle. pub fn renderer_wakeup(&self) -> &Wakeup { &self.renderer_wakeup } pub fn thread_enter(&mut self) -> Result { // Always free our thread enter state when we're done. let enter_state = self.thread_enter_state.take(); // If we have thread enter state then we're going to validate // and set that all up now so that we can error before we actually // start the command and pty. let inputs: Vec> = match &enter_state { Some(v) => v.prepare_input()?, None => Vec::new(), }; // Setup our backend let backend = self.backend.thread_enter( Arc::clone(&self.output), Arc::clone(&self.surface_mailbox), Arc::clone(&self.renderer_state), )?; let mut data = ThreadData { renderer_state: Arc::clone(&self.renderer_state), surface_mailbox: Arc::clone(&self.surface_mailbox), backend, mailbox: self.mailbox.clone(), }; // If we have inputs, then queue them all up. for input in inputs { self.queue_write(&mut data, &input, false); } Ok(data) } pub fn thread_exit(&mut self, data: &mut ThreadData) { self.backend.thread_exit(&mut data.backend); } /// Send a message to the the mailbox and notify the IO thread. /// /// Do not call this while holding the terminal lock if the mailbox may /// be full; use `Mailbox::send_locked` + `Mailbox::notify` in that case. pub fn queue_message(&self, msg: Message) { self.mailbox.send(msg); self.mailbox.notify(); } /// Queue a write directly to the pty. /// /// If you're using `Thread`, this must ONLY be called from the mailbox /// thread. If you're not on the thread, use `queue_message` with /// mailbox messages instead. pub fn queue_write(&mut self, td: &mut ThreadData, data: &[u8], linefeed: bool) { self.backend.queue_write(&mut td.backend, data, linefeed); } /// Update the configuration. pub fn change_config(&mut self, td: &mut ThreadData, config: DerivedConfig) { // The remainder of this function is modifying terminal state or // the read thread data, all of which requires holding the renderer // state lock. let mut term = self.renderer_state.lock_terminal(); // Update our stream handler. The stream handler uses the same // renderer mutex so this is safe to do despite being executed // from another thread. self.output.inner().stream.change_config(&mut term, &config); td.backend.change_config(&config); // Update the configuration that we know about. // // Specific things we don't update: // - command, working-directory: we never restart the underlying // process so we don't care or need to know about these. // Update the default palette. term.colors.palette.change_default(config.palette); term.flags.dirty.palette = true; // Update all our other colors term.colors.background.default = Some(config.background.to_terminal_rgb()); term.colors.foreground.default = Some(config.foreground.to_terminal_rgb()); term.colors.cursor.default = config.cursor_color.and_then(TerminalColor::to_terminal_rgb); // Set the image size limits set_image_limits(&mut term, config.image_storage_limit); self.config = config; } /// Resize the terminal. pub fn resize(&mut self, td: &mut ThreadData, size: Size) -> Result<(), Error> { self.size = size; let grid_size = size.grid(); // Update the size of our pty. self.backend.resize(grid_size, size.terminal())?; // Enter the critical area that we want to keep small { let state = Arc::clone(&self.renderer_state); let mut term = state.lock_terminal(); self.output().inner().stream.state.size = size; // Update the size of our terminal state term.resize(grid_size.columns, grid_size.rows)?; // Update our pixel sizes term.width_px = u32::from(grid_size.columns) * size.cell.width; term.height_px = u32::from(grid_size.rows) * size.cell.height; // Disable synchronized output mode so that we show changes // immediately for a resize. This is allowed by the spec. term.modes.set(Mode::SynchronizedOutput, false); // If we have size reporting enabled we need to send a report. if term.modes.get(Mode::InBandSizeReports) { self.size_report_locked(td, SizeReport::Mode2048); } } // Mail the renderer so that it can update the GPU and re-render self.renderer_mailbox .push(RendererMessage::Resize(size), Timeout::Forever); if let Err(err) = self.renderer_wakeup.notify() { log::warn!("failed to notify renderer err={err}"); } Ok(()) } /// Make a size report. pub fn size_report(&mut self, td: &mut ThreadData, style: SizeReport) { let state = Arc::clone(&self.renderer_state); let _term = state.lock_terminal(); self.size_report_locked(td, style); } fn size_report_locked(&mut self, td: &mut ThreadData, style: SizeReport) { let grid_size = self.size.grid(); let cell = self.size.cell; let columns = u32::from(grid_size.columns); let rows = u32::from(grid_size.rows); let message = match style { SizeReport::Mode2048 => format!( "\x1B[48;{};{};{};{}t", rows, columns, rows * cell.height, columns * cell.width, ), SizeReport::Csi14T => { format!("\x1b[4;{};{}t", rows * cell.height, columns * cell.width) } SizeReport::Csi16T => format!("\x1b[6;{};{}t", cell.height, cell.width), SizeReport::Csi18T => format!("\x1b[8;{rows};{columns}t"), }; self.queue_write(td, message.as_bytes(), false); } /// Reset the synchronized output mode. This is usually called by timer /// expiration from the termio thread. pub fn reset_synchronized_output(&mut self) { let mut term = self.renderer_state.lock_terminal(); term.modes.set(Mode::SynchronizedOutput, false); if let Err(err) = self.renderer_wakeup.notify() { log::warn!("failed to notify renderer err={err}"); } } /// Clear the screen. pub fn clear_screen(&mut self, td: &mut ThreadData, history: bool) { { let mut term = self.renderer_state.lock_terminal(); // If we're on the alternate screen, we do not clear. Since this is an // emulator-level screen clear, this messes up the running programs // knowledge of where the cursor is and causes rendering issues. So, // for alt screen, we do nothing. if term.screens.active_key == ScreenKey::Alternate { return; } // Clear our selection term.screen_mut().clear_selection(); // Clear our scrollback if history { term.erase_display(csi::EraseDisplay::Scrollback, false); } // If we're not at a prompt, we just delete above the cursor. if !term.cursor_is_at_prompt() { let y = term.screen().cursor.y; if y > 0 { term.screen_mut().erase_rows( Point::Active(Coordinate { x: 0, y: 0 }), Some(Point::Active(Coordinate { x: 0, y: u32::from(y) - 1, })), ); } // Clear all Kitty graphics state for this screen. This copies // Kitty's behavior when Cmd+K deletes all Kitty graphics. I // didn't spend time researching whether it only deletes Kitty // graphics that are placed above the cursor or if it deletes // all of them. We delete all of them for now but if this behavior // isn't fully correct we should fix this later. let info = TerminalInfo::from_terminal(&term); let screen = term.screen_mut(); screen .kitty_images .delete(&mut screen.pages, &info, KittyDelete::All(true)); return; } // At a prompt, we want to first fully clear the screen, and then after // send a FF (0x0C) to the shell so that it can repaint the screen. term.erase_display(csi::EraseDisplay::Complete, false); } // If we reached here it means we're at a prompt, so we send a form-feed. self.queue_write(td, &[0x0C], false); } /// Scroll the viewport pub fn scroll_viewport(&mut self, scroll: ScrollViewport) { self.renderer_state.lock_terminal().scroll_viewport(scroll); } /// Jump the viewport to the prompt. pub fn jump_to_prompt(&mut self, delta: isize) { { let mut term = self.renderer_state.lock_terminal(); term.screen_mut().scroll(Scroll::DeltaPrompt(delta)); } if let Err(err) = self.renderer_wakeup.notify() { log::warn!("failed to notify renderer err={err}"); } } /// Called when focus is gained or lost (when focus events are enabled) pub fn focus_gained(&mut self, td: &mut ThreadData, focused: bool) { let focus_event = self .renderer_state .lock_terminal() .modes .get(Mode::FocusEvent); // If we have focus events enabled, we send the focus event. if focus_event { let seq: &[u8] = if focused { b"\x1b[I" } else { b"\x1b[O" }; self.queue_write(td, seq, false); } // We always notify our backend of focus changes. self.backend.focus_gained(&mut td.backend, focused); } /// Process output from the pty. See [`Output::process_output`]. pub fn process_output(&self, buf: &[u8]) { self.output.process_output(buf); } /// Sends a DSR response for the current color scheme to the pty. pub fn color_scheme_report(&mut self, td: &mut ThreadData, force: bool) { let state = Arc::clone(&self.renderer_state); let term = state.lock_terminal(); self.color_scheme_report_locked(&term, td, force); } pub fn color_scheme_report_locked( &mut self, term: &Terminal, td: &mut ThreadData, force: bool, ) { if !force && !term.modes.get(Mode::ReportColorScheme) { return; } let output: &[u8] = match self.config.conditional_state.theme { conditional::Theme::Light => b"\x1B[?997;2n", conditional::Theme::Dark => b"\x1B[?997;1n", }; self.queue_write(td, output, false); } } /// Apply `image-storage-limit` to every initialized screen. fn set_image_limits(term: &mut Terminal, limit: usize) { for key in [ScreenKey::Primary, ScreenKey::Alternate] { if let Some(screen) = term.screens.get_mut(key) && let Err(err) = screen.kitty_images.set_limit(&mut screen.pages, limit) { log::warn!("failed to set kitty image storage limit: {err}"); } } }