terminal.rs

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