terminal.rs

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