terminal.rs

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