terminal.rs

   1pub mod mappings;
   2
   3pub use alacritty_terminal;
   4
   5mod pty_info;
   6mod terminal_hyperlinks;
   7pub mod terminal_settings;
   8
   9use alacritty_terminal::{
  10    Term,
  11    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
  12    event_loop::{EventLoop, Msg, Notifier},
  13    grid::{Dimensions, Grid, Row, Scroll as AlacScroll},
  14    index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
  15    selection::{Selection, SelectionRange, SelectionType},
  16    sync::FairMutex,
  17    term::{
  18        Config, RenderableCursor, TermMode,
  19        cell::{Cell, Flags},
  20        search::{Match, RegexIter, RegexSearch},
  21    },
  22    tty::{self},
  23    vi_mode::{ViModeCursor, ViMotion},
  24    vte::ansi::{
  25        ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode,
  26    },
  27};
  28use anyhow::{Context as _, Result, bail};
  29use log::trace;
  30
  31use futures::{
  32    FutureExt,
  33    channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
  34};
  35
  36use itertools::Itertools as _;
  37use mappings::mouse::{
  38    alt_scroll, grid_point, grid_point_and_side, mouse_button_report, mouse_moved_report,
  39    scroll_report,
  40};
  41
  42use collections::{HashMap, VecDeque};
  43use futures::StreamExt;
  44use pty_info::{ProcessIdGetter, PtyProcessInfo};
  45use serde::{Deserialize, Serialize};
  46use settings::Settings;
  47use smol::channel::{Receiver, Sender};
  48use task::{HideStrategy, Shell, SpawnInTerminal};
  49use terminal_hyperlinks::RegexSearches;
  50use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
  51use theme::{ActiveTheme, Theme};
  52use urlencoding;
  53use util::{paths::PathStyle, truncate_and_trailoff};
  54
  55#[cfg(unix)]
  56use std::os::unix::process::ExitStatusExt;
  57use std::{
  58    borrow::Cow,
  59    cmp::{self, min},
  60    fmt::Display,
  61    ops::{Deref, RangeInclusive},
  62    path::PathBuf,
  63    process::ExitStatus,
  64    sync::Arc,
  65    time::{Duration, Instant},
  66};
  67use thiserror::Error;
  68
  69use gpui::{
  70    App, AppContext as _, BackgroundExecutor, Bounds, ClipboardItem, Context, EventEmitter, Hsla,
  71    Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
  72    Rgba, ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px,
  73};
  74
  75use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
  76
  77actions!(
  78    terminal,
  79    [
  80        /// Clears the terminal screen.
  81        Clear,
  82        /// Copies selected text to the clipboard.
  83        Copy,
  84        /// Pastes from the clipboard.
  85        Paste,
  86        /// Shows the character palette for special characters.
  87        ShowCharacterPalette,
  88        /// Searches for text in the terminal.
  89        SearchTest,
  90        /// Scrolls up by one line.
  91        ScrollLineUp,
  92        /// Scrolls down by one line.
  93        ScrollLineDown,
  94        /// Scrolls up by one page.
  95        ScrollPageUp,
  96        /// Scrolls down by one page.
  97        ScrollPageDown,
  98        /// Scrolls up by half a page.
  99        ScrollHalfPageUp,
 100        /// Scrolls down by half a page.
 101        ScrollHalfPageDown,
 102        /// Scrolls to the top of the terminal buffer.
 103        ScrollToTop,
 104        /// Scrolls to the bottom of the terminal buffer.
 105        ScrollToBottom,
 106        /// Toggles vi mode in the terminal.
 107        ToggleViMode,
 108        /// Selects all text in the terminal.
 109        SelectAll,
 110    ]
 111);
 112
 113const DEBUG_TERMINAL_WIDTH: Pixels = px(500.);
 114const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
 115const DEBUG_CELL_WIDTH: Pixels = px(5.);
 116const DEBUG_LINE_HEIGHT: Pixels = px(5.);
 117
 118/// Inserts Zed-specific environment variables for terminal sessions.
 119/// Used by both local terminals and remote terminals (via SSH).
 120pub fn insert_zed_terminal_env(
 121    env: &mut HashMap<String, String>,
 122    version: &impl std::fmt::Display,
 123) {
 124    env.insert("ZED_TERM".to_string(), "true".to_string());
 125    env.insert("TERM_PROGRAM".to_string(), "zed".to_string());
 126    env.insert("TERM".to_string(), "xterm-256color".to_string());
 127    env.insert("COLORTERM".to_string(), "truecolor".to_string());
 128    env.insert("TERM_PROGRAM_VERSION".to_string(), version.to_string());
 129}
 130
 131///Upward flowing events, for changing the title and such
 132#[derive(Clone, Debug, PartialEq, Eq)]
 133pub enum Event {
 134    TitleChanged,
 135    BreadcrumbsChanged,
 136    CloseTerminal,
 137    Bell,
 138    Wakeup,
 139    BlinkChanged(bool),
 140    SelectionsChanged,
 141    NewNavigationTarget(Option<MaybeNavigationTarget>),
 142    Open(MaybeNavigationTarget),
 143}
 144
 145#[derive(Clone, Debug, PartialEq, Eq)]
 146pub struct PathLikeTarget {
 147    /// File system path, absolute or relative, existing or not.
 148    /// Might have line and column number(s) attached as `file.rs:1:23`
 149    pub maybe_path: String,
 150    /// Current working directory of the terminal
 151    pub terminal_dir: Option<PathBuf>,
 152}
 153
 154/// A string inside terminal, potentially useful as a URI that can be opened.
 155#[derive(Clone, Debug, PartialEq, Eq)]
 156pub enum MaybeNavigationTarget {
 157    /// HTTP, git, etc. string determined by the `URL_REGEX` regex.
 158    Url(String),
 159    /// File system path, absolute or relative, existing or not.
 160    /// Might have line and column number(s) attached as `file.rs:1:23`
 161    PathLike(PathLikeTarget),
 162}
 163
 164#[derive(Clone)]
 165enum InternalEvent {
 166    Resize(TerminalBounds),
 167    Clear,
 168    // FocusNextMatch,
 169    Scroll(AlacScroll),
 170    ScrollToAlacPoint(AlacPoint),
 171    SetSelection(Option<(Selection, AlacPoint)>),
 172    UpdateSelection(Point<Pixels>),
 173    FindHyperlink(Point<Pixels>, bool),
 174    ProcessHyperlink((String, bool, Match), bool),
 175    // Whether keep selection when copy
 176    Copy(Option<bool>),
 177    // Vi mode events
 178    ToggleViMode,
 179    ViMotion(ViMotion),
 180    MoveViCursorToAlacPoint(AlacPoint),
 181}
 182
 183///A translation struct for Alacritty to communicate with us from their event loop
 184#[derive(Clone)]
 185pub struct ZedListener(pub UnboundedSender<AlacTermEvent>);
 186
 187impl EventListener for ZedListener {
 188    fn send_event(&self, event: AlacTermEvent) {
 189        self.0.unbounded_send(event).ok();
 190    }
 191}
 192
 193#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
 194pub struct TerminalBounds {
 195    pub cell_width: Pixels,
 196    pub line_height: Pixels,
 197    pub bounds: Bounds<Pixels>,
 198}
 199
 200impl TerminalBounds {
 201    pub fn new(line_height: Pixels, cell_width: Pixels, bounds: Bounds<Pixels>) -> Self {
 202        TerminalBounds {
 203            cell_width,
 204            line_height,
 205            bounds,
 206        }
 207    }
 208
 209    pub fn num_lines(&self) -> usize {
 210        (self.bounds.size.height / self.line_height).floor() as usize
 211    }
 212
 213    pub fn num_columns(&self) -> usize {
 214        (self.bounds.size.width / self.cell_width).floor() as usize
 215    }
 216
 217    pub fn height(&self) -> Pixels {
 218        self.bounds.size.height
 219    }
 220
 221    pub fn width(&self) -> Pixels {
 222        self.bounds.size.width
 223    }
 224
 225    pub fn cell_width(&self) -> Pixels {
 226        self.cell_width
 227    }
 228
 229    pub fn line_height(&self) -> Pixels {
 230        self.line_height
 231    }
 232}
 233
 234impl Default for TerminalBounds {
 235    fn default() -> Self {
 236        TerminalBounds::new(
 237            DEBUG_LINE_HEIGHT,
 238            DEBUG_CELL_WIDTH,
 239            Bounds {
 240                origin: Point::default(),
 241                size: Size {
 242                    width: DEBUG_TERMINAL_WIDTH,
 243                    height: DEBUG_TERMINAL_HEIGHT,
 244                },
 245            },
 246        )
 247    }
 248}
 249
 250impl From<TerminalBounds> for WindowSize {
 251    fn from(val: TerminalBounds) -> Self {
 252        WindowSize {
 253            num_lines: val.num_lines() as u16,
 254            num_cols: val.num_columns() as u16,
 255            cell_width: f32::from(val.cell_width()) as u16,
 256            cell_height: f32::from(val.line_height()) as u16,
 257        }
 258    }
 259}
 260
 261impl Dimensions for TerminalBounds {
 262    /// Note: this is supposed to be for the back buffer's length,
 263    /// but we exclusively use it to resize the terminal, which does not
 264    /// use this method. We still have to implement it for the trait though,
 265    /// hence, this comment.
 266    fn total_lines(&self) -> usize {
 267        self.screen_lines()
 268    }
 269
 270    fn screen_lines(&self) -> usize {
 271        self.num_lines()
 272    }
 273
 274    fn columns(&self) -> usize {
 275        self.num_columns()
 276    }
 277}
 278
 279#[derive(Error, Debug)]
 280pub struct TerminalError {
 281    pub directory: Option<PathBuf>,
 282    pub program: Option<String>,
 283    pub args: Option<Vec<String>>,
 284    pub title_override: Option<String>,
 285    pub source: std::io::Error,
 286}
 287
 288impl TerminalError {
 289    pub fn fmt_directory(&self) -> String {
 290        self.directory
 291            .clone()
 292            .map(|path| {
 293                match path
 294                    .into_os_string()
 295                    .into_string()
 296                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
 297                {
 298                    Ok(s) => s,
 299                    Err(s) => s,
 300                }
 301            })
 302            .unwrap_or_else(|| "<none specified>".to_string())
 303    }
 304
 305    pub fn fmt_shell(&self) -> String {
 306        if let Some(title_override) = &self.title_override {
 307            format!(
 308                "{} {} ({})",
 309                self.program.as_deref().unwrap_or("<system defined shell>"),
 310                self.args.as_ref().into_iter().flatten().format(" "),
 311                title_override
 312            )
 313        } else {
 314            format!(
 315                "{} {}",
 316                self.program.as_deref().unwrap_or("<system defined shell>"),
 317                self.args.as_ref().into_iter().flatten().format(" ")
 318            )
 319        }
 320    }
 321}
 322
 323impl Display for TerminalError {
 324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 325        let dir_string: String = self.fmt_directory();
 326        let shell = self.fmt_shell();
 327
 328        write!(
 329            f,
 330            "Working directory: {} Shell command: `{}`, IOError: {}",
 331            dir_string, shell, self.source
 332        )
 333    }
 334}
 335
 336// https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213
 337const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000;
 338pub const MAX_SCROLL_HISTORY_LINES: usize = 100_000;
 339
 340pub struct TerminalBuilder {
 341    terminal: Terminal,
 342    events_rx: UnboundedReceiver<AlacTermEvent>,
 343}
 344
 345impl TerminalBuilder {
 346    pub fn new_display_only(
 347        cursor_shape: CursorShape,
 348        alternate_scroll: AlternateScroll,
 349        max_scroll_history_lines: Option<usize>,
 350        window_id: u64,
 351        background_executor: &BackgroundExecutor,
 352        path_style: PathStyle,
 353    ) -> Result<TerminalBuilder> {
 354        // Create a display-only terminal (no actual PTY).
 355        let default_cursor_style = AlacCursorStyle::from(cursor_shape);
 356        let scrolling_history = max_scroll_history_lines
 357            .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES)
 358            .min(MAX_SCROLL_HISTORY_LINES);
 359        let config = Config {
 360            scrolling_history,
 361            default_cursor_style,
 362            ..Config::default()
 363        };
 364
 365        let (events_tx, events_rx) = unbounded();
 366        let mut term = Term::new(
 367            config.clone(),
 368            &TerminalBounds::default(),
 369            ZedListener(events_tx),
 370        );
 371
 372        if let AlternateScroll::Off = alternate_scroll {
 373            term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
 374        }
 375
 376        let term = Arc::new(FairMutex::new(term));
 377
 378        let terminal = Terminal {
 379            task: None,
 380            terminal_type: TerminalType::DisplayOnly,
 381            completion_tx: None,
 382            term,
 383            term_config: config,
 384            title_override: None,
 385            events: VecDeque::with_capacity(10),
 386            last_content: Default::default(),
 387            last_mouse: None,
 388            matches: Vec::new(),
 389
 390            selection_head: None,
 391            breadcrumb_text: String::new(),
 392            scroll_px: px(0.),
 393            next_link_id: 0,
 394            selection_phase: SelectionPhase::Ended,
 395            hyperlink_regex_searches: RegexSearches::default(),
 396            vi_mode_enabled: false,
 397            is_remote_terminal: false,
 398            last_mouse_move_time: Instant::now(),
 399            last_hyperlink_search_position: None,
 400            mouse_down_hyperlink: None,
 401            #[cfg(windows)]
 402            shell_program: None,
 403            activation_script: Vec::new(),
 404            template: CopyTemplate {
 405                shell: Shell::System,
 406                env: HashMap::default(),
 407                cursor_shape,
 408                alternate_scroll,
 409                max_scroll_history_lines,
 410                path_hyperlink_regexes: Vec::default(),
 411                path_hyperlink_timeout_ms: 0,
 412                window_id,
 413            },
 414            child_exited: None,
 415            event_loop_task: Task::ready(Ok(())),
 416            background_executor: background_executor.clone(),
 417            path_style,
 418        };
 419
 420        Ok(TerminalBuilder {
 421            terminal,
 422            events_rx,
 423        })
 424    }
 425
 426    pub fn new(
 427        working_directory: Option<PathBuf>,
 428        task: Option<TaskState>,
 429        shell: Shell,
 430        mut env: HashMap<String, String>,
 431        cursor_shape: CursorShape,
 432        alternate_scroll: AlternateScroll,
 433        max_scroll_history_lines: Option<usize>,
 434        path_hyperlink_regexes: Vec<String>,
 435        path_hyperlink_timeout_ms: u64,
 436        is_remote_terminal: bool,
 437        window_id: u64,
 438        completion_tx: Option<Sender<Option<ExitStatus>>>,
 439        cx: &App,
 440        activation_script: Vec<String>,
 441        path_style: PathStyle,
 442    ) -> Task<Result<TerminalBuilder>> {
 443        let version = release_channel::AppVersion::global(cx);
 444        let background_executor = cx.background_executor().clone();
 445        let fut = async move {
 446            // Remove SHLVL so the spawned shell initializes it to 1, matching
 447            // the behavior of standalone terminal emulators like iTerm2/Kitty/Alacritty.
 448            env.remove("SHLVL");
 449
 450            // If the parent environment doesn't have a locale set
 451            // (As is the case when launched from a .app on MacOS),
 452            // and the Project doesn't have a locale set, then
 453            // set a fallback for our child environment to use.
 454            if std::env::var("LANG").is_err() {
 455                env.entry("LANG".to_string())
 456                    .or_insert_with(|| "en_US.UTF-8".to_string());
 457            }
 458
 459            insert_zed_terminal_env(&mut env, &version);
 460
 461            #[derive(Default)]
 462            struct ShellParams {
 463                program: String,
 464                args: Option<Vec<String>>,
 465                title_override: Option<String>,
 466            }
 467
 468            impl ShellParams {
 469                fn new(
 470                    program: String,
 471                    args: Option<Vec<String>>,
 472                    title_override: Option<String>,
 473                ) -> Self {
 474                    log::debug!("Using {program} as shell");
 475                    Self {
 476                        program,
 477                        args,
 478                        title_override,
 479                    }
 480                }
 481            }
 482
 483            let shell_params = match shell.clone() {
 484                Shell::System => {
 485                    if cfg!(windows) {
 486                        Some(ShellParams::new(
 487                            util::shell::get_windows_system_shell(),
 488                            None,
 489                            None,
 490                        ))
 491                    } else {
 492                        None
 493                    }
 494                }
 495                Shell::Program(program) => Some(ShellParams::new(program, None, None)),
 496                Shell::WithArguments {
 497                    program,
 498                    args,
 499                    title_override,
 500                } => Some(ShellParams::new(program, Some(args), title_override)),
 501            };
 502            let terminal_title_override =
 503                shell_params.as_ref().and_then(|e| e.title_override.clone());
 504
 505            #[cfg(windows)]
 506            let shell_program = shell_params.as_ref().map(|params| {
 507                use util::ResultExt;
 508
 509                Self::resolve_path(&params.program)
 510                    .log_err()
 511                    .unwrap_or(params.program.clone())
 512            });
 513
 514            // Note: when remoting, this shell_kind will scrutinize `ssh` or
 515            // `wsl.exe` as a shell and fall back to posix or powershell based on
 516            // the compilation target. This is fine right now due to the restricted
 517            // way we use the return value, but would become incorrect if we
 518            // supported remoting into windows.
 519            let shell_kind = shell.shell_kind(cfg!(windows));
 520
 521            let pty_options = {
 522                let alac_shell = shell_params.as_ref().map(|params| {
 523                    alacritty_terminal::tty::Shell::new(
 524                        params.program.clone(),
 525                        params.args.clone().unwrap_or_default(),
 526                    )
 527                });
 528
 529                alacritty_terminal::tty::Options {
 530                    shell: alac_shell,
 531                    working_directory: working_directory.clone(),
 532                    drain_on_exit: true,
 533                    env: env.clone().into_iter().collect(),
 534                    #[cfg(windows)]
 535                    escape_args: shell_kind.tty_escape_args(),
 536                }
 537            };
 538
 539            let default_cursor_style = AlacCursorStyle::from(cursor_shape);
 540            let scrolling_history = if task.is_some() {
 541                // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.
 542                // After the task finishes, we do not allow appending to that terminal, so small tasks output should not
 543                // cause excessive memory usage over time.
 544                MAX_SCROLL_HISTORY_LINES
 545            } else {
 546                max_scroll_history_lines
 547                    .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES)
 548                    .min(MAX_SCROLL_HISTORY_LINES)
 549            };
 550            let config = Config {
 551                scrolling_history,
 552                default_cursor_style,
 553                ..Config::default()
 554            };
 555
 556            //Setup the pty...
 557            let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) {
 558                Ok(pty) => pty,
 559                Err(error) => {
 560                    bail!(TerminalError {
 561                        directory: working_directory,
 562                        program: shell_params.as_ref().map(|params| params.program.clone()),
 563                        args: shell_params.as_ref().and_then(|params| params.args.clone()),
 564                        title_override: terminal_title_override,
 565                        source: error,
 566                    });
 567                }
 568            };
 569
 570            //Spawn a task so the Alacritty EventLoop can communicate with us
 571            //TODO: Remove with a bounded sender which can be dispatched on &self
 572            let (events_tx, events_rx) = unbounded();
 573            //Set up the terminal...
 574            let mut term = Term::new(
 575                config.clone(),
 576                &TerminalBounds::default(),
 577                ZedListener(events_tx.clone()),
 578            );
 579
 580            //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
 581            if let AlternateScroll::Off = alternate_scroll {
 582                term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
 583            }
 584
 585            let term = Arc::new(FairMutex::new(term));
 586
 587            let pty_info = PtyProcessInfo::new(&pty);
 588
 589            //And connect them together
 590            let event_loop = EventLoop::new(
 591                term.clone(),
 592                ZedListener(events_tx),
 593                pty,
 594                pty_options.drain_on_exit,
 595                false,
 596            )
 597            .context("failed to create event loop")?;
 598
 599            let pty_tx = event_loop.channel();
 600            let _io_thread = event_loop.spawn(); // DANGER
 601
 602            let no_task = task.is_none();
 603            let terminal = Terminal {
 604                task,
 605                terminal_type: TerminalType::Pty {
 606                    pty_tx: Notifier(pty_tx),
 607                    info: Arc::new(pty_info),
 608                },
 609                completion_tx,
 610                term,
 611                term_config: config,
 612                title_override: terminal_title_override,
 613                events: VecDeque::with_capacity(10), //Should never get this high.
 614                last_content: Default::default(),
 615                last_mouse: None,
 616                matches: Vec::new(),
 617
 618                selection_head: None,
 619                breadcrumb_text: String::new(),
 620                scroll_px: px(0.),
 621                next_link_id: 0,
 622                selection_phase: SelectionPhase::Ended,
 623                hyperlink_regex_searches: RegexSearches::new(
 624                    &path_hyperlink_regexes,
 625                    path_hyperlink_timeout_ms,
 626                ),
 627                vi_mode_enabled: false,
 628                is_remote_terminal,
 629                last_mouse_move_time: Instant::now(),
 630                last_hyperlink_search_position: None,
 631                mouse_down_hyperlink: None,
 632                #[cfg(windows)]
 633                shell_program,
 634                activation_script: activation_script.clone(),
 635                template: CopyTemplate {
 636                    shell,
 637                    env,
 638                    cursor_shape,
 639                    alternate_scroll,
 640                    max_scroll_history_lines,
 641                    path_hyperlink_regexes,
 642                    path_hyperlink_timeout_ms,
 643                    window_id,
 644                },
 645                child_exited: None,
 646                event_loop_task: Task::ready(Ok(())),
 647                background_executor,
 648                path_style,
 649            };
 650
 651            if !activation_script.is_empty() && no_task {
 652                for activation_script in activation_script {
 653                    terminal.write_to_pty(activation_script.into_bytes());
 654                    // Simulate enter key press
 655                    // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character)
 656                    // and generally mess up the rendering.
 657                    terminal.write_to_pty(b"\x0d");
 658                }
 659                // In order to clear the screen at this point, we have two options:
 660                // 1. We can send a shell-specific command such as "clear" or "cls"
 661                // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event
 662                //    and clear the screen using `terminal.clear()` method
 663                // We cannot issue a `terminal.clear()` command at this point as alacritty is evented
 664                // and while we have sent the activation script to the pty, it will be executed asynchronously.
 665                // Therefore, we somehow need to wait for the activation script to finish executing before we
 666                // can proceed with clearing the screen.
 667                terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
 668                // Simulate enter key press
 669                terminal.write_to_pty(b"\x0d");
 670            }
 671
 672            Ok(TerminalBuilder {
 673                terminal,
 674                events_rx,
 675            })
 676        };
 677        // the thread we spawn things on has an effect on signal handling
 678        if !cfg!(target_os = "windows") {
 679            cx.spawn(async move |_| fut.await)
 680        } else {
 681            cx.background_spawn(fut)
 682        }
 683    }
 684
 685    pub fn subscribe(mut self, cx: &Context<Terminal>) -> Terminal {
 686        //Event loop
 687        self.terminal.event_loop_task = cx.spawn(async move |terminal, cx| {
 688            while let Some(event) = self.events_rx.next().await {
 689                terminal.update(cx, |terminal, cx| {
 690                    //Process the first event immediately for lowered latency
 691                    terminal.process_event(event, cx);
 692                })?;
 693
 694                'outer: loop {
 695                    let mut events = Vec::new();
 696
 697                    #[cfg(any(test, feature = "test-support"))]
 698                    let mut timer = cx.background_executor().simulate_random_delay().fuse();
 699                    #[cfg(not(any(test, feature = "test-support")))]
 700                    let mut timer = cx
 701                        .background_executor()
 702                        .timer(std::time::Duration::from_millis(4))
 703                        .fuse();
 704
 705                    let mut wakeup = false;
 706                    loop {
 707                        futures::select_biased! {
 708                            _ = timer => break,
 709                            event = self.events_rx.next() => {
 710                                if let Some(event) = event {
 711                                    if matches!(event, AlacTermEvent::Wakeup) {
 712                                        wakeup = true;
 713                                    } else {
 714                                        events.push(event);
 715                                    }
 716
 717                                    if events.len() > 100 {
 718                                        break;
 719                                    }
 720                                } else {
 721                                    break;
 722                                }
 723                            },
 724                        }
 725                    }
 726
 727                    if events.is_empty() && !wakeup {
 728                        smol::future::yield_now().await;
 729                        break 'outer;
 730                    }
 731
 732                    terminal.update(cx, |this, cx| {
 733                        if wakeup {
 734                            this.process_event(AlacTermEvent::Wakeup, cx);
 735                        }
 736
 737                        for event in events {
 738                            this.process_event(event, cx);
 739                        }
 740                    })?;
 741                    smol::future::yield_now().await;
 742                }
 743            }
 744            anyhow::Ok(())
 745        });
 746        self.terminal
 747    }
 748
 749    #[cfg(windows)]
 750    fn resolve_path(path: &str) -> Result<String> {
 751        use windows::Win32::Storage::FileSystem::SearchPathW;
 752        use windows::core::HSTRING;
 753
 754        let path = if path.starts_with(r"\\?\") || !path.contains(&['/', '\\']) {
 755            path.to_string()
 756        } else {
 757            r"\\?\".to_string() + path
 758        };
 759
 760        let required_length = unsafe { SearchPathW(None, &HSTRING::from(&path), None, None, None) };
 761        let mut buf = vec![0u16; required_length as usize];
 762        let size = unsafe { SearchPathW(None, &HSTRING::from(&path), None, Some(&mut buf), None) };
 763
 764        Ok(String::from_utf16(&buf[..size as usize])?)
 765    }
 766}
 767
 768#[derive(Debug, Clone, Deserialize, Serialize)]
 769pub struct IndexedCell {
 770    pub point: AlacPoint,
 771    pub cell: Cell,
 772}
 773
 774impl Deref for IndexedCell {
 775    type Target = Cell;
 776
 777    #[inline]
 778    fn deref(&self) -> &Cell {
 779        &self.cell
 780    }
 781}
 782
 783// TODO: Un-pub
 784#[derive(Clone)]
 785pub struct TerminalContent {
 786    pub cells: Vec<IndexedCell>,
 787    pub mode: TermMode,
 788    pub display_offset: usize,
 789    pub selection_text: Option<String>,
 790    pub selection: Option<SelectionRange>,
 791    pub cursor: RenderableCursor,
 792    pub cursor_char: char,
 793    pub terminal_bounds: TerminalBounds,
 794    pub last_hovered_word: Option<HoveredWord>,
 795    pub scrolled_to_top: bool,
 796    pub scrolled_to_bottom: bool,
 797}
 798
 799#[derive(Debug, Clone, Eq, PartialEq)]
 800pub struct HoveredWord {
 801    pub word: String,
 802    pub word_match: RangeInclusive<AlacPoint>,
 803    pub id: usize,
 804}
 805
 806impl Default for TerminalContent {
 807    fn default() -> Self {
 808        TerminalContent {
 809            cells: Default::default(),
 810            mode: Default::default(),
 811            display_offset: Default::default(),
 812            selection_text: Default::default(),
 813            selection: Default::default(),
 814            cursor: RenderableCursor {
 815                shape: alacritty_terminal::vte::ansi::CursorShape::Block,
 816                point: AlacPoint::new(Line(0), Column(0)),
 817            },
 818            cursor_char: Default::default(),
 819            terminal_bounds: Default::default(),
 820            last_hovered_word: None,
 821            scrolled_to_top: false,
 822            scrolled_to_bottom: false,
 823        }
 824    }
 825}
 826
 827#[derive(PartialEq, Eq)]
 828pub enum SelectionPhase {
 829    Selecting,
 830    Ended,
 831}
 832
 833enum TerminalType {
 834    Pty {
 835        pty_tx: Notifier,
 836        info: Arc<PtyProcessInfo>,
 837    },
 838    DisplayOnly,
 839}
 840
 841pub struct Terminal {
 842    terminal_type: TerminalType,
 843    completion_tx: Option<Sender<Option<ExitStatus>>>,
 844    term: Arc<FairMutex<Term<ZedListener>>>,
 845    term_config: Config,
 846    events: VecDeque<InternalEvent>,
 847    /// This is only used for mouse mode cell change detection
 848    last_mouse: Option<(AlacPoint, AlacDirection)>,
 849    pub matches: Vec<RangeInclusive<AlacPoint>>,
 850    pub last_content: TerminalContent,
 851    pub selection_head: Option<AlacPoint>,
 852
 853    pub breadcrumb_text: String,
 854    title_override: Option<String>,
 855    scroll_px: Pixels,
 856    next_link_id: usize,
 857    selection_phase: SelectionPhase,
 858    hyperlink_regex_searches: RegexSearches,
 859    task: Option<TaskState>,
 860    vi_mode_enabled: bool,
 861    is_remote_terminal: bool,
 862    last_mouse_move_time: Instant,
 863    last_hyperlink_search_position: Option<Point<Pixels>>,
 864    mouse_down_hyperlink: Option<(String, bool, Match)>,
 865    #[cfg(windows)]
 866    shell_program: Option<String>,
 867    template: CopyTemplate,
 868    activation_script: Vec<String>,
 869    child_exited: Option<ExitStatus>,
 870    event_loop_task: Task<Result<(), anyhow::Error>>,
 871    background_executor: BackgroundExecutor,
 872    path_style: PathStyle,
 873}
 874
 875struct CopyTemplate {
 876    shell: Shell,
 877    env: HashMap<String, String>,
 878    cursor_shape: CursorShape,
 879    alternate_scroll: AlternateScroll,
 880    max_scroll_history_lines: Option<usize>,
 881    path_hyperlink_regexes: Vec<String>,
 882    path_hyperlink_timeout_ms: u64,
 883    window_id: u64,
 884}
 885
 886#[derive(Debug)]
 887pub struct TaskState {
 888    pub status: TaskStatus,
 889    pub completion_rx: Receiver<Option<ExitStatus>>,
 890    pub spawned_task: SpawnInTerminal,
 891}
 892
 893/// A status of the current terminal tab's task.
 894#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 895pub enum TaskStatus {
 896    /// The task had been started, but got cancelled or somehow otherwise it did not
 897    /// report its exit code before the terminal event loop was shut down.
 898    Unknown,
 899    /// The task is started and running currently.
 900    Running,
 901    /// After the start, the task stopped running and reported its error code back.
 902    Completed { success: bool },
 903}
 904
 905impl TaskStatus {
 906    fn register_terminal_exit(&mut self) {
 907        if self == &Self::Running {
 908            *self = Self::Unknown;
 909        }
 910    }
 911
 912    fn register_task_exit(&mut self, error_code: i32) {
 913        *self = TaskStatus::Completed {
 914            success: error_code == 0,
 915        };
 916    }
 917}
 918
 919const FIND_HYPERLINK_THROTTLE_PX: Pixels = px(5.0);
 920
 921impl Terminal {
 922    fn process_event(&mut self, event: AlacTermEvent, cx: &mut Context<Self>) {
 923        match event {
 924            AlacTermEvent::Title(title) => {
 925                // ignore default shell program title change as windows always sends those events
 926                // and it would end up showing the shell executable path in breadcrumbs
 927                #[cfg(windows)]
 928                {
 929                    if self
 930                        .shell_program
 931                        .as_ref()
 932                        .map(|e| *e == title)
 933                        .unwrap_or(false)
 934                    {
 935                        return;
 936                    }
 937                }
 938
 939                self.breadcrumb_text = title;
 940                cx.emit(Event::BreadcrumbsChanged);
 941            }
 942            AlacTermEvent::ResetTitle => {
 943                self.breadcrumb_text = String::new();
 944                cx.emit(Event::BreadcrumbsChanged);
 945            }
 946            AlacTermEvent::ClipboardStore(_, data) => {
 947                cx.write_to_clipboard(ClipboardItem::new_string(data))
 948            }
 949            AlacTermEvent::ClipboardLoad(_, format) => {
 950                self.write_to_pty(
 951                    match &cx.read_from_clipboard().and_then(|item| item.text()) {
 952                        // The terminal only supports pasting strings, not images.
 953                        Some(text) => format(text),
 954                        _ => format(""),
 955                    }
 956                    .into_bytes(),
 957                )
 958            }
 959            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.into_bytes()),
 960            AlacTermEvent::TextAreaSizeRequest(format) => {
 961                self.write_to_pty(format(self.last_content.terminal_bounds.into()).into_bytes())
 962            }
 963            AlacTermEvent::CursorBlinkingChange => {
 964                let terminal = self.term.lock();
 965                let blinking = terminal.cursor_style().blinking;
 966                cx.emit(Event::BlinkChanged(blinking));
 967            }
 968            AlacTermEvent::Bell => {
 969                cx.emit(Event::Bell);
 970            }
 971            AlacTermEvent::Exit => self.register_task_finished(Some(9), cx),
 972            AlacTermEvent::MouseCursorDirty => {
 973                //NOOP, Handled in render
 974            }
 975            AlacTermEvent::Wakeup => {
 976                cx.emit(Event::Wakeup);
 977
 978                if let TerminalType::Pty { info, .. } = &self.terminal_type {
 979                    info.emit_title_changed_if_changed(cx);
 980                }
 981            }
 982            AlacTermEvent::ColorRequest(index, format) => {
 983                // It's important that the color request is processed here to retain relative order
 984                // with other PTY writes. Otherwise applications might witness out-of-order
 985                // responses to requests. For example: An application sending `OSC 11 ; ? ST`
 986                // (color request) followed by `CSI c` (request device attributes) would receive
 987                // the response to `CSI c` first.
 988                // Instead of locking, we could store the colors in `self.last_content`. But then
 989                // we might respond with out of date value if a "set color" sequence is immediately
 990                // followed by a color request sequence.
 991                let color = self.term.lock().colors()[index]
 992                    .unwrap_or_else(|| to_alac_rgb(get_color_at_index(index, cx.theme().as_ref())));
 993                self.write_to_pty(format(color).into_bytes());
 994            }
 995            AlacTermEvent::ChildExit(raw_status) => {
 996                self.register_task_finished(Some(raw_status), cx);
 997            }
 998        }
 999    }
