terminal.rs

   1pub mod mappings;
   2pub use alacritty_terminal;
   3
   4use alacritty_terminal::{
   5    ansi::{ClearMode, Handler},
   6    config::{Config, Program, PtyConfig, Scrolling},
   7    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
   8    event_loop::{EventLoop, Msg, Notifier},
   9    grid::{Dimensions, Scroll as AlacScroll},
  10    index::{Column, Direction as AlacDirection, Line, Point},
  11    selection::{Selection, SelectionRange, SelectionType},
  12    sync::FairMutex,
  13    term::{
  14        cell::Cell,
  15        color::Rgb,
  16        search::{Match, RegexIter, RegexSearch},
  17        RenderableCursor, TermMode,
  18    },
  19    tty::{self, setup_env},
  20    Term,
  21};
  22use anyhow::{bail, Result};
  23
  24use futures::{
  25    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
  26    FutureExt,
  27};
  28
  29use mappings::mouse::{
  30    alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
  31};
  32
  33use procinfo::LocalProcessInfo;
  34use schemars::JsonSchema;
  35use serde::{Deserialize, Serialize};
  36use util::truncate_and_trailoff;
  37
  38use std::{
  39    cmp::min,
  40    collections::{HashMap, VecDeque},
  41    fmt::Display,
  42    ops::{Deref, Index, RangeInclusive, Sub},
  43    os::unix::prelude::AsRawFd,
  44    path::PathBuf,
  45    sync::Arc,
  46    time::{Duration, Instant},
  47};
  48use thiserror::Error;
  49
  50use gpui::{
  51    fonts,
  52    geometry::vector::{vec2f, Vector2F},
  53    keymap_matcher::Keystroke,
  54    platform::{MouseButton, MouseMovedEvent, TouchPhase},
  55    scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
  56    AppContext, ClipboardItem, Entity, ModelContext, Task,
  57};
  58
  59use crate::mappings::{
  60    colors::{get_color_at_index, to_alac_rgb},
  61    keys::to_esc_str,
  62};
  63use lazy_static::lazy_static;
  64
  65///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
  66///Scroll multiplier that is set to 3 by default. This will be removed when I
  67///Implement scroll bars.
  68const SCROLL_MULTIPLIER: f32 = 4.;
  69const MAX_SEARCH_LINES: usize = 100;
  70const DEBUG_TERMINAL_WIDTH: f32 = 500.;
  71const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
  72const DEBUG_CELL_WIDTH: f32 = 5.;
  73const DEBUG_LINE_HEIGHT: f32 = 5.;
  74
  75// Regex Copied from alacritty's ui_config.rs
  76
  77lazy_static! {
  78    static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap();
  79}
  80
  81///Upward flowing events, for changing the title and such
  82#[derive(Clone, Copy, Debug)]
  83pub enum Event {
  84    TitleChanged,
  85    BreadcrumbsChanged,
  86    CloseTerminal,
  87    Bell,
  88    Wakeup,
  89    BlinkChanged,
  90    SelectionsChanged,
  91}
  92
  93#[derive(Clone)]
  94enum InternalEvent {
  95    ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
  96    Resize(TerminalSize),
  97    Clear,
  98    // FocusNextMatch,
  99    Scroll(AlacScroll),
 100    ScrollToPoint(Point),
 101    SetSelection(Option<(Selection, Point)>),
 102    UpdateSelection(Vector2F),
 103    // Adjusted mouse position, should open
 104    FindHyperlink(Vector2F, bool),
 105    Copy,
 106}
 107
 108///A translation struct for Alacritty to communicate with us from their event loop
 109#[derive(Clone)]
 110pub struct ZedListener(UnboundedSender<AlacTermEvent>);
 111
 112impl EventListener for ZedListener {
 113    fn send_event(&self, event: AlacTermEvent) {
 114        self.0.unbounded_send(event).ok();
 115    }
 116}
 117
 118pub fn init(cx: &mut AppContext) {
 119    settings::register::<TerminalSettings>(cx);
 120}
 121
 122#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 123#[serde(rename_all = "snake_case")]
 124pub enum TerminalDockPosition {
 125    Left,
 126    Bottom,
 127    Right,
 128}
 129
 130#[derive(Deserialize)]
 131pub struct TerminalSettings {
 132    pub shell: Shell,
 133    pub working_directory: WorkingDirectory,
 134    font_size: Option<f32>,
 135    pub font_family: Option<String>,
 136    pub line_height: TerminalLineHeight,
 137    pub font_features: Option<fonts::Features>,
 138    pub env: HashMap<String, String>,
 139    pub blinking: TerminalBlink,
 140    pub alternate_scroll: AlternateScroll,
 141    pub option_as_meta: bool,
 142    pub copy_on_select: bool,
 143    pub dock: TerminalDockPosition,
 144    pub default_width: f32,
 145    pub default_height: f32,
 146}
 147
 148#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 149pub struct TerminalSettingsContent {
 150    pub shell: Option<Shell>,
 151    pub working_directory: Option<WorkingDirectory>,
 152    pub font_size: Option<f32>,
 153    pub font_family: Option<String>,
 154    pub line_height: Option<TerminalLineHeight>,
 155    pub font_features: Option<fonts::Features>,
 156    pub env: Option<HashMap<String, String>>,
 157    pub blinking: Option<TerminalBlink>,
 158    pub alternate_scroll: Option<AlternateScroll>,
 159    pub option_as_meta: Option<bool>,
 160    pub copy_on_select: Option<bool>,
 161    pub dock: Option<TerminalDockPosition>,
 162    pub default_width: Option<f32>,
 163    pub default_height: Option<f32>,
 164}
 165
 166impl TerminalSettings {
 167    pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
 168        self.font_size
 169            .map(|size| theme::adjusted_font_size(size, cx))
 170    }
 171}
 172
 173impl settings::Setting for TerminalSettings {
 174    const KEY: Option<&'static str> = Some("terminal");
 175
 176    type FileContent = TerminalSettingsContent;
 177
 178    fn load(
 179        default_value: &Self::FileContent,
 180        user_values: &[&Self::FileContent],
 181        _: &AppContext,
 182    ) -> Result<Self> {
 183        Self::load_via_json_merge(default_value, user_values)
 184    }
 185}
 186
 187#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
 188#[serde(rename_all = "snake_case")]
 189pub enum TerminalLineHeight {
 190    #[default]
 191    Comfortable,
 192    Standard,
 193    Custom(f32),
 194}
 195
 196impl TerminalLineHeight {
 197    pub fn value(&self) -> f32 {
 198        match self {
 199            TerminalLineHeight::Comfortable => 1.618,
 200            TerminalLineHeight::Standard => 1.3,
 201            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
 202        }
 203    }
 204}
 205
 206#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 207#[serde(rename_all = "snake_case")]
 208pub enum TerminalBlink {
 209    Off,
 210    TerminalControlled,
 211    On,
 212}
 213
 214#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 215#[serde(rename_all = "snake_case")]
 216pub enum Shell {
 217    System,
 218    Program(String),
 219    WithArguments { program: String, args: Vec<String> },
 220}
 221
 222#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 223#[serde(rename_all = "snake_case")]
 224pub enum AlternateScroll {
 225    On,
 226    Off,
 227}
 228
 229#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 230#[serde(rename_all = "snake_case")]
 231pub enum WorkingDirectory {
 232    CurrentProjectDirectory,
 233    FirstProjectDirectory,
 234    AlwaysHome,
 235    Always { directory: String },
 236}
 237
 238#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
 239pub struct TerminalSize {
 240    pub cell_width: f32,
 241    pub line_height: f32,
 242    pub height: f32,
 243    pub width: f32,
 244}
 245
 246impl TerminalSize {
 247    pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
 248        TerminalSize {
 249            cell_width,
 250            line_height,
 251            width: size.x(),
 252            height: size.y(),
 253        }
 254    }
 255
 256    pub fn num_lines(&self) -> usize {
 257        (self.height / self.line_height).floor() as usize
 258    }
 259
 260    pub fn num_columns(&self) -> usize {
 261        (self.width / self.cell_width).floor() as usize
 262    }
 263
 264    pub fn height(&self) -> f32 {
 265        self.height
 266    }
 267
 268    pub fn width(&self) -> f32 {
 269        self.width
 270    }
 271
 272    pub fn cell_width(&self) -> f32 {
 273        self.cell_width
 274    }
 275
 276    pub fn line_height(&self) -> f32 {
 277        self.line_height
 278    }
 279}
 280impl Default for TerminalSize {
 281    fn default() -> Self {
 282        TerminalSize::new(
 283            DEBUG_LINE_HEIGHT,
 284            DEBUG_CELL_WIDTH,
 285            vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
 286        )
 287    }
 288}
 289
 290impl From<TerminalSize> for WindowSize {
 291    fn from(val: TerminalSize) -> Self {
 292        WindowSize {
 293            num_lines: val.num_lines() as u16,
 294            num_cols: val.num_columns() as u16,
 295            cell_width: val.cell_width() as u16,
 296            cell_height: val.line_height() as u16,
 297        }
 298    }
 299}
 300
 301impl Dimensions for TerminalSize {
 302    /// Note: this is supposed to be for the back buffer's length,
 303    /// but we exclusively use it to resize the terminal, which does not
 304    /// use this method. We still have to implement it for the trait though,
 305    /// hence, this comment.
 306    fn total_lines(&self) -> usize {
 307        self.screen_lines()
 308    }
 309
 310    fn screen_lines(&self) -> usize {
 311        self.num_lines()
 312    }
 313
 314    fn columns(&self) -> usize {
 315        self.num_columns()
 316    }
 317}
 318
 319#[derive(Error, Debug)]
 320pub struct TerminalError {
 321    pub directory: Option<PathBuf>,
 322    pub shell: Shell,
 323    pub source: std::io::Error,
 324}
 325
 326impl TerminalError {
 327    pub fn fmt_directory(&self) -> String {
 328        self.directory
 329            .clone()
 330            .map(|path| {
 331                match path
 332                    .into_os_string()
 333                    .into_string()
 334                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
 335                {
 336                    Ok(s) => s,
 337                    Err(s) => s,
 338                }
 339            })
 340            .unwrap_or_else(|| {
 341                let default_dir =
 342                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
 343                match default_dir {
 344                    Some(dir) => format!("<none specified, using home directory> {}", dir),
 345                    None => "<none specified, could not find home directory>".to_string(),
 346                }
 347            })
 348    }
 349
 350    pub fn shell_to_string(&self) -> String {
 351        match &self.shell {
 352            Shell::System => "<system shell>".to_string(),
 353            Shell::Program(p) => p.to_string(),
 354            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 355        }
 356    }
 357
 358    pub fn fmt_shell(&self) -> String {
 359        match &self.shell {
 360            Shell::System => "<system defined shell>".to_string(),
 361            Shell::Program(s) => s.to_string(),
 362            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 363        }
 364    }
 365}
 366
 367impl Display for TerminalError {
 368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 369        let dir_string: String = self.fmt_directory();
 370        let shell = self.fmt_shell();
 371
 372        write!(
 373            f,
 374            "Working directory: {} Shell command: `{}`, IOError: {}",
 375            dir_string, shell, self.source
 376        )
 377    }
 378}
 379
 380pub struct TerminalBuilder {
 381    terminal: Terminal,
 382    events_rx: UnboundedReceiver<AlacTermEvent>,
 383}
 384
 385impl TerminalBuilder {
 386    pub fn new(
 387        working_directory: Option<PathBuf>,
 388        shell: Shell,
 389        mut env: HashMap<String, String>,
 390        blink_settings: Option<TerminalBlink>,
 391        alternate_scroll: AlternateScroll,
 392        window_id: usize,
 393    ) -> Result<TerminalBuilder> {
 394        let pty_config = {
 395            let alac_shell = match shell.clone() {
 396                Shell::System => None,
 397                Shell::Program(program) => Some(Program::Just(program)),
 398                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
 399            };
 400
 401            PtyConfig {
 402                shell: alac_shell,
 403                working_directory: working_directory.clone(),
 404                hold: false,
 405            }
 406        };
 407
 408        //TODO: Properly set the current locale,
 409        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
 410        env.insert("ZED_TERM".to_string(), true.to_string());
 411
 412        let alac_scrolling = Scrolling::default();
 413        // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
 414
 415        let config = Config {
 416            pty_config: pty_config.clone(),
 417            env,
 418            scrolling: alac_scrolling,
 419            ..Default::default()
 420        };
 421
 422        setup_env(&config);
 423
 424        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
 425        //TODO: Remove with a bounded sender which can be dispatched on &self
 426        let (events_tx, events_rx) = unbounded();
 427        //Set up the terminal...
 428        let mut term = Term::new(
 429            &config,
 430            &TerminalSize::default(),
 431            ZedListener(events_tx.clone()),
 432        );
 433
 434        //Start off blinking if we need to
 435        if let Some(TerminalBlink::On) = blink_settings {
 436            term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
 437        }
 438
 439        //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
 440        if let AlternateScroll::Off = alternate_scroll {
 441            term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
 442        }
 443
 444        let term = Arc::new(FairMutex::new(term));
 445
 446        //Setup the pty...
 447        let pty = match tty::new(
 448            &pty_config,
 449            TerminalSize::default().into(),
 450            window_id as u64,
 451        ) {
 452            Ok(pty) => pty,
 453            Err(error) => {
 454                bail!(TerminalError {
 455                    directory: working_directory,
 456                    shell,
 457                    source: error,
 458                });
 459            }
 460        };
 461
 462        let fd = pty.file().as_raw_fd();
 463        let shell_pid = pty.child().id();
 464
 465        //And connect them together
 466        let event_loop = EventLoop::new(
 467            term.clone(),
 468            ZedListener(events_tx.clone()),
 469            pty,
 470            pty_config.hold,
 471            false,
 472        );
 473
 474        //Kick things off
 475        let pty_tx = event_loop.channel();
 476        let _io_thread = event_loop.spawn();
 477
 478        let terminal = Terminal {
 479            pty_tx: Notifier(pty_tx),
 480            term,
 481            events: VecDeque::with_capacity(10), //Should never get this high.
 482            last_content: Default::default(),
 483            last_mouse: None,
 484            matches: Vec::new(),
 485            last_synced: Instant::now(),
 486            sync_task: None,
 487            selection_head: None,
 488            shell_fd: fd as u32,
 489            shell_pid,
 490            foreground_process_info: None,
 491            breadcrumb_text: String::new(),
 492            scroll_px: 0.,
 493            last_mouse_position: None,
 494            next_link_id: 0,
 495            selection_phase: SelectionPhase::Ended,
 496        };
 497
 498        Ok(TerminalBuilder {
 499            terminal,
 500            events_rx,
 501        })
 502    }
 503
 504    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
 505        //Event loop
 506        cx.spawn_weak(|this, mut cx| async move {
 507            use futures::StreamExt;
 508
 509            while let Some(event) = self.events_rx.next().await {
 510                this.upgrade(&cx)?.update(&mut cx, |this, cx| {
 511                    //Process the first event immediately for lowered latency
 512                    this.process_event(&event, cx);
 513                });
 514
 515                'outer: loop {
 516                    let mut events = vec![];
 517                    let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
 518                    let mut wakeup = false;
 519                    loop {
 520                        futures::select_biased! {
 521                            _ = timer => break,
 522                            event = self.events_rx.next() => {
 523                                if let Some(event) = event {
 524                                    if matches!(event, AlacTermEvent::Wakeup) {
 525                                        wakeup = true;
 526                                    } else {
 527                                        events.push(event);
 528                                    }
 529
 530                                    if events.len() > 100 {
 531                                        break;
 532                                    }
 533                                } else {
 534                                    break;
 535                                }
 536                            },
 537                        }
 538                    }
 539
 540                    if events.is_empty() && wakeup == false {
 541                        smol::future::yield_now().await;
 542                        break 'outer;
 543                    } else {
 544                        this.upgrade(&cx)?.update(&mut cx, |this, cx| {
 545                            if wakeup {
 546                                this.process_event(&AlacTermEvent::Wakeup, cx);
 547                            }
 548
 549                            for event in events {
 550                                this.process_event(&event, cx);
 551                            }
 552                        });
 553                        smol::future::yield_now().await;
 554                    }
 555                }
 556            }
 557
 558            Some(())
 559        })
 560        .detach();
 561
 562        self.terminal
 563    }
 564}
 565
 566#[derive(Debug, Clone, Deserialize, Serialize)]
 567pub struct IndexedCell {
 568    pub point: Point,
 569    pub cell: Cell,
 570}
 571
 572impl Deref for IndexedCell {
 573    type Target = Cell;
 574
 575    #[inline]
 576    fn deref(&self) -> &Cell {
 577        &self.cell
 578    }
 579}
 580
 581// TODO: Un-pub
 582#[derive(Clone)]
 583pub struct TerminalContent {
 584    pub cells: Vec<IndexedCell>,
 585    pub mode: TermMode,
 586    pub display_offset: usize,
 587    pub selection_text: Option<String>,
 588    pub selection: Option<SelectionRange>,
 589    pub cursor: RenderableCursor,
 590    pub cursor_char: char,
 591    pub size: TerminalSize,
 592    pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
 593}
 594
 595impl Default for TerminalContent {
 596    fn default() -> Self {
 597        TerminalContent {
 598            cells: Default::default(),
 599            mode: Default::default(),
 600            display_offset: Default::default(),
 601            selection_text: Default::default(),
 602            selection: Default::default(),
 603            cursor: RenderableCursor {
 604                shape: alacritty_terminal::ansi::CursorShape::Block,
 605                point: Point::new(Line(0), Column(0)),
 606            },
 607            cursor_char: Default::default(),
 608            size: Default::default(),
 609            last_hovered_hyperlink: None,
 610        }
 611    }
 612}
 613
 614#[derive(PartialEq, Eq)]
 615pub enum SelectionPhase {
 616    Selecting,
 617    Ended,
 618}
 619
 620pub struct Terminal {
 621    pty_tx: Notifier,
 622    term: Arc<FairMutex<Term<ZedListener>>>,
 623    events: VecDeque<InternalEvent>,
 624    /// This is only used for mouse mode cell change detection
 625    last_mouse: Option<(Point, AlacDirection)>,
 626    /// This is only used for terminal hyperlink checking
 627    last_mouse_position: Option<Vector2F>,
 628    pub matches: Vec<RangeInclusive<Point>>,
 629    pub last_content: TerminalContent,
 630    last_synced: Instant,
 631    sync_task: Option<Task<()>>,
 632    pub selection_head: Option<Point>,
 633    pub breadcrumb_text: String,
 634    shell_pid: u32,
 635    shell_fd: u32,
 636    pub foreground_process_info: Option<LocalProcessInfo>,
 637    scroll_px: f32,
 638    next_link_id: usize,
 639    selection_phase: SelectionPhase,
 640}
 641
 642impl Terminal {
 643    fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
 644        match event {
 645            AlacTermEvent::Title(title) => {
 646                self.breadcrumb_text = title.to_string();
 647                cx.emit(Event::BreadcrumbsChanged);
 648            }
 649            AlacTermEvent::ResetTitle => {
 650                self.breadcrumb_text = String::new();
 651                cx.emit(Event::BreadcrumbsChanged);
 652            }
 653            AlacTermEvent::ClipboardStore(_, data) => {
 654                cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
 655            }
 656            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
 657                &cx.read_from_clipboard()
 658                    .map(|ci| ci.text().to_string())
 659                    .unwrap_or_else(|| "".to_string()),
 660            )),
 661            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
 662            AlacTermEvent::TextAreaSizeRequest(format) => {
 663                self.write_to_pty(format(self.last_content.size.into()))
 664            }
 665            AlacTermEvent::CursorBlinkingChange => {
 666                cx.emit(Event::BlinkChanged);
 667            }
 668            AlacTermEvent::Bell => {
 669                cx.emit(Event::Bell);
 670            }
 671            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
 672            AlacTermEvent::MouseCursorDirty => {
 673                //NOOP, Handled in render
 674            }
 675            AlacTermEvent::Wakeup => {
 676                cx.emit(Event::Wakeup);
 677
 678                if self.update_process_info() {
 679                    cx.emit(Event::TitleChanged);
 680                }
 681            }
 682            AlacTermEvent::ColorRequest(idx, fun_ptr) => {
 683                self.events
 684                    .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
 685            }
 686        }
 687    }
 688
 689    /// Update the cached process info, returns whether the Zed-relevant info has changed
 690    fn update_process_info(&mut self) -> bool {
 691        let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
 692        if pid < 0 {
 693            pid = self.shell_pid as i32;
 694        }
 695
 696        if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
 697            let res = self
 698                .foreground_process_info
 699                .as_ref()
 700                .map(|old_info| {
 701                    process_info.cwd != old_info.cwd || process_info.name != old_info.name
 702                })
 703                .unwrap_or(true);
 704
 705            self.foreground_process_info = Some(process_info.clone());
 706
 707            res
 708        } else {
 709            false
 710        }
 711    }
 712
 713    ///Takes events from Alacritty and translates them to behavior on this view
 714    fn process_terminal_event(
 715        &mut self,
 716        event: &InternalEvent,
 717        term: &mut Term<ZedListener>,
 718        cx: &mut ModelContext<Self>,
 719    ) {
 720        match event {
 721            InternalEvent::ColorRequest(index, format) => {
 722                let color = term.colors()[*index].unwrap_or_else(|| {
 723                    let term_style = &theme::current(cx).terminal;
 724                    to_alac_rgb(get_color_at_index(index, &term_style))
 725                });
 726                self.write_to_pty(format(color))
 727            }
 728            InternalEvent::Resize(mut new_size) => {
 729                new_size.height = f32::max(new_size.line_height, new_size.height);
 730                new_size.width = f32::max(new_size.cell_width, new_size.width);
 731
 732                self.last_content.size = new_size.clone();
 733
 734                self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
 735
 736                term.resize(new_size);
 737            }
 738            InternalEvent::Clear => {
 739                // Clear back buffer
 740                term.clear_screen(ClearMode::Saved);
 741
 742                let cursor = term.grid().cursor.point;
 743
 744                // Clear the lines above
 745                term.grid_mut().reset_region(..cursor.line);
 746
 747                // Copy the current line up
 748                let line = term.grid()[cursor.line][..Column(term.grid().columns())]
 749                    .iter()
 750                    .cloned()
 751                    .enumerate()
 752                    .collect::<Vec<(usize, Cell)>>();
 753
 754                for (i, cell) in line {
 755                    term.grid_mut()[Line(0)][Column(i)] = cell;
 756                }
 757
 758                // Reset the cursor
 759                term.grid_mut().cursor.point =
 760                    Point::new(Line(0), term.grid_mut().cursor.point.column);
 761                let new_cursor = term.grid().cursor.point;
 762
 763                // Clear the lines below the new cursor
 764                if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
 765                    term.grid_mut().reset_region((new_cursor.line + 1)..);
 766                }
 767
 768                cx.emit(Event::Wakeup);
 769            }
 770            InternalEvent::Scroll(scroll) => {
 771                term.scroll_display(*scroll);
 772                self.refresh_hyperlink();
 773            }
 774            InternalEvent::SetSelection(selection) => {
 775                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
 776
 777                if let Some((_, head)) = selection {
 778                    self.selection_head = Some(*head);
 779                }
 780                cx.emit(Event::SelectionsChanged)
 781            }
 782            InternalEvent::UpdateSelection(position) => {
 783                if let Some(mut selection) = term.selection.take() {
 784                    let point = grid_point(
 785                        *position,
 786                        self.last_content.size,
 787                        term.grid().display_offset(),
 788                    );
 789
 790                    let side = mouse_side(*position, self.last_content.size);
 791
 792                    selection.update(point, side);
 793                    term.selection = Some(selection);
 794
 795                    self.selection_head = Some(point);
 796                    cx.emit(Event::SelectionsChanged)
 797                }
 798            }
 799
 800            InternalEvent::Copy => {
 801                if let Some(txt) = term.selection_to_string() {
 802                    cx.write_to_clipboard(ClipboardItem::new(txt))
 803                }
 804            }
 805            InternalEvent::ScrollToPoint(point) => {
 806                term.scroll_to_point(*point);
 807                self.refresh_hyperlink();
 808            }
 809            InternalEvent::FindHyperlink(position, open) => {
 810                let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
 811
 812                let point = grid_point(
 813                    *position,
 814                    self.last_content.size,
 815                    term.grid().display_offset(),
 816                )
 817                .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
 818
 819                let link = term.grid().index(point).hyperlink();
 820                let found_url = if link.is_some() {
 821                    let mut min_index = point;
 822                    loop {
 823                        let new_min_index =
 824                            min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1);
 825                        if new_min_index == min_index {
 826                            break;
 827                        } else if term.grid().index(new_min_index).hyperlink() != link {
 828                            break;
 829                        } else {
 830                            min_index = new_min_index
 831                        }
 832                    }
 833
 834                    let mut max_index = point;
 835                    loop {
 836                        let new_max_index =
 837                            max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1);
 838                        if new_max_index == max_index {
 839                            break;
 840                        } else if term.grid().index(new_max_index).hyperlink() != link {
 841                            break;
 842                        } else {
 843                            max_index = new_max_index
 844                        }
 845                    }
 846
 847                    let url = link.unwrap().uri().to_owned();
 848                    let url_match = min_index..=max_index;
 849
 850                    Some((url, url_match))
 851                } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
 852                    let url = term.bounds_to_string(*url_match.start(), *url_match.end());
 853
 854                    Some((url, url_match))
 855                } else {
 856                    None
 857                };
 858
 859                if let Some((url, url_match)) = found_url {
 860                    if *open {
 861                        cx.platform().open_url(url.as_str());
 862                    } else {
 863                        self.update_hyperlink(prev_hyperlink, url, url_match);
 864                    }
 865                }
 866            }
 867        }
 868    }
 869
 870    fn update_hyperlink(
 871        &mut self,
 872        prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
 873        url: String,
 874        url_match: RangeInclusive<Point>,
 875    ) {
 876        if let Some(prev_hyperlink) = prev_hyperlink {
 877            if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match {
 878                self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2));
 879            } else {
 880                self.last_content.last_hovered_hyperlink =
 881                    Some((url, url_match, self.next_link_id()));
 882            }
 883        } else {
 884            self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
 885        }
 886    }
 887
 888    fn next_link_id(&mut self) -> usize {
 889        let res = self.next_link_id;
 890        self.next_link_id = self.next_link_id.wrapping_add(1);
 891        res
 892    }
 893
 894    pub fn last_content(&self) -> &TerminalContent {
 895        &self.last_content
 896    }
 897
 898    //To test:
 899    //- Activate match on terminal (scrolling and selection)
 900    //- Editor search snapping behavior
 901
 902    pub fn activate_match(&mut self, index: usize) {
 903        if let Some(search_match) = self.matches.get(index).cloned() {
 904            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
 905
 906            self.events
 907                .push_back(InternalEvent::ScrollToPoint(*search_match.start()));
 908        }
 909    }
 910
 911    pub fn select_matches(&mut self, matches: Vec<RangeInclusive<Point>>) {
 912        let matches_to_select = self
 913            .matches
 914            .iter()
 915            .filter(|self_match| matches.contains(self_match))
 916            .cloned()
 917            .collect::<Vec<_>>();
 918        for match_to_select in matches_to_select {
 919            self.set_selection(Some((
 920                make_selection(&match_to_select),
 921                *match_to_select.end(),
 922            )));
 923        }
 924    }
 925
 926    fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
 927        self.events
 928            .push_back(InternalEvent::SetSelection(selection));
 929    }
 930
 931    pub fn copy(&mut self) {
 932        self.events.push_back(InternalEvent::Copy);
 933    }
 934
 935    pub fn clear(&mut self) {
 936        self.events.push_back(InternalEvent::Clear)
 937    }
 938
 939    ///Resize the terminal and the PTY.
 940    pub fn set_size(&mut self, new_size: TerminalSize) {
 941        self.events.push_back(InternalEvent::Resize(new_size))
 942    }
 943
 944    ///Write the Input payload to the tty.
 945    fn write_to_pty(&self, input: String) {
 946        self.pty_tx.notify(input.into_bytes());
 947    }
 948
 949    pub fn input(&mut self, input: String) {
 950        self.events
 951            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
 952        self.events.push_back(InternalEvent::SetSelection(None));
 953
 954        self.write_to_pty(input);
 955    }
 956
 957    pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
 958        let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
 959        if let Some(esc) = esc {
 960            self.input(esc);
 961            true
 962        } else {
 963            false
 964        }
 965    }
 966
 967    ///Paste text into the terminal
 968    pub fn paste(&mut self, text: &str) {
 969        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
 970            format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
 971        } else {
 972            text.replace("\r\n", "\r").replace('\n', "\r")
 973        };
 974
 975        self.input(paste_text);
 976    }
 977
 978    pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
 979        let term = self.term.clone();
 980
 981        let mut terminal = if let Some(term) = term.try_lock_unfair() {
 982            term
 983        } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
 984            term.lock_unfair() //It's been too long, force block
 985        } else if let None = self.sync_task {
 986            //Skip this frame
 987            let delay = cx.background().timer(Duration::from_millis(16));
 988            self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
 989                delay.await;
 990                cx.update(|cx| {
 991                    if let Some(handle) = weak_handle.upgrade(cx) {
 992                        handle.update(cx, |terminal, cx| {
 993                            terminal.sync_task.take();
 994                            cx.notify();
 995                        });
 996                    }
 997                });
 998            }));
 999            return;
