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