terminal.rs

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