1000        } else {
1001            //No lock and delayed rendering already scheduled, nothing to do
1002            return;
1003        };
1004
1005        //Note that the ordering of events matters for event processing
1006        while let Some(e) = self.events.pop_front() {
1007            self.process_terminal_event(&e, &mut terminal, cx)
1008        }
1009
1010        self.last_content = Self::make_content(&terminal, &self.last_content);
1011        self.last_synced = Instant::now();
1012    }
1013
1014    fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
1015        let content = term.renderable_content();
1016        TerminalContent {
1017            cells: content
1018                .display_iter
1019                //TODO: Add this once there's a way to retain empty lines
1020                // .filter(|ic| {
1021                //     !ic.flags.contains(Flags::HIDDEN)
1022                //         && !(ic.bg == Named(NamedColor::Background)
1023                //             && ic.c == ' '
1024                //             && !ic.flags.contains(Flags::INVERSE))
1025                // })
1026                .map(|ic| IndexedCell {
1027                    point: ic.point,
1028                    cell: ic.cell.clone(),
1029                })
1030                .collect::<Vec<IndexedCell>>(),
1031            mode: content.mode,
1032            display_offset: content.display_offset,
1033            selection_text: term.selection_to_string(),
1034            selection: content.selection,
1035            cursor: content.cursor,
1036            cursor_char: term.grid()[content.cursor.point].c,
1037            size: last_content.size,
1038            last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
1039        }
1040    }
1041
1042    pub fn focus_in(&self) {
1043        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1044            self.write_to_pty("\x1b[I".to_string());
1045        }
1046    }
1047
1048    pub fn focus_out(&mut self) {
1049        self.last_mouse_position = None;
1050        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1051            self.write_to_pty("\x1b[O".to_string());
1052        }
1053    }
1054
1055    pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
1056        match self.last_mouse {
1057            Some((old_point, old_side)) => {
1058                if old_point == point && old_side == side {
1059                    false
1060                } else {
1061                    self.last_mouse = Some((point, side));
1062                    true
1063                }
1064            }
1065            None => {
1066                self.last_mouse = Some((point, side));
1067                true
1068            }
1069        }
1070    }
1071
1072    pub fn mouse_mode(&self, shift: bool) -> bool {
1073        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
1074    }
1075
1076    pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
1077        let position = e.position.sub(origin);
1078        self.last_mouse_position = Some(position);
1079        if self.mouse_mode(e.shift) {
1080            let point = grid_point(
1081                position,
1082                self.last_content.size,
1083                self.last_content.display_offset,
1084            );
1085            let side = mouse_side(position, self.last_content.size);
1086
1087            if self.mouse_changed(point, side) {
1088                if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
1089                    self.pty_tx.notify(bytes);
1090                }
1091            }
1092        } else {
1093            self.hyperlink_from_position(Some(position));
1094        }
1095    }
1096
1097    fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
1098        if self.selection_phase == SelectionPhase::Selecting {
1099            self.last_content.last_hovered_hyperlink = None;
1100        } else if let Some(position) = position {
1101            self.events
1102                .push_back(InternalEvent::FindHyperlink(position, false));
1103        }
1104    }
1105
1106    pub fn mouse_drag(&mut self, e: MouseDrag, origin: Vector2F) {
1107        let position = e.position.sub(origin);
1108        self.last_mouse_position = Some(position);
1109
1110        if !self.mouse_mode(e.shift) {
1111            self.selection_phase = SelectionPhase::Selecting;
1112            // Alacritty has the same ordering, of first updating the selection
1113            // then scrolling 15ms later
1114            self.events
1115                .push_back(InternalEvent::UpdateSelection(position));
1116
1117            // Doesn't make sense to scroll the alt screen
1118            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
1119                let scroll_delta = match self.drag_line_delta(e) {
1120                    Some(value) => value,
1121                    None => return,
1122                };
1123
1124                let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
1125
1126                self.events
1127                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
1128            }
1129        }
1130    }
1131
1132    fn drag_line_delta(&mut self, e: MouseDrag) -> Option<f32> {
1133        //TODO: Why do these need to be doubled? Probably the same problem that the IME has
1134        let top = e.region.origin_y() + (self.last_content.size.line_height * 2.);
1135        let bottom = e.region.lower_left().y() - (self.last_content.size.line_height * 2.);
1136        let scroll_delta = if e.position.y() < top {
1137            (top - e.position.y()).powf(1.1)
1138        } else if e.position.y() > bottom {
1139            -((e.position.y() - bottom).powf(1.1))
1140        } else {
1141            return None; //Nothing to do
1142        };
1143        Some(scroll_delta)
1144    }
1145
1146    pub fn mouse_down(&mut self, e: &MouseDown, origin: Vector2F) {
1147        let position = e.position.sub(origin);
1148        let point = grid_point(
1149            position,
1150            self.last_content.size,
1151            self.last_content.display_offset,
1152        );
1153
1154        if self.mouse_mode(e.shift) {
1155            if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
1156                self.pty_tx.notify(bytes);
1157            }
1158        } else if e.button == MouseButton::Left {
1159            let position = e.position.sub(origin);
1160            let point = grid_point(
1161                position,
1162                self.last_content.size,
1163                self.last_content.display_offset,
1164            );
1165
1166            // Use .opposite so that selection is inclusive of the cell clicked.
1167            let side = mouse_side(position, self.last_content.size);
1168
1169            let selection_type = match e.click_count {
1170                0 => return, //This is a release
1171                1 => Some(SelectionType::Simple),
1172                2 => Some(SelectionType::Semantic),
1173                3 => Some(SelectionType::Lines),
1174                _ => None,
1175            };
1176
1177            let selection =
1178                selection_type.map(|selection_type| Selection::new(selection_type, point, side));
1179
1180            if let Some(sel) = selection {
1181                self.events
1182                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
1183            }
1184        }
1185    }
1186
1187    pub fn mouse_up(&mut self, e: &MouseUp, origin: Vector2F, cx: &mut ModelContext<Self>) {
1188        let setting = settings::get::<TerminalSettings>(cx);
1189
1190        let position = e.position.sub(origin);
1191        if self.mouse_mode(e.shift) {
1192            let point = grid_point(
1193                position,
1194                self.last_content.size,
1195                self.last_content.display_offset,
1196            );
1197
1198            if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
1199                self.pty_tx.notify(bytes);
1200            }
1201        } else {
1202            if e.button == MouseButton::Left && setting.copy_on_select {
1203                self.copy();
1204            }
1205
1206            //Hyperlinks
1207            if self.selection_phase == SelectionPhase::Ended {
1208                let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
1209                if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
1210                    cx.platform().open_url(link.uri());
1211                } else {
1212                    self.events
1213                        .push_back(InternalEvent::FindHyperlink(position, true));
1214                }
1215            }
1216        }
1217
1218        self.selection_phase = SelectionPhase::Ended;
1219        self.last_mouse = None;
1220    }
1221
1222    ///Scroll the terminal
1223    pub fn scroll_wheel(&mut self, e: MouseScrollWheel, origin: Vector2F) {
1224        let mouse_mode = self.mouse_mode(e.shift);
1225
1226        if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
1227            if mouse_mode {
1228                let point = grid_point(
1229                    e.position.sub(origin),
1230                    self.last_content.size,
1231                    self.last_content.display_offset,
1232                );
1233
1234                if let Some(scrolls) =
1235                    scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
1236                {
1237                    for scroll in scrolls {
1238                        self.pty_tx.notify(scroll);
1239                    }
1240                };
1241            } else if self
1242                .last_content
1243                .mode
1244                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
1245                && !e.shift
1246            {
1247                self.pty_tx.notify(alt_scroll(scroll_lines))
1248            } else {
1249                if scroll_lines != 0 {
1250                    let scroll = AlacScroll::Delta(scroll_lines);
1251
1252                    self.events.push_back(InternalEvent::Scroll(scroll));
1253                }
1254            }
1255        }
1256    }
1257
1258    pub fn refresh_hyperlink(&mut self) {
1259        self.hyperlink_from_position(self.last_mouse_position);
1260    }
1261
1262    fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
1263        let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
1264        let line_height = self.last_content.size.line_height;
1265        match e.phase {
1266            /* Reset scroll state on started */
1267            Some(TouchPhase::Started) => {
1268                self.scroll_px = 0.;
1269                None
1270            }
1271            /* Calculate the appropriate scroll lines */
1272            Some(gpui::platform::TouchPhase::Moved) => {
1273                let old_offset = (self.scroll_px / line_height) as i32;
1274
1275                self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
1276
1277                let new_offset = (self.scroll_px / line_height) as i32;
1278
1279                // Whenever we hit the edges, reset our stored scroll to 0
1280                // so we can respond to changes in direction quickly
1281                self.scroll_px %= self.last_content.size.height;
1282
1283                Some(new_offset - old_offset)
1284            }
1285            /* Fall back to delta / line_height */
1286            None => Some(
1287                ((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
1288            ),
1289            _ => None,
1290        }
1291    }
1292
1293    pub fn find_matches(
1294        &mut self,
1295        searcher: RegexSearch,
1296        cx: &mut ModelContext<Self>,
1297    ) -> Task<Vec<RangeInclusive<Point>>> {
1298        let term = self.term.clone();
1299        cx.background().spawn(async move {
1300            let term = term.lock();
1301
1302            all_search_matches(&term, &searcher).collect()
1303        })
1304    }
1305
1306    pub fn title(&self) -> String {
1307        self.foreground_process_info
1308            .as_ref()
1309            .map(|fpi| {
1310                format!(
1311                    "{}{}",
1312                    truncate_and_trailoff(
1313                        &fpi.cwd
1314                            .file_name()
1315                            .map(|name| name.to_string_lossy().to_string())
1316                            .unwrap_or_default(),
1317                        25
1318                    ),
1319                    truncate_and_trailoff(
1320                        &{
1321                            format!(
1322                                "{}{}",
1323                                fpi.name,
1324                                if fpi.argv.len() >= 1 {
1325                                    format!(" {}", (&fpi.argv[1..]).join(" "))
1326                                } else {
1327                                    "".to_string()
1328                                }
1329                            )
1330                        },
1331                        25
1332                    )
1333                )
1334            })
1335            .unwrap_or_else(|| "Terminal".to_string())
1336    }
1337}
1338
1339impl Drop for Terminal {
1340    fn drop(&mut self) {
1341        self.pty_tx.0.send(Msg::Shutdown).ok();
1342    }
1343}
1344
1345impl Entity for Terminal {
1346    type Event = Event;
1347}
1348
1349/// Based on alacritty/src/display/hint.rs > regex_match_at
1350/// Retrieve the match, if the specified point is inside the content matching the regex.
1351fn regex_match_at<T>(term: &Term<T>, point: Point, regex: &RegexSearch) -> Option<Match> {
1352    visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
1353}
1354
1355/// Copied from alacritty/src/display/hint.rs:
1356/// Iterate over all visible regex matches.
1357pub fn visible_regex_match_iter<'a, T>(
1358    term: &'a Term<T>,
1359    regex: &'a RegexSearch,
1360) -> impl Iterator<Item = Match> + 'a {
1361    let viewport_start = Line(-(term.grid().display_offset() as i32));
1362    let viewport_end = viewport_start + term.bottommost_line();
1363    let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
1364    let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
1365    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1366    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1367
1368    RegexIter::new(start, end, AlacDirection::Right, term, regex)
1369        .skip_while(move |rm| rm.end().line < viewport_start)
1370        .take_while(move |rm| rm.start().line <= viewport_end)
1371}
1372
1373fn make_selection(range: &RangeInclusive<Point>) -> Selection {
1374    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1375    selection.update(*range.end(), AlacDirection::Right);
1376    selection
1377}
1378
1379fn all_search_matches<'a, T>(
1380    term: &'a Term<T>,
1381    regex: &'a RegexSearch,
1382) -> impl Iterator<Item = Match> + 'a {
1383    let start = Point::new(term.grid().topmost_line(), Column(0));
1384    let end = Point::new(term.grid().bottommost_line(), term.grid().last_column());
1385    RegexIter::new(start, end, AlacDirection::Right, term, regex)
1386}
1387
1388fn content_index_for_mouse(pos: Vector2F, size: &TerminalSize) -> usize {
1389    let col = (pos.x() / size.cell_width()).round() as usize;
1390
1391    let clamped_col = min(col, size.columns() - 1);
1392
1393    let row = (pos.y() / size.line_height()).round() as usize;
1394
1395    let clamped_row = min(row, size.screen_lines() - 1);
1396
1397    clamped_row * size.columns() + clamped_col
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402    use alacritty_terminal::{
1403        index::{Column, Line, Point},
1404        term::cell::Cell,
1405    };
1406    use gpui::geometry::vector::vec2f;
1407    use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
1408
1409    use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
1410
1411    #[test]
1412    fn test_mouse_to_cell_test() {
1413        let mut rng = thread_rng();
1414        const ITERATIONS: usize = 10;
1415        const PRECISION: usize = 1000;
1416
1417        for _ in 0..ITERATIONS {
1418            let viewport_cells = rng.gen_range(15..20);
1419            let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
1420
1421            let size = crate::TerminalSize {
1422                cell_width: cell_size,
1423                line_height: cell_size,
1424                height: cell_size * (viewport_cells as f32),
1425                width: cell_size * (viewport_cells as f32),
1426            };
1427
1428            let cells = get_cells(size, &mut rng);
1429            let content = convert_cells_to_content(size, &cells);
1430
1431            for row in 0..(viewport_cells - 1) {
1432                let row = row as usize;
1433                for col in 0..(viewport_cells - 1) {
1434                    let col = col as usize;
1435
1436                    let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1437                    let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1438
1439                    let mouse_pos = vec2f(
1440                        col as f32 * cell_size + col_offset,
1441                        row as f32 * cell_size + row_offset,
1442                    );
1443
1444                    let content_index = content_index_for_mouse(mouse_pos, &content.size);
1445                    let mouse_cell = content.cells[content_index].c;
1446                    let real_cell = cells[row][col];
1447
1448                    assert_eq!(mouse_cell, real_cell);
1449                }
1450            }
1451        }
1452    }
1453
1454    #[test]
1455    fn test_mouse_to_cell_clamp() {
1456        let mut rng = thread_rng();
1457
1458        let size = crate::TerminalSize {
1459            cell_width: 10.,
1460            line_height: 10.,
1461            height: 100.,
1462            width: 100.,
1463        };
1464
1465        let cells = get_cells(size, &mut rng);
1466        let content = convert_cells_to_content(size, &cells);
1467
1468        assert_eq!(
1469            content.cells[content_index_for_mouse(vec2f(-10., -10.), &content.size)].c,
1470            cells[0][0]
1471        );
1472        assert_eq!(
1473            content.cells[content_index_for_mouse(vec2f(1000., 1000.), &content.size)].c,
1474            cells[9][9]
1475        );
1476    }
1477
1478    fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
1479        let mut cells = Vec::new();
1480
1481        for _ in 0..((size.height() / size.line_height()) as usize) {
1482            let mut row_vec = Vec::new();
1483            for _ in 0..((size.width() / size.cell_width()) as usize) {
1484                let cell_char = rng.sample(Alphanumeric) as char;
1485                row_vec.push(cell_char)
1486            }
1487            cells.push(row_vec)
1488        }
1489
1490        cells
1491    }
1492
1493    fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
1494        let mut ic = Vec::new();
1495
1496        for row in 0..cells.len() {
1497            for col in 0..cells[row].len() {
1498                let cell_char = cells[row][col];
1499                ic.push(IndexedCell {
1500                    point: Point::new(Line(row as i32), Column(col)),
1501                    cell: Cell {
1502                        c: cell_char,
1503                        ..Default::default()
1504                    },
1505                });
1506            }
1507        }
1508
1509        TerminalContent {
1510            cells: ic,
1511            size,
1512            ..Default::default()
1513        }
1514    }
1515}