1000
1001    pub fn selection_started(&self) -> bool {
1002        self.selection_phase == SelectionPhase::Selecting
1003    }
1004
1005    fn process_terminal_event(
1006        &mut self,
1007        event: &InternalEvent,
1008        term: &mut Term<ZedListener>,
1009        window: &mut Window,
1010        cx: &mut Context<Self>,
1011    ) {
1012        match event {
1013            &InternalEvent::Resize(mut new_bounds) => {
1014                trace!("Resizing: new_bounds={new_bounds:?}");
1015                new_bounds.bounds.size.height =
1016                    cmp::max(new_bounds.line_height, new_bounds.height());
1017                new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width());
1018
1019                self.last_content.terminal_bounds = new_bounds;
1020
1021                if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type {
1022                    pty_tx.0.send(Msg::Resize(new_bounds.into())).ok();
1023                }
1024
1025                term.resize(new_bounds);
1026                // If there are matches we need to emit a wake up event to
1027                // invalidate the matches and recalculate their locations
1028                // in the new terminal layout
1029                if !self.matches.is_empty() {
1030                    cx.emit(Event::Wakeup);
1031                }
1032            }
1033            InternalEvent::Clear => {
1034                trace!("Clearing");
1035                // Clear back buffer
1036                term.clear_screen(ClearMode::Saved);
1037
1038                let cursor = term.grid().cursor.point;
1039
1040                // Clear the lines above
1041                term.grid_mut().reset_region(..cursor.line);
1042
1043                // Copy the current line up
1044                let line = term.grid()[cursor.line][..Column(term.grid().columns())]
1045                    .iter()
1046                    .cloned()
1047                    .enumerate()
1048                    .collect::<Vec<(usize, Cell)>>();
1049
1050                for (i, cell) in line {
1051                    term.grid_mut()[Line(0)][Column(i)] = cell;
1052                }
1053
1054                // Reset the cursor
1055                term.grid_mut().cursor.point =
1056                    AlacPoint::new(Line(0), term.grid_mut().cursor.point.column);
1057                let new_cursor = term.grid().cursor.point;
1058
1059                // Clear the lines below the new cursor
1060                if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
1061                    term.grid_mut().reset_region((new_cursor.line + 1)..);
1062                }
1063
1064                cx.emit(Event::Wakeup);
1065            }
1066            InternalEvent::Scroll(scroll) => {
1067                trace!("Scrolling: scroll={scroll:?}");
1068                term.scroll_display(*scroll);
1069                self.refresh_hovered_word(window);
1070
1071                if self.vi_mode_enabled {
1072                    match *scroll {
1073                        AlacScroll::Delta(delta) => {
1074                            term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta);
1075                        }
1076                        AlacScroll::PageUp => {
1077                            let lines = term.screen_lines() as i32;
1078                            term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines);
1079                        }
1080                        AlacScroll::PageDown => {
1081                            let lines = -(term.screen_lines() as i32);
1082                            term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines);
1083                        }
1084                        AlacScroll::Top => {
1085                            let point = AlacPoint::new(term.topmost_line(), Column(0));
1086                            term.vi_mode_cursor = ViModeCursor::new(point);
1087                        }
1088                        AlacScroll::Bottom => {
1089                            let point = AlacPoint::new(term.bottommost_line(), Column(0));
1090                            term.vi_mode_cursor = ViModeCursor::new(point);
1091                        }
1092                    }
1093                    if let Some(mut selection) = term.selection.take() {
1094                        let point = term.vi_mode_cursor.point;
1095                        selection.update(point, AlacDirection::Right);
1096                        term.selection = Some(selection);
1097
1098                        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1099                        if let Some(selection_text) = term.selection_to_string() {
1100                            cx.write_to_primary(ClipboardItem::new_string(selection_text));
1101                        }
1102
1103                        self.selection_head = Some(point);
1104                        cx.emit(Event::SelectionsChanged)
1105                    }
1106                }
1107            }
1108            InternalEvent::SetSelection(selection) => {
1109                trace!("Setting selection: selection={selection:?}");
1110                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
1111
1112                #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1113                if let Some(selection_text) = term.selection_to_string() {
1114                    cx.write_to_primary(ClipboardItem::new_string(selection_text));
1115                }
1116
1117                if let Some((_, head)) = selection {
1118                    self.selection_head = Some(*head);
1119                }
1120                cx.emit(Event::SelectionsChanged)
1121            }
1122            InternalEvent::UpdateSelection(position) => {
1123                trace!("Updating selection: position={position:?}");
1124                if let Some(mut selection) = term.selection.take() {
1125                    let (point, side) = grid_point_and_side(
1126                        *position,
1127                        self.last_content.terminal_bounds,
1128                        term.grid().display_offset(),
1129                    );
1130
1131                    selection.update(point, side);
1132                    term.selection = Some(selection);
1133
1134                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1135                    if let Some(selection_text) = term.selection_to_string() {
1136                        cx.write_to_primary(ClipboardItem::new_string(selection_text));
1137                    }
1138
1139                    self.selection_head = Some(point);
1140                    cx.emit(Event::SelectionsChanged)
1141                }
1142            }
1143
1144            InternalEvent::Copy(keep_selection) => {
1145                trace!("Copying selection: keep_selection={keep_selection:?}");
1146                if let Some(txt) = term.selection_to_string() {
1147                    cx.write_to_clipboard(ClipboardItem::new_string(txt));
1148                    if !keep_selection.unwrap_or_else(|| {
1149                        let settings = TerminalSettings::get_global(cx);
1150                        settings.keep_selection_on_copy
1151                    }) {
1152                        self.events.push_back(InternalEvent::SetSelection(None));
1153                    }
1154                }
1155            }
1156            InternalEvent::ScrollToAlacPoint(point) => {
1157                trace!("Scrolling to point: point={point:?}");
1158                term.scroll_to_point(*point);
1159                self.refresh_hovered_word(window);
1160            }
1161            InternalEvent::MoveViCursorToAlacPoint(point) => {
1162                trace!("Move vi cursor to point: point={point:?}");
1163                term.vi_goto_point(*point);
1164                self.refresh_hovered_word(window);
1165            }
1166            InternalEvent::ToggleViMode => {
1167                trace!("Toggling vi mode");
1168                self.vi_mode_enabled = !self.vi_mode_enabled;
1169                term.toggle_vi_mode();
1170            }
1171            InternalEvent::ViMotion(motion) => {
1172                trace!("Performing vi motion: motion={motion:?}");
1173                term.vi_motion(*motion);
1174            }
1175            InternalEvent::FindHyperlink(position, open) => {
1176                trace!("Finding hyperlink at position: position={position:?}, open={open:?}");
1177
1178                let point = grid_point(
1179                    *position,
1180                    self.last_content.terminal_bounds,
1181                    term.grid().display_offset(),
1182                )
1183                .grid_clamp(term, Boundary::Grid);
1184
1185                match terminal_hyperlinks::find_from_grid_point(
1186                    term,
1187                    point,
1188                    &mut self.hyperlink_regex_searches,
1189                    self.path_style,
1190                ) {
1191                    Some(hyperlink) => {
1192                        self.process_hyperlink(hyperlink, *open, cx);
1193                    }
1194                    None => {
1195                        self.last_content.last_hovered_word = None;
1196                        cx.emit(Event::NewNavigationTarget(None));
1197                    }
1198                }
1199            }
1200            InternalEvent::ProcessHyperlink(hyperlink, open) => {
1201                self.process_hyperlink(hyperlink.clone(), *open, cx);
1202            }
1203        }
1204    }
1205
1206    fn process_hyperlink(
1207        &mut self,
1208        hyperlink: (String, bool, Match),
1209        open: bool,
1210        cx: &mut Context<Self>,
1211    ) {
1212        let (maybe_url_or_path, is_url, url_match) = hyperlink;
1213        let prev_hovered_word = self.last_content.last_hovered_word.take();
1214
1215        let target = if is_url {
1216            if let Some(path) = maybe_url_or_path.strip_prefix("file://") {
1217                let decoded_path = urlencoding::decode(path)
1218                    .map(|decoded| decoded.into_owned())
1219                    .unwrap_or(path.to_owned());
1220
1221                MaybeNavigationTarget::PathLike(PathLikeTarget {
1222                    maybe_path: decoded_path,
1223                    terminal_dir: self.working_directory(),
1224                })
1225            } else {
1226                MaybeNavigationTarget::Url(maybe_url_or_path.clone())
1227            }
1228        } else {
1229            MaybeNavigationTarget::PathLike(PathLikeTarget {
1230                maybe_path: maybe_url_or_path.clone(),
1231                terminal_dir: self.working_directory(),
1232            })
1233        };
1234
1235        if open {
1236            cx.emit(Event::Open(target));
1237        } else {
1238            self.update_selected_word(prev_hovered_word, url_match, maybe_url_or_path, target, cx);
1239        }
1240    }
1241
1242    fn update_selected_word(
1243        &mut self,
1244        prev_word: Option<HoveredWord>,
1245        word_match: RangeInclusive<AlacPoint>,
1246        word: String,
1247        navigation_target: MaybeNavigationTarget,
1248        cx: &mut Context<Self>,
1249    ) {
1250        if let Some(prev_word) = prev_word
1251            && prev_word.word == word
1252            && prev_word.word_match == word_match
1253        {
1254            self.last_content.last_hovered_word = Some(HoveredWord {
1255                word,
1256                word_match,
1257                id: prev_word.id,
1258            });
1259            return;
1260        }
1261
1262        self.last_content.last_hovered_word = Some(HoveredWord {
1263            word,
1264            word_match,
1265            id: self.next_link_id(),
1266        });
1267        cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
1268        cx.notify()
1269    }
1270
1271    fn next_link_id(&mut self) -> usize {
1272        let res = self.next_link_id;
1273        self.next_link_id = self.next_link_id.wrapping_add(1);
1274        res
1275    }
1276
1277    pub fn last_content(&self) -> &TerminalContent {
1278        &self.last_content
1279    }
1280
1281    pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) {
1282        self.term_config.default_cursor_style = cursor_shape.into();
1283        self.term.lock().set_options(self.term_config.clone());
1284    }
1285
1286    pub fn write_output(&mut self, bytes: &[u8], cx: &mut Context<Self>) {
1287        // Inject bytes directly into the terminal emulator and refresh the UI.
1288        // This bypasses the PTY/event loop for display-only terminals.
1289        //
1290        // We first convert LF to CRLF, to get the expected line wrapping in Alacritty.
1291        // When output comes from piped commands (not a PTY) such as codex-acp, and that
1292        // output only contains LF (\n) without a CR (\r) after it, such as the output
1293        // of the `ls` command when running outside a PTY, Alacritty moves the cursor
1294        // cursor down a line but does not move it back to the initial column. This makes
1295        // the rendered output look ridiculous. To prevent this, we insert a CR (\r) before
1296        // each LF that didn't already have one. (Alacritty doesn't have a setting for this.)
1297        let mut converted = Vec::with_capacity(bytes.len());
1298        let mut prev_byte = 0u8;
1299        for &byte in bytes {
1300            if byte == b'\n' && prev_byte != b'\r' {
1301                converted.push(b'\r');
1302            }
1303            converted.push(byte);
1304            prev_byte = byte;
1305        }
1306
1307        let mut processor = alacritty_terminal::vte::ansi::Processor::<
1308            alacritty_terminal::vte::ansi::StdSyncHandler,
1309        >::new();
1310        {
1311            let mut term = self.term.lock();
1312            processor.advance(&mut *term, &converted);
1313        }
1314        cx.emit(Event::Wakeup);
1315    }
1316
1317    pub fn total_lines(&self) -> usize {
1318        self.term.lock_unfair().total_lines()
1319    }
1320
1321    pub fn viewport_lines(&self) -> usize {
1322        self.term.lock_unfair().screen_lines()
1323    }
1324
1325    //To test:
1326    //- Activate match on terminal (scrolling and selection)
1327    //- Editor search snapping behavior
1328
1329    pub fn activate_match(&mut self, index: usize) {
1330        if let Some(search_match) = self.matches.get(index).cloned() {
1331            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
1332            if self.vi_mode_enabled {
1333                self.events
1334                    .push_back(InternalEvent::MoveViCursorToAlacPoint(*search_match.end()));
1335            } else {
1336                self.events
1337                    .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
1338            }
1339        }
1340    }
1341
1342    pub fn select_matches(&mut self, matches: &[RangeInclusive<AlacPoint>]) {
1343        let matches_to_select = self
1344            .matches
1345            .iter()
1346            .filter(|self_match| matches.contains(self_match))
1347            .cloned()
1348            .collect::<Vec<_>>();
1349        for match_to_select in matches_to_select {
1350            self.set_selection(Some((
1351                make_selection(&match_to_select),
1352                *match_to_select.end(),
1353            )));
1354        }
1355    }
1356
1357    pub fn select_all(&mut self) {
1358        let term = self.term.lock();
1359        let start = AlacPoint::new(term.topmost_line(), Column(0));
1360        let end = AlacPoint::new(term.bottommost_line(), term.last_column());
1361        drop(term);
1362        self.set_selection(Some((make_selection(&(start..=end)), end)));
1363    }
1364
1365    fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) {
1366        self.events
1367            .push_back(InternalEvent::SetSelection(selection));
1368    }
1369
1370    pub fn copy(&mut self, keep_selection: Option<bool>) {
1371        self.events.push_back(InternalEvent::Copy(keep_selection));
1372    }
1373
1374    pub fn clear(&mut self) {
1375        self.events.push_back(InternalEvent::Clear)
1376    }
1377
1378    pub fn scroll_line_up(&mut self) {
1379        self.events
1380            .push_back(InternalEvent::Scroll(AlacScroll::Delta(1)));
1381    }
1382
1383    pub fn scroll_up_by(&mut self, lines: usize) {
1384        self.events
1385            .push_back(InternalEvent::Scroll(AlacScroll::Delta(lines as i32)));
1386    }
1387
1388    pub fn scroll_line_down(&mut self) {
1389        self.events
1390            .push_back(InternalEvent::Scroll(AlacScroll::Delta(-1)));
1391    }
1392
1393    pub fn scroll_down_by(&mut self, lines: usize) {
1394        self.events
1395            .push_back(InternalEvent::Scroll(AlacScroll::Delta(-(lines as i32))));
1396    }
1397
1398    pub fn scroll_page_up(&mut self) {
1399        self.events
1400            .push_back(InternalEvent::Scroll(AlacScroll::PageUp));
1401    }
1402
1403    pub fn scroll_page_down(&mut self) {
1404        self.events
1405            .push_back(InternalEvent::Scroll(AlacScroll::PageDown));
1406    }
1407
1408    pub fn scroll_to_top(&mut self) {
1409        self.events
1410            .push_back(InternalEvent::Scroll(AlacScroll::Top));
1411    }
1412
1413    pub fn scroll_to_bottom(&mut self) {
1414        self.events
1415            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
1416    }
1417
1418    pub fn scrolled_to_top(&self) -> bool {
1419        self.last_content.scrolled_to_top
1420    }
1421
1422    pub fn scrolled_to_bottom(&self) -> bool {
1423        self.last_content.scrolled_to_bottom
1424    }
1425
1426    ///Resize the terminal and the PTY.
1427    pub fn set_size(&mut self, new_bounds: TerminalBounds) {
1428        if self.last_content.terminal_bounds != new_bounds {
1429            self.events.push_back(InternalEvent::Resize(new_bounds))
1430        }
1431    }
1432
1433    /// Write the Input payload to the PTY, if applicable.
1434    /// (This is a no-op for display-only terminals.)
1435    fn write_to_pty(&self, input: impl Into<Cow<'static, [u8]>>) {
1436        if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type {
1437            let input = input.into();
1438            if log::log_enabled!(log::Level::Debug) {
1439                if let Ok(str) = str::from_utf8(&input) {
1440                    log::debug!("Writing to PTY: {:?}", str);
1441                } else {
1442                    log::debug!("Writing to PTY: {:?}", input);
1443                }
1444            }
1445            pty_tx.notify(input);
1446        }
1447    }
1448
1449    pub fn input(&mut self, input: impl Into<Cow<'static, [u8]>>) {
1450        self.events
1451            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
1452        self.events.push_back(InternalEvent::SetSelection(None));
1453
1454        self.write_to_pty(input);
1455    }
1456
1457    pub fn toggle_vi_mode(&mut self) {
1458        self.events.push_back(InternalEvent::ToggleViMode);
1459    }
1460
1461    pub fn vi_motion(&mut self, keystroke: &Keystroke) {
1462        if !self.vi_mode_enabled {
1463            return;
1464        }
1465
1466        let key: Cow<'_, str> = if keystroke.modifiers.shift {
1467            Cow::Owned(keystroke.key.to_uppercase())
1468        } else {
1469            Cow::Borrowed(keystroke.key.as_str())
1470        };
1471
1472        let motion: Option<ViMotion> = match key.as_ref() {
1473            "h" | "left" => Some(ViMotion::Left),
1474            "j" | "down" => Some(ViMotion::Down),
1475            "k" | "up" => Some(ViMotion::Up),
1476            "l" | "right" => Some(ViMotion::Right),
1477            "w" => Some(ViMotion::WordRight),
1478            "b" if !keystroke.modifiers.control => Some(ViMotion::WordLeft),
1479            "e" => Some(ViMotion::WordRightEnd),
1480            "%" => Some(ViMotion::Bracket),
1481            "$" => Some(ViMotion::Last),
1482            "0" => Some(ViMotion::First),
1483            "^" => Some(ViMotion::FirstOccupied),
1484            "H" => Some(ViMotion::High),
1485            "M" => Some(ViMotion::Middle),
1486            "L" => Some(ViMotion::Low),
1487            _ => None,
1488        };
1489
1490        if let Some(motion) = motion {
1491            let cursor = self.last_content.cursor.point;
1492            let cursor_pos = Point {
1493                x: cursor.column.0 as f32 * self.last_content.terminal_bounds.cell_width,
1494                y: cursor.line.0 as f32 * self.last_content.terminal_bounds.line_height,
1495            };
1496            self.events
1497                .push_back(InternalEvent::UpdateSelection(cursor_pos));
1498            self.events.push_back(InternalEvent::ViMotion(motion));
1499            return;
1500        }
1501
1502        let scroll_motion = match key.as_ref() {
1503            "g" => Some(AlacScroll::Top),
1504            "G" => Some(AlacScroll::Bottom),
1505            "b" if keystroke.modifiers.control => Some(AlacScroll::PageUp),
1506            "f" if keystroke.modifiers.control => Some(AlacScroll::PageDown),
1507            "d" if keystroke.modifiers.control => {
1508                let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2;
1509                Some(AlacScroll::Delta(-amount))
1510            }
1511            "u" if keystroke.modifiers.control => {
1512                let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2;
1513                Some(AlacScroll::Delta(amount))
1514            }
1515            _ => None,
1516        };
1517
1518        if let Some(scroll_motion) = scroll_motion {
1519            self.events.push_back(InternalEvent::Scroll(scroll_motion));
1520            return;
1521        }
1522
1523        match key.as_ref() {
1524            "v" => {
1525                let point = self.last_content.cursor.point;
1526                let selection_type = SelectionType::Simple;
1527                let side = AlacDirection::Right;
1528                let selection = Selection::new(selection_type, point, side);
1529                self.events
1530                    .push_back(InternalEvent::SetSelection(Some((selection, point))));
1531            }
1532
1533            "escape" => {
1534                self.events.push_back(InternalEvent::SetSelection(None));
1535            }
1536
1537            "y" => {
1538                self.copy(Some(false));
1539            }
1540
1541            "i" => {
1542                self.scroll_to_bottom();
1543                self.toggle_vi_mode();
1544            }
1545            _ => {}
1546        }
1547    }
1548
1549    pub fn try_keystroke(&mut self, keystroke: &Keystroke, option_as_meta: bool) -> bool {
1550        if self.vi_mode_enabled {
1551            self.vi_motion(keystroke);
1552            return true;
1553        }
1554
1555        // Keep default terminal behavior
1556        let esc = to_esc_str(keystroke, &self.last_content.mode, option_as_meta);
1557        if let Some(esc) = esc {
1558            match esc {
1559                Cow::Borrowed(string) => self.input(string.as_bytes()),
1560                Cow::Owned(string) => self.input(string.into_bytes()),
1561            };
1562            true
1563        } else {
1564            false
1565        }
1566    }
1567
1568    pub fn try_modifiers_change(
1569        &mut self,
1570        modifiers: &Modifiers,
1571        window: &Window,
1572        cx: &mut Context<Self>,
1573    ) {
1574        if self
1575            .last_content
1576            .terminal_bounds
1577            .bounds
1578            .contains(&window.mouse_position())
1579            && modifiers.secondary()
1580        {
1581            self.refresh_hovered_word(window);
1582        }
1583        cx.notify();
1584    }
1585
1586    ///Paste text into the terminal
1587    pub fn paste(&mut self, text: &str) {
1588        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
1589            format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
1590        } else {
1591            text.replace("\r\n", "\r").replace('\n', "\r")
1592        };
1593
1594        self.input(paste_text.into_bytes());
1595    }
1596
1597    pub fn sync(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1598        let term = self.term.clone();
1599        let mut terminal = term.lock_unfair();
1600        //Note that the ordering of events matters for event processing
1601        while let Some(e) = self.events.pop_front() {
1602            self.process_terminal_event(&e, &mut terminal, window, cx)
1603        }
1604
1605        self.last_content = Self::make_content(&terminal, &self.last_content);
1606    }
1607
1608    fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
1609        let content = term.renderable_content();
1610
1611        // Pre-allocate with estimated size to reduce reallocations
1612        let estimated_size = content.display_iter.size_hint().0;
1613        let mut cells = Vec::with_capacity(estimated_size);
1614
1615        cells.extend(content.display_iter.map(|ic| IndexedCell {
1616            point: ic.point,
1617            cell: ic.cell.clone(),
1618        }));
1619
1620        let selection_text = if content.selection.is_some() {
1621            term.selection_to_string()
1622        } else {
1623            None
1624        };
1625
1626        TerminalContent {
1627            cells,
1628            mode: content.mode,
1629            display_offset: content.display_offset,
1630            selection_text,
1631            selection: content.selection,
1632            cursor: content.cursor,
1633            cursor_char: term.grid()[content.cursor.point].c,
1634            terminal_bounds: last_content.terminal_bounds,
1635            last_hovered_word: last_content.last_hovered_word.clone(),
1636            scrolled_to_top: content.display_offset == term.history_size(),
1637            scrolled_to_bottom: content.display_offset == 0,
1638        }
1639    }
1640
1641    pub fn get_content(&self) -> String {
1642        let term = self.term.lock_unfair();
1643        let start = AlacPoint::new(term.topmost_line(), Column(0));
1644        let end = AlacPoint::new(term.bottommost_line(), term.last_column());
1645        term.bounds_to_string(start, end)
1646    }
1647
1648    pub fn last_n_non_empty_lines(&self, n: usize) -> Vec<String> {
1649        let term = self.term.clone();
1650        let terminal = term.lock_unfair();
1651        let grid = terminal.grid();
1652        let mut lines = Vec::new();
1653
1654        let mut current_line = grid.bottommost_line().0;
1655        let topmost_line = grid.topmost_line().0;
1656
1657        while current_line >= topmost_line && lines.len() < n {
1658            let logical_line_start = self.find_logical_line_start(grid, current_line, topmost_line);
1659            let logical_line = self.construct_logical_line(grid, logical_line_start, current_line);
1660
1661            if let Some(line) = self.process_line(logical_line) {
1662                lines.push(line);
1663            }
1664
1665            // Move to the line above the start of the current logical line
1666            current_line = logical_line_start - 1;
1667        }
1668
1669        lines.reverse();
1670        lines
1671    }
1672
1673    fn find_logical_line_start(&self, grid: &Grid<Cell>, current: i32, topmost: i32) -> i32 {
1674        let mut line_start = current;
1675        while line_start > topmost {
1676            let prev_line = Line(line_start - 1);
1677            let last_cell = &grid[prev_line][Column(grid.columns() - 1)];
1678            if !last_cell.flags.contains(Flags::WRAPLINE) {
1679                break;
1680            }
1681            line_start -= 1;
1682        }
1683        line_start
1684    }
1685
1686    fn construct_logical_line(&self, grid: &Grid<Cell>, start: i32, end: i32) -> String {
1687        let mut logical_line = String::new();
1688        for row in start..=end {
1689            let grid_row = &grid[Line(row)];
1690            logical_line.push_str(&row_to_string(grid_row));
1691        }
1692        logical_line
1693    }
1694
1695    fn process_line(&self, line: String) -> Option<String> {
1696        let trimmed = line.trim_end().to_string();
1697        if !trimmed.is_empty() {
1698            Some(trimmed)
1699        } else {
1700            None
1701        }
1702    }
1703
1704    pub fn focus_in(&self) {
1705        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1706            self.write_to_pty("\x1b[I".as_bytes());
1707        }
1708    }
1709
1710    pub fn focus_out(&mut self) {
1711        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1712            self.write_to_pty("\x1b[O".as_bytes());
1713        }
1714    }
1715
1716    pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool {
1717        match self.last_mouse {
1718            Some((old_point, old_side)) => {
1719                if old_point == point && old_side == side {
1720                    false
1721                } else {
1722                    self.last_mouse = Some((point, side));
1723                    true
1724                }
1725            }
1726            None => {
1727                self.last_mouse = Some((point, side));
1728                true
1729            }
1730        }
1731    }
1732
1733    pub fn mouse_mode(&self, shift: bool) -> bool {
1734        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
1735    }
1736
1737    pub fn mouse_move(&mut self, e: &MouseMoveEvent, cx: &mut Context<Self>) {
1738        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
1739        if self.mouse_mode(e.modifiers.shift) {
1740            let (point, side) = grid_point_and_side(
1741                position,
1742                self.last_content.terminal_bounds,
1743                self.last_content.display_offset,
1744            );
1745
1746            if self.mouse_changed(point, side)
1747                && let Some(bytes) =
1748                    mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode)
1749            {
1750                self.write_to_pty(bytes);
1751            }
1752        } else {
1753            self.schedule_find_hyperlink(e.modifiers, e.position);
1754        }
1755        cx.notify();
1756    }
1757
1758    fn schedule_find_hyperlink(&mut self, modifiers: Modifiers, position: Point<Pixels>) {
1759        if self.selection_phase == SelectionPhase::Selecting
1760            || !modifiers.secondary()
1761            || !self.last_content.terminal_bounds.bounds.contains(&position)
1762        {
1763            self.last_content.last_hovered_word = None;
1764            return;
1765        }
1766
1767        // Throttle hyperlink searches to avoid excessive processing
1768        let now = Instant::now();
1769        if self
1770            .last_hyperlink_search_position
1771            .map_or(true, |last_pos| {
1772                // Only search if mouse moved significantly or enough time passed
1773                let distance_moved = ((position.x - last_pos.x).abs()
1774                    + (position.y - last_pos.y).abs())
1775                    > FIND_HYPERLINK_THROTTLE_PX;
1776                let time_elapsed = now.duration_since(self.last_mouse_move_time).as_millis() > 100;
1777                distance_moved || time_elapsed
1778            })
1779        {
1780            self.last_mouse_move_time = now;
1781            self.last_hyperlink_search_position = Some(position);
1782            self.events.push_back(InternalEvent::FindHyperlink(
1783                position - self.last_content.terminal_bounds.bounds.origin,
1784                false,
1785            ));
1786        }
1787    }
1788
1789    pub fn select_word_at_event_position(&mut self, e: &MouseDownEvent) {
1790        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
1791        let (point, side) = grid_point_and_side(
1792            position,
1793            self.last_content.terminal_bounds,
1794            self.last_content.display_offset,
1795        );
1796        let selection = Selection::new(SelectionType::Semantic, point, side);
1797        self.events
1798            .push_back(InternalEvent::SetSelection(Some((selection, point))));
1799    }
1800
1801    pub fn mouse_drag(
1802        &mut self,
1803        e: &MouseMoveEvent,
1804        region: Bounds<Pixels>,
1805        cx: &mut Context<Self>,
1806    ) {
1807        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
1808        if !self.mouse_mode(e.modifiers.shift) {
1809            if let Some((.., hyperlink_range)) = &self.mouse_down_hyperlink {
1810                let point = grid_point(
1811                    position,
1812                    self.last_content.terminal_bounds,
1813                    self.last_content.display_offset,
1814                );
1815
1816                if !hyperlink_range.contains(&point) {
1817                    self.mouse_down_hyperlink = None;
1818                } else {
1819                    return;
1820                }
1821            }
1822
1823            self.selection_phase = SelectionPhase::Selecting;
1824            // Alacritty has the same ordering, of first updating the selection
1825            // then scrolling 15ms later
1826            self.events
1827                .push_back(InternalEvent::UpdateSelection(position));
1828
1829            // Doesn't make sense to scroll the alt screen
1830            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
1831                let scroll_lines = match self.drag_line_delta(e, region) {
1832                    Some(value) => value,
1833                    None => return,
1834                };
1835
1836                self.events
1837                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
1838            }
1839
1840            cx.notify();
1841        }
1842    }
1843
1844    fn drag_line_delta(&self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<i32> {
1845        let top = region.origin.y;
1846        let bottom = region.bottom_left().y;
1847
1848        let scroll_lines = if e.position.y < top {
1849            let scroll_delta = (top - e.position.y).pow(1.1);
1850            (scroll_delta / self.last_content.terminal_bounds.line_height).ceil() as i32
1851        } else if e.position.y > bottom {
1852            let scroll_delta = -((e.position.y - bottom).pow(1.1));
1853            (scroll_delta / self.last_content.terminal_bounds.line_height).floor() as i32
1854        } else {
1855            return None;
1856        };
1857
1858        Some(scroll_lines.clamp(-3, 3))
1859    }
1860
1861    pub fn mouse_down(&mut self, e: &MouseDownEvent, _cx: &mut Context<Self>) {
1862        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
1863        let point = grid_point(
1864            position,
1865            self.last_content.terminal_bounds,
1866            self.last_content.display_offset,
1867        );
1868
1869        if e.button == MouseButton::Left
1870            && e.modifiers.secondary()
1871            && !self.mouse_mode(e.modifiers.shift)
1872        {
1873            let term_lock = self.term.lock();
1874            self.mouse_down_hyperlink = terminal_hyperlinks::find_from_grid_point(
1875                &term_lock,
1876                point,
1877                &mut self.hyperlink_regex_searches,
1878                self.path_style,
1879            );
1880            drop(term_lock);
1881
1882            if self.mouse_down_hyperlink.is_some() {
1883                return;
1884            }
1885        }
1886
1887        if self.mouse_mode(e.modifiers.shift) {
1888            if let Some(bytes) =
1889                mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode)
1890            {
1891                self.write_to_pty(bytes);
1892            }
1893        } else {
1894            match e.button {
1895                MouseButton::Left => {
1896                    let (point, side) = grid_point_and_side(
1897                        position,
1898                        self.last_content.terminal_bounds,
1899                        self.last_content.display_offset,
1900                    );
1901
1902                    let selection_type = match e.click_count {
1903                        0 => return, //This is a release
1904                        1 => Some(SelectionType::Simple),
1905                        2 => Some(SelectionType::Semantic),
1906                        3 => Some(SelectionType::Lines),
1907                        _ => None,
1908                    };
1909
1910                    if selection_type == Some(SelectionType::Simple) && e.modifiers.shift {
1911                        self.events
1912                            .push_back(InternalEvent::UpdateSelection(position));
1913                        return;
1914                    }
1915
1916                    let selection = selection_type
1917                        .map(|selection_type| Selection::new(selection_type, point, side));
1918
1919                    if let Some(sel) = selection {
1920                        self.events
1921                            .push_back(InternalEvent::SetSelection(Some((sel, point))));
1922                    }
1923                }
1924                #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1925                MouseButton::Middle => {
1926                    if let Some(item) = _cx.read_from_primary() {
1927                        let text = item.text().unwrap_or_default();
1928                        self.input(text.into_bytes());
1929                    }
1930                }
1931                _ => {}
1932            }
1933        }
1934    }
1935
1936    pub fn mouse_up(&mut self, e: &MouseUpEvent, cx: &Context<Self>) {
1937        let setting = TerminalSettings::get_global(cx);
1938
1939        let position = e.position - self.last_content.terminal_bounds.bounds.origin;
1940        if self.mouse_mode(e.modifiers.shift) {
1941            let point = grid_point(
1942                position,
1943                self.last_content.terminal_bounds,
1944                self.last_content.display_offset,
1945            );
1946
1947            if let Some(bytes) =
1948                mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode)
1949            {
1950                self.write_to_pty(bytes);
1951            }
1952        } else {
1953            if e.button == MouseButton::Left && setting.copy_on_select {
1954                self.copy(Some(true));
1955            }
1956
1957            if let Some(mouse_down_hyperlink) = self.mouse_down_hyperlink.take() {
1958                let point = grid_point(
1959                    position,
1960                    self.last_content.terminal_bounds,
1961                    self.last_content.display_offset,
1962                );
1963
1964                if let Some(mouse_up_hyperlink) = {
1965                    let term_lock = self.term.lock();
1966                    terminal_hyperlinks::find_from_grid_point(
1967                        &term_lock,
1968                        point,
1969                        &mut self.hyperlink_regex_searches,
1970                        self.path_style,
1971                    )
1972                } {
1973                    if mouse_down_hyperlink == mouse_up_hyperlink {
1974                        self.events
1975                            .push_back(InternalEvent::ProcessHyperlink(mouse_up_hyperlink, true));
1976                        self.selection_phase = SelectionPhase::Ended;
1977                        self.last_mouse = None;
1978                        return;
1979                    }
1980                }
1981            }
1982
1983            //Hyperlinks
1984            if self.selection_phase == SelectionPhase::Ended {
1985                let mouse_cell_index =
1986                    content_index_for_mouse(position, &self.last_content.terminal_bounds);
1987                if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
1988                    cx.open_url(link.uri());
1989                } else if e.modifiers.secondary() {
1990                    self.events
1991                        .push_back(InternalEvent::FindHyperlink(position, true));
1992                }
1993            }
1994        }
1995
1996        self.selection_phase = SelectionPhase::Ended;
1997        self.last_mouse = None;
1998    }
1999
2000    ///Scroll the terminal
2001    pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, scroll_multiplier: f32) {
2002        let mouse_mode = self.mouse_mode(e.shift);
2003        let scroll_multiplier = if mouse_mode { 1. } else { scroll_multiplier };
2004
2005        if let Some(scroll_lines) = self.determine_scroll_lines(e, scroll_multiplier)
2006            && scroll_lines != 0
2007        {
2008            if mouse_mode {
2009                let point = grid_point(
2010                    e.position - self.last_content.terminal_bounds.bounds.origin,
2011                    self.last_content.terminal_bounds,
2012                    self.last_content.display_offset,
2013                );
2014
2015                if let Some(scrolls) = scroll_report(point, scroll_lines, e, self.last_content.mode)
2016                {
2017                    for scroll in scrolls {
2018                        self.write_to_pty(scroll);
2019                    }
2020                };
2021            } else if self
2022                .last_content
2023                .mode
2024                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
2025                && !e.shift
2026            {
2027                self.write_to_pty(alt_scroll(scroll_lines));
2028            } else {
2029                let scroll = AlacScroll::Delta(scroll_lines);
2030
2031                self.events.push_back(InternalEvent::Scroll(scroll));
2032            }
2033        }
2034    }
2035
2036    fn refresh_hovered_word(&mut self, window: &Window) {
2037        self.schedule_find_hyperlink(window.modifiers(), window.mouse_position());
2038    }
2039
2040    fn determine_scroll_lines(
2041        &mut self,
2042        e: &ScrollWheelEvent,
2043        scroll_multiplier: f32,
2044    ) -> Option<i32> {
2045        let line_height = self.last_content.terminal_bounds.line_height;
2046        match e.touch_phase {
2047            /* Reset scroll state on started */
2048            TouchPhase::Started => {
2049                self.scroll_px = px(0.);
2050                None
2051            }
2052            /* Calculate the appropriate scroll lines */
2053            TouchPhase::Moved => {
2054                let old_offset = (self.scroll_px / line_height) as i32;
2055
2056                self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier;
2057
2058                let new_offset = (self.scroll_px / line_height) as i32;
2059
2060                // Whenever we hit the edges, reset our stored scroll to 0
2061                // so we can respond to changes in direction quickly
2062                self.scroll_px %= self.last_content.terminal_bounds.height();
2063
2064                Some(new_offset - old_offset)
2065            }
2066            TouchPhase::Ended => None,
2067        }
2068    }
2069
2070    pub fn find_matches(
2071        &self,
2072        mut searcher: RegexSearch,
2073        cx: &Context<Self>,
2074    ) -> Task<Vec<RangeInclusive<AlacPoint>>> {
2075        let term = self.term.clone();
2076        cx.background_spawn(async move {
2077            let term = term.lock();
2078
2079            all_search_matches(&term, &mut searcher).collect()
2080        })
2081    }
2082
2083    pub fn working_directory(&self) -> Option<PathBuf> {
2084        if self.is_remote_terminal {
2085            // We can't yet reliably detect the working directory of a shell on the
2086            // SSH host. Until we can do that, it doesn't make sense to display
2087            // the working directory on the client and persist that.
2088            None
2089        } else {
2090            self.client_side_working_directory()
2091        }
2092    }
2093
2094    /// Returns the working directory of the process that's connected to the PTY.
2095    /// That means it returns the working directory of the local shell or program
2096    /// that's running inside the terminal.
2097    ///
2098    /// This does *not* return the working directory of the shell that runs on the
2099    /// remote host, in case Zed is connected to a remote host.
2100    fn client_side_working_directory(&self) -> Option<PathBuf> {
2101        match &self.terminal_type {
2102            TerminalType::Pty { info, .. } => info
2103                .current
2104                .read()
2105                .as_ref()
2106                .map(|process| process.cwd.clone()),
2107            TerminalType::DisplayOnly => None,
2108        }
2109    }
2110
2111    pub fn title(&self, truncate: bool) -> String {
2112        const MAX_CHARS: usize = 25;
2113        match &self.task {
2114            Some(task_state) => {
2115                if truncate {
2116                    truncate_and_trailoff(&task_state.spawned_task.label, MAX_CHARS)
2117                } else {
2118                    task_state.spawned_task.full_label.clone()
2119                }
2120            }
2121            None => self
2122                .title_override
2123                .as_ref()
2124                .map(|title_override| title_override.to_string())
2125                .unwrap_or_else(|| match &self.terminal_type {
2126                    TerminalType::Pty { info, .. } => info
2127                        .current
2128                        .read()
2129                        .as_ref()
2130                        .map(|fpi| {
2131                            let process_file = fpi
2132                                .cwd
2133                                .file_name()
2134                                .map(|name| name.to_string_lossy().into_owned())
2135                                .unwrap_or_default();
2136
2137                            let argv = fpi.argv.as_slice();
2138                            let process_name = format!(
2139                                "{}{}",
2140                                fpi.name,
2141                                if !argv.is_empty() {
2142                                    format!(" {}", (argv[1..]).join(" "))
2143                                } else {
2144                                    "".to_string()
2145                                }
2146                            );
2147                            let (process_file, process_name) = if truncate {
2148                                (
2149                                    truncate_and_trailoff(&process_file, MAX_CHARS),
2150                                    truncate_and_trailoff(&process_name, MAX_CHARS),
2151                                )
2152                            } else {
2153                                (process_file, process_name)
2154                            };
2155                            format!("{process_file}{process_name}")
2156                        })
2157                        .unwrap_or_else(|| "Terminal".to_string()),
2158                    TerminalType::DisplayOnly => "Terminal".to_string(),
2159                }),
2160        }
2161    }
2162
2163    pub fn kill_active_task(&mut self) {
2164        if let Some(task) = self.task()
2165            && task.status == TaskStatus::Running
2166        {
2167            if let TerminalType::Pty { info, .. } = &self.terminal_type {
2168                // First kill the foreground process group (the command running in the shell)
2169                info.kill_current_process();
2170                // Then kill the shell itself so that the terminal exits properly
2171                // and wait_for_completed_task can complete
2172                info.kill_child_process();
2173            }
2174        }
2175    }
2176
2177    pub fn pid(&self) -> Option<sysinfo::Pid> {
2178        match &self.terminal_type {
2179            TerminalType::Pty { info, .. } => info.pid(),
2180            TerminalType::DisplayOnly => None,
2181        }
2182    }
2183
2184    pub fn pid_getter(&self) -> Option<&ProcessIdGetter> {
2185        match &self.terminal_type {
2186            TerminalType::Pty { info, .. } => Some(info.pid_getter()),
2187            TerminalType::DisplayOnly => None,
2188        }
2189    }
2190
2191    pub fn task(&self) -> Option<&TaskState> {
2192        self.task.as_ref()
2193    }
2194
2195    pub fn wait_for_completed_task(&self, cx: &App) -> Task<Option<ExitStatus>> {
2196        if let Some(task) = self.task() {
2197            if task.status == TaskStatus::Running {
2198                let completion_receiver = task.completion_rx.clone();
2199                return cx.spawn(async move |_| completion_receiver.recv().await.ok().flatten());
2200            } else if let Ok(status) = task.completion_rx.try_recv() {
2201                return Task::ready(status);
2202            }
2203        }
2204        Task::ready(None)
2205    }
2206
2207    fn register_task_finished(&mut self, raw_status: Option<i32>, cx: &mut Context<Terminal>) {
2208        let exit_status: Option<ExitStatus> = raw_status.map(|value| {
2209            #[cfg(unix)]
2210            {
2211                std::os::unix::process::ExitStatusExt::from_raw(value)
2212            }
2213            #[cfg(windows)]
2214            {
2215                std::os::windows::process::ExitStatusExt::from_raw(value as u32)
2216            }
2217        });
2218
2219        if let Some(tx) = &self.completion_tx {
2220            tx.try_send(exit_status).ok();
2221        }
2222        if let Some(e) = exit_status {
2223            self.child_exited = Some(e);
2224        }
2225        let task = match &mut self.task {
2226            Some(task) => task,
2227            None => {
2228                if self.child_exited.is_none_or(|e| e.code() == Some(0)) {
2229                    cx.emit(Event::CloseTerminal);
2230                }
2231                return;
2232            }
2233        };
2234        if task.status != TaskStatus::Running {
2235            return;
2236        }
2237        match exit_status.and_then(|e| e.code()) {
2238            Some(error_code) => {
2239                task.status.register_task_exit(error_code);
2240            }
2241            None => {
2242                task.status.register_terminal_exit();
2243            }
2244        };
2245
2246        let (finished_successfully, task_line, command_line) = task_summary(task, exit_status);
2247        let mut lines_to_show = Vec::new();
2248        if task.spawned_task.show_summary {
2249            lines_to_show.push(task_line.as_str());
2250        }
2251        if task.spawned_task.show_command {
2252            lines_to_show.push(command_line.as_str());
2253        }
2254
2255        if !lines_to_show.is_empty() {
2256            // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once,
2257            // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned
2258            // when Zed task finishes and no more output is made.
2259            // After the task summary is output once, no more text is appended to the terminal.
2260            unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) };
2261        }
2262
2263        match task.spawned_task.hide {
2264            HideStrategy::Never => {}
2265            HideStrategy::Always => {
2266                cx.emit(Event::CloseTerminal);
2267            }
2268            HideStrategy::OnSuccess => {
2269                if finished_successfully {
2270                    cx.emit(Event::CloseTerminal);
2271                }
2272            }
2273        }
2274    }
2275
2276    pub fn vi_mode_enabled(&self) -> bool {
2277        self.vi_mode_enabled
2278    }
2279
2280    pub fn clone_builder(&self, cx: &App, cwd: Option<PathBuf>) -> Task<Result<TerminalBuilder>> {
2281        let working_directory = self.working_directory().or_else(|| cwd);
2282        TerminalBuilder::new(
2283            working_directory,
2284            None,
2285            self.template.shell.clone(),
2286            self.template.env.clone(),
2287            self.template.cursor_shape,
2288            self.template.alternate_scroll,
2289            self.template.max_scroll_history_lines,
2290            self.template.path_hyperlink_regexes.clone(),
2291            self.template.path_hyperlink_timeout_ms,
2292            self.is_remote_terminal,
2293            self.template.window_id,
2294            None,
2295            cx,
2296            self.activation_script.clone(),
2297            self.path_style,
2298        )
2299    }
2300}
2301
2302// Helper function to convert a grid row to a string
2303pub fn row_to_string(row: &Row<Cell>) -> String {
2304    row[..Column(row.len())]
2305        .iter()
2306        .map(|cell| cell.c)
2307        .collect::<String>()
2308}
2309
2310const TASK_DELIMITER: &str = "";
2311fn task_summary(task: &TaskState, exit_status: Option<ExitStatus>) -> (bool, String, String) {
2312    let escaped_full_label = task
2313        .spawned_task
2314        .full_label
2315        .replace("\r\n", "\r")
2316        .replace('\n', "\r");
2317    let task_label = |suffix: &str| format!("{TASK_DELIMITER}Task `{escaped_full_label}` {suffix}");
2318    let (success, task_line) = match exit_status {
2319        Some(status) => {
2320            let code = status.code();
2321            #[cfg(unix)]
2322            let signal = status.signal();
2323            #[cfg(not(unix))]
2324            let signal: Option<i32> = None;
2325
2326            match (code, signal) {
2327                (Some(0), _) => (true, task_label("finished successfully")),
2328                (Some(code), _) => (
2329                    false,
2330                    task_label(&format!("finished with exit code: {code}")),
2331                ),
2332                (None, Some(signal)) => (
2333                    false,
2334                    task_label(&format!("terminated by signal: {signal}")),
2335                ),
2336                (None, None) => (false, task_label("finished")),
2337            }
2338        }
2339        None => (false, task_label("finished")),
2340    };
2341    let escaped_command_label = task
2342        .spawned_task
2343        .command_label
2344        .replace("\r\n", "\r")
2345        .replace('\n', "\r");
2346    let command_line = format!("{TASK_DELIMITER}Command: {escaped_command_label}");
2347    (success, task_line, command_line)
2348}
2349
2350/// Appends a stringified task summary to the terminal, after its output.
2351///
2352/// SAFETY: This function should only be called after terminal's PTY is no longer alive.
2353/// New text being added to the terminal here, uses "less public" APIs,
2354/// which are not maintaining the entire terminal state intact.
2355///
2356///
2357/// The library
2358///
2359/// * does not increment inner grid cursor's _lines_ on `input` calls
2360///   (but displaying the lines correctly and incrementing cursor's columns)
2361///
2362/// * ignores `\n` and \r` character input, requiring the `newline` call instead
2363///
2364/// * does not alter grid state after `newline` call
2365///   so its `bottommost_line` is always the same additions, and
2366///   the cursor's `point` is not updated to the new line and column values
2367///
2368/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic.
2369///   Still, subsequent `append_text_to_term` invocations are possible and display the contents correctly.
2370///
2371/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations,
2372/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone.
2373/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized.
2374/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts.
2375unsafe fn append_text_to_term(term: &mut Term<ZedListener>, text_lines: &[&str]) {
2376    term.newline();
2377    term.grid_mut().cursor.point.column = Column(0);
2378    for line in text_lines {
2379        for c in line.chars() {
2380            term.input(c);
2381        }
2382        term.newline();
2383        term.grid_mut().cursor.point.column = Column(0);
2384    }
2385}
2386
2387impl Drop for Terminal {
2388    fn drop(&mut self) {
2389        if let TerminalType::Pty { pty_tx, info } =
2390            std::mem::replace(&mut self.terminal_type, TerminalType::DisplayOnly)
2391        {
2392            pty_tx.0.send(Msg::Shutdown).ok();
2393
2394            let timer = self.background_executor.timer(Duration::from_millis(100));
2395            self.background_executor
2396                .spawn(async move {
2397                    timer.await;
2398                    info.kill_child_process();
2399                })
2400                .detach();
2401        }
2402    }
2403}
2404
2405impl EventEmitter<Event> for Terminal {}
2406
2407fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
2408    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
2409    selection.update(*range.end(), AlacDirection::Right);
2410    selection
2411}
2412
2413fn all_search_matches<'a, T>(
2414    term: &'a Term<T>,
2415    regex: &'a mut RegexSearch,
2416) -> impl Iterator<Item = Match> + 'a {
2417    let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
2418    let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
2419    RegexIter::new(start, end, AlacDirection::Right, term, regex)
2420}
2421
2422fn content_index_for_mouse(pos: Point<Pixels>, terminal_bounds: &TerminalBounds) -> usize {
2423    let col = (pos.x / terminal_bounds.cell_width()).round() as usize;
2424    let clamped_col = min(col, terminal_bounds.columns() - 1);
2425    let row = (pos.y / terminal_bounds.line_height()).round() as usize;
2426    let clamped_row = min(row, terminal_bounds.screen_lines() - 1);
2427    clamped_row * terminal_bounds.columns() + clamped_col
2428}
2429
2430/// Converts an 8 bit ANSI color to its GPUI equivalent.
2431/// Accepts `usize` for compatibility with the `alacritty::Colors` interface,
2432/// Other than that use case, should only be called with values in the `[0,255]` range
2433pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla {
2434    let colors = theme.colors();
2435
2436    match index {
2437        // 0-15 are the same as the named colors above
2438        0 => colors.terminal_ansi_black,
2439        1 => colors.terminal_ansi_red,
2440        2 => colors.terminal_ansi_green,
2441        3 => colors.terminal_ansi_yellow,
2442        4 => colors.terminal_ansi_blue,
2443        5 => colors.terminal_ansi_magenta,
2444        6 => colors.terminal_ansi_cyan,
2445        7 => colors.terminal_ansi_white,
2446        8 => colors.terminal_ansi_bright_black,
2447        9 => colors.terminal_ansi_bright_red,
2448        10 => colors.terminal_ansi_bright_green,
2449        11 => colors.terminal_ansi_bright_yellow,
2450        12 => colors.terminal_ansi_bright_blue,
2451        13 => colors.terminal_ansi_bright_magenta,
2452        14 => colors.terminal_ansi_bright_cyan,
2453        15 => colors.terminal_ansi_bright_white,
2454        // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
2455        // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
2456        16..=231 => {
2457            let (r, g, b) = rgb_for_index(index as u8);
2458            rgba_color(
2459                if r == 0 { 0 } else { r * 40 + 55 },
2460                if g == 0 { 0 } else { g * 40 + 55 },
2461                if b == 0 { 0 } else { b * 40 + 55 },
2462            )
2463        }
2464        // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
2465        232..=255 => {
2466            let i = index as u8 - 232; // Align index to 0..24
2467            let value = i * 10 + 8;
2468            rgba_color(value, value, value)
2469        }
2470        // For compatibility with the alacritty::Colors interface
2471        // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
2472        256 => colors.terminal_foreground,
2473        257 => colors.terminal_background,
2474        258 => theme.players().local().cursor,
2475        259 => colors.terminal_ansi_dim_black,
2476        260 => colors.terminal_ansi_dim_red,
2477        261 => colors.terminal_ansi_dim_green,
2478        262 => colors.terminal_ansi_dim_yellow,
2479        263 => colors.terminal_ansi_dim_blue,
2480        264 => colors.terminal_ansi_dim_magenta,
2481        265 => colors.terminal_ansi_dim_cyan,
2482        266 => colors.terminal_ansi_dim_white,
2483        267 => colors.terminal_bright_foreground,
2484        268 => colors.terminal_ansi_black, // 'Dim Background', non-standard color
2485
2486        _ => black(),
2487    }
2488}
2489
2490/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube.
2491///
2492/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
2493///
2494/// Wikipedia gives a formula for calculating the index for a given color:
2495///
2496/// ```text
2497/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
2498/// ```
2499///
2500/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index.
2501fn rgb_for_index(i: u8) -> (u8, u8, u8) {
2502    debug_assert!((16..=231).contains(&i));
2503    let i = i - 16;
2504    let r = (i - (i % 36)) / 36;
2505    let g = ((i % 36) - (i % 6)) / 6;
2506    let b = (i % 36) % 6;
2507    (r, g, b)
2508}
2509
2510pub fn rgba_color(r: u8, g: u8, b: u8) -> Hsla {
2511    Rgba {
2512        r: (r as f32 / 255.),
2513        g: (g as f32 / 255.),
2514        b: (b as f32 / 255.),
2515        a: 1.,
2516    }
2517    .into()
2518}
2519
2520#[cfg(test)]
2521mod tests {
2522    use std::time::Duration;
2523
2524    use super::*;
2525    use crate::{
2526        IndexedCell, TerminalBounds, TerminalBuilder, TerminalContent, content_index_for_mouse,
2527        rgb_for_index,
2528    };
2529    use alacritty_terminal::{
2530        index::{Column, Line, Point as AlacPoint},
2531        term::cell::Cell,
2532    };
2533    use collections::HashMap;
2534    use gpui::{
2535        Entity, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
2536        Point, TestAppContext, bounds, point, size,
2537    };
2538    use parking_lot::Mutex;
2539    use rand::{Rng, distr, rngs::ThreadRng};
2540    use smol::channel::Receiver;
2541    use task::{Shell, ShellBuilder};
2542
2543    #[cfg(target_os = "macos")]
2544    fn init_test(cx: &mut TestAppContext) {
2545        cx.update(|cx| {
2546            let settings_store = settings::SettingsStore::test(cx);
2547            cx.set_global(settings_store);
2548            theme::init(theme::LoadThemes::JustBase, cx);
2549        });
2550    }
2551
2552    /// Helper to build a test terminal running a shell command.
2553    /// Returns the terminal entity and a receiver for the completion signal.
2554    async fn build_test_terminal(
2555        cx: &mut TestAppContext,
2556        command: &str,
2557        args: &[&str],
2558    ) -> (Entity<Terminal>, Receiver<Option<ExitStatus>>) {
2559        let (completion_tx, completion_rx) = smol::channel::unbounded();
2560        let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
2561        let (program, args) =
2562            ShellBuilder::new(&Shell::System, false).build(Some(command.to_owned()), &args);
2563        let builder = cx
2564            .update(|cx| {
2565                TerminalBuilder::new(
2566                    None,
2567                    None,
2568                    task::Shell::WithArguments {
2569                        program,
2570                        args,
2571                        title_override: None,
2572                    },
2573                    HashMap::default(),
2574                    CursorShape::default(),
2575                    AlternateScroll::On,
2576                    None,
2577                    vec![],
2578                    0,
2579                    false,
2580                    0,
2581                    Some(completion_tx),
2582                    cx,
2583                    vec![],
2584                    PathStyle::local(),
2585                )
2586            })
2587            .await
2588            .unwrap();
2589        let terminal = cx.new(|cx| builder.subscribe(cx));
2590        (terminal, completion_rx)
2591    }
2592
2593    fn init_ctrl_click_hyperlink_test(cx: &mut TestAppContext, output: &[u8]) -> Entity<Terminal> {
2594        cx.update(|cx| {
2595            let settings_store = settings::SettingsStore::test(cx);
2596            cx.set_global(settings_store);
2597        });
2598
2599        let terminal = cx.new(|cx| {
2600            TerminalBuilder::new_display_only(
2601                CursorShape::default(),
2602                AlternateScroll::On,
2603                None,
2604                0,
2605                cx.background_executor(),
2606                PathStyle::local(),
2607            )
2608            .unwrap()
2609            .subscribe(cx)
2610        });
2611
2612        terminal.update(cx, |terminal, cx| {
2613            terminal.write_output(output, cx);
2614        });
2615
2616        cx.run_until_parked();
2617
2618        terminal.update(cx, |terminal, _cx| {
2619            let term_lock = terminal.term.lock();
2620            terminal.last_content = Terminal::make_content(&term_lock, &terminal.last_content);
2621            drop(term_lock);
2622
2623            let terminal_bounds = TerminalBounds::new(
2624                px(20.0),
2625                px(10.0),
2626                bounds(point(px(0.0), px(0.0)), size(px(400.0), px(400.0))),
2627            );
2628            terminal.last_content.terminal_bounds = terminal_bounds;
2629            terminal.events.clear();
2630        });
2631
2632        terminal
2633    }
2634
2635    fn ctrl_mouse_down_at(
2636        terminal: &mut Terminal,
2637        position: Point<Pixels>,
2638        cx: &mut Context<Terminal>,
2639    ) {
2640        let mouse_down = MouseDownEvent {
2641            button: MouseButton::Left,
2642            position,
2643            modifiers: Modifiers::secondary_key(),
2644            click_count: 1,
2645            first_mouse: true,
2646        };
2647        terminal.mouse_down(&mouse_down, cx);
2648    }
2649
2650    fn ctrl_mouse_move_to(
2651        terminal: &mut Terminal,
2652        position: Point<Pixels>,
2653        cx: &mut Context<Terminal>,
2654    ) {
2655        let terminal_bounds = terminal.last_content.terminal_bounds.bounds;
2656        let drag_event = MouseMoveEvent {
2657            position,
2658            pressed_button: Some(MouseButton::Left),
2659            modifiers: Modifiers::secondary_key(),
2660        };
2661        terminal.mouse_drag(&drag_event, terminal_bounds, cx);
2662    }
2663
2664    fn ctrl_mouse_up_at(
2665        terminal: &mut Terminal,
2666        position: Point<Pixels>,
2667        cx: &mut Context<Terminal>,
2668    ) {
2669        let mouse_up = MouseUpEvent {
2670            button: MouseButton::Left,
2671            position,
2672            modifiers: Modifiers::secondary_key(),
2673            click_count: 1,
2674        };
2675        terminal.mouse_up(&mouse_up, cx);
2676    }
2677
2678    #[gpui::test]
2679    async fn test_basic_terminal(cx: &mut TestAppContext) {
2680        cx.executor().allow_parking();
2681
2682        let (terminal, completion_rx) = build_test_terminal(cx, "echo", &["hello"]).await;
2683        assert_eq!(
2684            completion_rx.recv().await.unwrap(),
2685            Some(ExitStatus::default())
2686        );
2687        assert_eq!(
2688            terminal.update(cx, |term, _| term.get_content()).trim(),
2689            "hello"
2690        );
2691
2692        // Inject additional output directly into the emulator (display-only path)
2693        terminal.update(cx, |term, cx| {
2694            term.write_output(b"\nfrom_injection", cx);
2695        });
2696
2697        let content_after = terminal.update(cx, |term, _| term.get_content());
2698        assert!(
2699            content_after.contains("from_injection"),
2700            "expected injected output to appear, got: {content_after}"
2701        );
2702    }
2703
2704    // TODO should be tested on Linux too, but does not work there well
2705    #[cfg(target_os = "macos")]
2706    #[gpui::test(iterations = 10)]
2707    async fn test_terminal_eof(cx: &mut TestAppContext) {
2708        init_test(cx);
2709
2710        cx.executor().allow_parking();
2711
2712        let (completion_tx, completion_rx) = smol::channel::unbounded();
2713        let builder = cx
2714            .update(|cx| {
2715                TerminalBuilder::new(
2716                    None,
2717                    None,
2718                    task::Shell::System,
2719                    HashMap::default(),
2720                    CursorShape::default(),
2721                    AlternateScroll::On,
2722                    None,
2723                    vec![],
2724                    0,
2725                    false,
2726                    0,
2727                    Some(completion_tx),
2728                    cx,
2729                    Vec::new(),
2730                    PathStyle::local(),
2731                )
2732            })
2733            .await
2734            .unwrap();
2735        // Build an empty command, which will result in a tty shell spawned.
2736        let terminal = cx.new(|cx| builder.subscribe(cx));
2737
2738        let (event_tx, event_rx) = smol::channel::unbounded::<Event>();
2739        cx.update(|cx| {
2740            cx.subscribe(&terminal, move |_, e, _| {
2741                event_tx.send_blocking(e.clone()).unwrap();
2742            })
2743        })
2744        .detach();
2745        cx.background_spawn(async move {
2746            assert_eq!(
2747                completion_rx.recv().await.unwrap(),
2748                Some(ExitStatus::default()),
2749                "EOF should result in the tty shell exiting successfully",
2750            );
2751        })
2752        .detach();
2753
2754        let first_event = event_rx.recv().await.expect("No wakeup event received");
2755
2756        terminal.update(cx, |terminal, _| {
2757            let success = terminal.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false);
2758            assert!(success, "Should have registered ctrl-c sequence");
2759        });
2760        terminal.update(cx, |terminal, _| {
2761            let success = terminal.try_keystroke(&Keystroke::parse("ctrl-d").unwrap(), false);
2762            assert!(success, "Should have registered ctrl-d sequence");
2763        });
2764
2765        let mut all_events = vec![first_event];
2766        while let Ok(new_event) = event_rx.recv().await {
2767            all_events.push(new_event.clone());
2768            if new_event == Event::CloseTerminal {
2769                break;
2770            }
2771        }
2772        assert!(
2773            all_events.contains(&Event::CloseTerminal),
2774            "EOF command sequence should have triggered a TTY terminal exit, but got events: {all_events:?}",
2775        );
2776    }
2777
2778    #[gpui::test(iterations = 10)]
2779    async fn test_terminal_no_exit_on_spawn_failure(cx: &mut TestAppContext) {
2780        cx.executor().allow_parking();
2781
2782        let (completion_tx, completion_rx) = smol::channel::unbounded();
2783        let (program, args) = ShellBuilder::new(&Shell::System, false)
2784            .build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]);
2785        let builder = cx
2786            .update(|cx| {
2787                TerminalBuilder::new(
2788                    None,
2789                    None,
2790                    task::Shell::WithArguments {
2791                        program,
2792                        args,
2793                        title_override: None,
2794                    },
2795                    HashMap::default(),
2796                    CursorShape::default(),
2797                    AlternateScroll::On,
2798                    None,
2799                    Vec::new(),
2800                    0,
2801                    false,
2802                    0,
2803                    Some(completion_tx),
2804                    cx,
2805                    Vec::new(),
2806                    PathStyle::local(),
2807                )
2808            })
2809            .await
2810            .unwrap();
2811        let terminal = cx.new(|cx| builder.subscribe(cx));
2812
2813        let all_events: Arc<Mutex<Vec<Event>>> = Arc::new(Mutex::new(Vec::new()));
2814        cx.update({
2815            let all_events = all_events.clone();
2816            |cx| {
2817                cx.subscribe(&terminal, move |_, e, _| {
2818                    all_events.lock().push(e.clone());
2819                })
2820            }
2821        })
2822        .detach();
2823        let completion_check_task = cx.background_spawn(async move {
2824            // The channel may be closed if the terminal is dropped before sending
2825            // the completion signal, which can happen with certain task scheduling orders.
2826            let exit_status = completion_rx.recv().await.ok().flatten();
2827            if let Some(exit_status) = exit_status {
2828                assert!(
2829                    !exit_status.success(),
2830                    "Wrong shell command should result in a failure"
2831                );
2832                #[cfg(target_os = "windows")]
2833                assert_eq!(exit_status.code(), Some(1));
2834                #[cfg(not(target_os = "windows"))]
2835                assert_eq!(exit_status.code(), Some(127)); // code 127 means "command not found" on Unix
2836            }
2837        });
2838
2839        completion_check_task.await;
2840        cx.executor().timer(Duration::from_millis(500)).await;
2841
2842        assert!(
2843            !all_events
2844                .lock()
2845                .iter()
2846                .any(|event| event == &Event::CloseTerminal),
2847            "Wrong shell command should update the title but not should not close the terminal to show the error message, but got events: {all_events:?}",
2848        );
2849    }
2850
2851    #[test]
2852    fn test_rgb_for_index() {
2853        // Test every possible value in the color cube.
2854        for i in 16..=231 {
2855            let (r, g, b) = rgb_for_index(i);
2856            assert_eq!(i, 16 + 36 * r + 6 * g + b);
2857        }
2858    }
2859
2860    #[test]
2861    fn test_mouse_to_cell_test() {
2862        let mut rng = rand::rng();
2863        const ITERATIONS: usize = 10;
2864        const PRECISION: usize = 1000;
2865
2866        for _ in 0..ITERATIONS {
2867            let viewport_cells = rng.random_range(15..20);
2868            let cell_size =
2869                rng.random_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
2870
2871            let size = crate::TerminalBounds {
2872                cell_width: Pixels::from(cell_size),
2873                line_height: Pixels::from(cell_size),
2874                bounds: bounds(
2875                    Point::default(),
2876                    size(
2877                        Pixels::from(cell_size * (viewport_cells as f32)),
2878                        Pixels::from(cell_size * (viewport_cells as f32)),
2879                    ),
2880                ),
2881            };
2882
2883            let cells = get_cells(size, &mut rng);
2884            let content = convert_cells_to_content(size, &cells);
2885
2886            for row in 0..(viewport_cells - 1) {
2887                let row = row as usize;
2888                for col in 0..(viewport_cells - 1) {
2889                    let col = col as usize;
2890
2891                    let row_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32;
2892                    let col_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32;
2893
2894                    let mouse_pos = point(
2895                        Pixels::from(col as f32 * cell_size + col_offset),
2896                        Pixels::from(row as f32 * cell_size + row_offset),
2897                    );
2898
2899                    let content_index =
2900                        content_index_for_mouse(mouse_pos, &content.terminal_bounds);
2901                    let mouse_cell = content.cells[content_index].c;
2902                    let real_cell = cells[row][col];
2903
2904                    assert_eq!(mouse_cell, real_cell);
2905                }
2906            }
2907        }
2908    }
2909
2910    #[test]
2911    fn test_mouse_to_cell_clamp() {
2912        let mut rng = rand::rng();
2913
2914        let size = crate::TerminalBounds {
2915            cell_width: Pixels::from(10.),
2916            line_height: Pixels::from(10.),
2917            bounds: bounds(
2918                Point::default(),
2919                size(Pixels::from(100.), Pixels::from(100.)),
2920            ),
2921        };
2922
2923        let cells = get_cells(size, &mut rng);
2924        let content = convert_cells_to_content(size, &cells);
2925
2926        assert_eq!(
2927            content.cells[content_index_for_mouse(
2928                point(Pixels::from(-10.), Pixels::from(-10.)),
2929                &content.terminal_bounds,
2930            )]
2931            .c,
2932            cells[0][0]
2933        );
2934        assert_eq!(
2935            content.cells[content_index_for_mouse(
2936                point(Pixels::from(1000.), Pixels::from(1000.)),
2937                &content.terminal_bounds,
2938            )]
2939            .c,
2940            cells[9][9]
2941        );
2942    }
2943
2944    fn get_cells(size: TerminalBounds, rng: &mut ThreadRng) -> Vec<Vec<char>> {
2945        let mut cells = Vec::new();
2946
2947        for _ in 0..((size.height() / size.line_height()) as usize) {
2948            let mut row_vec = Vec::new();
2949            for _ in 0..((size.width() / size.cell_width()) as usize) {
2950                let cell_char = rng.sample(distr::Alphanumeric) as char;
2951                row_vec.push(cell_char)
2952            }
2953            cells.push(row_vec)
2954        }
2955
2956        cells
2957    }
2958
2959    fn convert_cells_to_content(
2960        terminal_bounds: TerminalBounds,
2961        cells: &[Vec<char>],
2962    ) -> TerminalContent {
2963        let mut ic = Vec::new();
2964
2965        for (index, row) in cells.iter().enumerate() {
2966            for (cell_index, cell_char) in row.iter().enumerate() {
2967                ic.push(IndexedCell {
2968                    point: AlacPoint::new(Line(index as i32), Column(cell_index)),
2969                    cell: Cell {
2970                        c: *cell_char,
2971                        ..Default::default()
2972                    },
2973                });
2974            }
2975        }
2976
2977        TerminalContent {
2978            cells: ic,
2979            terminal_bounds,
2980            ..Default::default()
2981        }
2982    }
2983
2984    #[gpui::test]
2985    async fn test_write_output_converts_lf_to_crlf(cx: &mut TestAppContext) {
2986        let terminal = cx.new(|cx| {
2987            TerminalBuilder::new_display_only(
2988                CursorShape::default(),
2989                AlternateScroll::On,
2990                None,
2991                0,
2992                cx.background_executor(),
2993                PathStyle::local(),
2994            )
2995            .unwrap()
2996            .subscribe(cx)
2997        });
2998
2999        // Test simple LF conversion
3000        terminal.update(cx, |terminal, cx| {
3001            terminal.write_output(b"line1\nline2\n", cx);
3002        });
3003
3004        // Get the content by directly accessing the term
3005        let content = terminal.update(cx, |terminal, _cx| {
3006            let term = terminal.term.lock_unfair();
3007            Terminal::make_content(&term, &terminal.last_content)
3008        });
3009
3010        // If LF is properly converted to CRLF, each line should start at column 0
3011        // The diagonal staircase bug would cause increasing column positions
3012
3013        // Get the cells and check that lines start at column 0
3014        let cells = &content.cells;
3015        let mut line1_col0 = false;
3016        let mut line2_col0 = false;
3017
3018        for cell in cells {
3019            if cell.c == 'l' && cell.point.column.0 == 0 {
3020                if cell.point.line.0 == 0 && !line1_col0 {
3021                    line1_col0 = true;
3022                } else if cell.point.line.0 == 1 && !line2_col0 {
3023                    line2_col0 = true;
3024                }
3025            }
3026        }
3027
3028        assert!(line1_col0, "First line should start at column 0");
3029        assert!(line2_col0, "Second line should start at column 0");
3030    }
3031
3032    #[gpui::test]
3033    async fn test_write_output_preserves_existing_crlf(cx: &mut TestAppContext) {
3034        let terminal = cx.new(|cx| {
3035            TerminalBuilder::new_display_only(
3036                CursorShape::default(),
3037                AlternateScroll::On,
3038                None,
3039                0,
3040                cx.background_executor(),
3041                PathStyle::local(),
3042            )
3043            .unwrap()
3044            .subscribe(cx)
3045        });
3046
3047        // Test that existing CRLF doesn't get doubled
3048        terminal.update(cx, |terminal, cx| {
3049            terminal.write_output(b"line1\r\nline2\r\n", cx);
3050        });
3051
3052        // Get the content by directly accessing the term
3053        let content = terminal.update(cx, |terminal, _cx| {
3054            let term = terminal.term.lock_unfair();
3055            Terminal::make_content(&term, &terminal.last_content)
3056        });
3057
3058        let cells = &content.cells;
3059
3060        // Check that both lines start at column 0
3061        let mut found_lines_at_column_0 = 0;
3062        for cell in cells {
3063            if cell.c == 'l' && cell.point.column.0 == 0 {
3064                found_lines_at_column_0 += 1;
3065            }
3066        }
3067
3068        assert!(
3069            found_lines_at_column_0 >= 2,
3070            "Both lines should start at column 0"
3071        );
3072    }
3073
3074    #[gpui::test]
3075    async fn test_write_output_preserves_bare_cr(cx: &mut TestAppContext) {
3076        let terminal = cx.new(|cx| {
3077            TerminalBuilder::new_display_only(
3078                CursorShape::default(),
3079                AlternateScroll::On,
3080                None,
3081                0,
3082                cx.background_executor(),
3083                PathStyle::local(),
3084            )
3085            .unwrap()
3086            .subscribe(cx)
3087        });
3088
3089        // Test that bare CR (without LF) is preserved
3090        terminal.update(cx, |terminal, cx| {
3091            terminal.write_output(b"hello\rworld", cx);
3092        });
3093
3094        // Get the content by directly accessing the term
3095        let content = terminal.update(cx, |terminal, _cx| {
3096            let term = terminal.term.lock_unfair();
3097            Terminal::make_content(&term, &terminal.last_content)
3098        });
3099
3100        let cells = &content.cells;
3101
3102        // Check that we have "world" at the beginning of the line
3103        let mut text = String::new();
3104        for cell in cells.iter().take(5) {
3105            if cell.point.line.0 == 0 {
3106                text.push(cell.c);
3107            }
3108        }
3109
3110        assert!(
3111            text.starts_with("world"),
3112            "Bare CR should allow overwriting: got '{}'",
3113            text
3114        );
3115    }
3116
3117    #[gpui::test]
3118    async fn test_hyperlink_ctrl_click_same_position(cx: &mut TestAppContext) {
3119        let terminal = init_ctrl_click_hyperlink_test(cx, b"Visit https://zed.dev/ for more\r\n");
3120
3121        terminal.update(cx, |terminal, cx| {
3122            let click_position = point(px(80.0), px(10.0));
3123            ctrl_mouse_down_at(terminal, click_position, cx);
3124            ctrl_mouse_up_at(terminal, click_position, cx);
3125
3126            assert!(
3127                terminal
3128                    .events
3129                    .iter()
3130                    .any(|event| matches!(event, InternalEvent::ProcessHyperlink(_, true))),
3131                "Should have ProcessHyperlink event when ctrl+clicking on same hyperlink position"
3132            );
3133        });
3134    }
3135
3136    #[gpui::test]
3137    async fn test_hyperlink_ctrl_click_drag_outside_bounds(cx: &mut TestAppContext) {
3138        let terminal = init_ctrl_click_hyperlink_test(
3139            cx,
3140            b"Visit https://zed.dev/ for more\r\nThis is another line\r\n",
3141        );
3142
3143        terminal.update(cx, |terminal, cx| {
3144            let down_position = point(px(80.0), px(10.0));
3145            let up_position = point(px(10.0), px(50.0));
3146
3147            ctrl_mouse_down_at(terminal, down_position, cx);
3148            ctrl_mouse_move_to(terminal, up_position, cx);
3149            ctrl_mouse_up_at(terminal, up_position, cx);
3150
3151            assert!(
3152                !terminal
3153                    .events
3154                    .iter()
3155                    .any(|event| matches!(event, InternalEvent::ProcessHyperlink(_, _))),
3156                "Should NOT have ProcessHyperlink event when dragging outside the hyperlink"
3157            );
3158        });
3159    }
3160
3161    #[gpui::test]
3162    async fn test_hyperlink_ctrl_click_drag_within_bounds(cx: &mut TestAppContext) {
3163        let terminal = init_ctrl_click_hyperlink_test(cx, b"Visit https://zed.dev/ for more\r\n");
3164
3165        terminal.update(cx, |terminal, cx| {
3166            let down_position = point(px(70.0), px(10.0));
3167            let up_position = point(px(130.0), px(10.0));
3168
3169            ctrl_mouse_down_at(terminal, down_position, cx);
3170            ctrl_mouse_move_to(terminal, up_position, cx);
3171            ctrl_mouse_up_at(terminal, up_position, cx);
3172
3173            assert!(
3174                terminal
3175                    .events
3176                    .iter()
3177                    .any(|event| matches!(event, InternalEvent::ProcessHyperlink(_, true))),
3178                "Should have ProcessHyperlink event when dragging within hyperlink bounds"
3179            );
3180        });
3181    }
3182
3183    /// Test that kill_active_task properly terminates both the foreground process
3184    /// and the shell, allowing wait_for_completed_task to complete and output to be captured.
3185    #[cfg(unix)]
3186    #[gpui::test]
3187    async fn test_kill_active_task_completes_and_captures_output(cx: &mut TestAppContext) {
3188        cx.executor().allow_parking();
3189
3190        // Run a command that prints output then sleeps for a long time
3191        // The echo ensures we have output to capture before killing
3192        let (terminal, completion_rx) =
3193            build_test_terminal(cx, "echo", &["test_output_before_kill; sleep 60"]).await;
3194
3195        // Wait a bit for the echo to execute and produce output
3196        cx.background_executor
3197            .timer(Duration::from_millis(200))
3198            .await;
3199
3200        // Kill the active task
3201        terminal.update(cx, |term, _cx| {
3202            term.kill_active_task();
3203        });
3204
3205        // wait_for_completed_task should complete within a reasonable time (not hang)
3206        let completion_result = completion_rx.recv().await;
3207        assert!(
3208            completion_result.is_ok(),
3209            "wait_for_completed_task should complete after kill_active_task, but it timed out"
3210        );
3211
3212        // The exit status should indicate the process was killed (not a clean exit)
3213        let exit_status = completion_result.unwrap();
3214        assert!(
3215            exit_status.is_some(),
3216            "Should have received an exit status after killing"
3217        );
3218
3219        // Verify that output captured before killing is still available
3220        let content = terminal.update(cx, |term, _| term.get_content());
3221        assert!(
3222            content.contains("test_output_before_kill"),
3223            "Output from before kill should be captured, got: {content}"
3224        );
3225    }
3226
3227    /// Test that kill_active_task on a task that's not running is a no-op
3228    #[gpui::test]
3229    async fn test_kill_active_task_on_completed_task_is_noop(cx: &mut TestAppContext) {
3230        cx.executor().allow_parking();
3231
3232        // Run a command that exits immediately
3233        let (terminal, completion_rx) = build_test_terminal(cx, "echo", &["done"]).await;
3234
3235        // Wait for the command to complete naturally
3236        let exit_status = completion_rx
3237            .recv()
3238            .await
3239            .expect("Should receive exit status");
3240        assert_eq!(exit_status, Some(ExitStatus::default()));
3241
3242        // Now try to kill - should be a no-op since task already completed
3243        terminal.update(cx, |term, _cx| {
3244            term.kill_active_task();
3245        });
3246
3247        // Content should still be there
3248        let content = terminal.update(cx, |term, _| term.get_content());
3249        assert!(
3250            content.contains("done"),
3251            "Output should still be present after no-op kill, got: {content}"
3252        );
3253    }
3254
3255    mod perf {
3256        use super::super::*;
3257        use gpui::{
3258            Entity, Point, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualContext,
3259            VisualTestContext, point,
3260        };
3261        use util::default;
3262        use util_macros::perf;
3263
3264        async fn init_scroll_perf_test(
3265            cx: &mut TestAppContext,
3266        ) -> (Entity<Terminal>, &mut VisualTestContext) {
3267            cx.update(|cx| {
3268                let settings_store = settings::SettingsStore::test(cx);
3269                cx.set_global(settings_store);
3270            });
3271
3272            cx.executor().allow_parking();
3273
3274            let window = cx.add_empty_window();
3275            let builder = window
3276                .update(|window, cx| {
3277                    let settings = TerminalSettings::get_global(cx);
3278                    let test_path_hyperlink_timeout_ms = 100;
3279                    TerminalBuilder::new(
3280                        None,
3281                        None,
3282                        task::Shell::System,
3283                        HashMap::default(),
3284                        CursorShape::default(),
3285                        AlternateScroll::On,
3286                        None,
3287                        settings.path_hyperlink_regexes.clone(),
3288                        test_path_hyperlink_timeout_ms,
3289                        false,
3290                        window.window_handle().window_id().as_u64(),
3291                        None,
3292                        cx,
3293                        vec![],
3294                        PathStyle::local(),
3295                    )
3296                })
3297                .await
3298                .unwrap();
3299            let terminal = window.new(|cx| builder.subscribe(cx));
3300
3301            terminal.update(window, |term, cx| {
3302                term.write_output("long line ".repeat(1000).as_bytes(), cx);
3303            });
3304
3305            (terminal, window)
3306        }
3307
3308        #[perf]
3309        #[gpui::test]
3310        async fn scroll_long_line_benchmark(cx: &mut TestAppContext) {
3311            let (terminal, window) = init_scroll_perf_test(cx).await;
3312            let wobble = point(FIND_HYPERLINK_THROTTLE_PX, px(0.0));
3313            let mut scroll_by = |lines: i32| {
3314                window.update_window_entity(&terminal, |terminal, window, cx| {
3315                    let bounds = terminal.last_content.terminal_bounds.bounds;
3316                    let center = bounds.origin + bounds.center();
3317                    let position = center + wobble * lines as f32;
3318
3319                    terminal.mouse_move(
3320                        &MouseMoveEvent {
3321                            position,
3322                            ..default()
3323                        },
3324                        cx,
3325                    );
3326
3327                    terminal.scroll_wheel(
3328                        &ScrollWheelEvent {
3329                            position,
3330                            delta: ScrollDelta::Lines(Point::new(0.0, lines as f32)),
3331                            ..default()
3332                        },
3333                        1.0,
3334                    );
3335
3336                    assert!(
3337                        terminal
3338                            .events
3339                            .iter()
3340                            .any(|event| matches!(event, InternalEvent::Scroll(_))),
3341                        "Should have Scroll event when scrolling within terminal bounds"
3342                    );
3343                    terminal.sync(window, cx);
3344                });
3345            };
3346
3347            for _ in 0..20000 {
3348                scroll_by(1);
3349                scroll_by(-1);
3350            }
3351        }
3352    }
3353}