terminal.rs

   1pub mod mappings;
   2pub mod modal;
   3pub mod terminal_container_view;
   4pub mod terminal_element;
   5pub mod terminal_view;
   6
   7use alacritty_terminal::{
   8    ansi::{ClearMode, Handler},
   9    config::{Config, Program, PtyConfig, Scrolling},
  10    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
  11    event_loop::{EventLoop, Msg, Notifier},
  12    grid::{Dimensions, Scroll as AlacScroll},
  13    index::{Column, Direction as AlacDirection, Line, Point},
  14    selection::{Selection, SelectionRange, SelectionType},
  15    sync::FairMutex,
  16    term::{
  17        cell::Cell,
  18        color::Rgb,
  19        search::{Match, RegexIter, RegexSearch},
  20        RenderableCursor, TermMode,
  21    },
  22    tty::{self, setup_env},
  23    Term,
  24};
  25use anyhow::{bail, Result};
  26
  27use futures::{
  28    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
  29    FutureExt,
  30};
  31
  32use mappings::mouse::{
  33    alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
  34};
  35use modal::deploy_modal;
  36
  37use procinfo::LocalProcessInfo;
  38use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
  39
  40use std::{
  41    collections::{HashMap, VecDeque},
  42    fmt::Display,
  43    ops::{Deref, RangeInclusive, Sub},
  44    os::unix::prelude::AsRawFd,
  45    path::PathBuf,
  46    sync::Arc,
  47    time::{Duration, Instant},
  48};
  49use thiserror::Error;
  50
  51use gpui::{
  52    geometry::vector::{vec2f, Vector2F},
  53    keymap::Keystroke,
  54    scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
  55    ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
  56    ScrollWheelEvent, Task,
  57};
  58
  59use crate::mappings::{
  60    colors::{get_color_at_index, to_alac_rgb},
  61    keys::to_esc_str,
  62};
  63
  64///Initialize and register all of our action handlers
  65pub fn init(cx: &mut MutableAppContext) {
  66    cx.add_action(deploy_modal);
  67
  68    terminal_view::init(cx);
  69    terminal_container_view::init(cx);
  70}
  71
  72///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
  73///Scroll multiplier that is set to 3 by default. This will be removed when I
  74///Implement scroll bars.
  75const SCROLL_MULTIPLIER: f32 = 4.;
  76const MAX_SEARCH_LINES: usize = 100;
  77const DEBUG_TERMINAL_WIDTH: f32 = 500.;
  78const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
  79const DEBUG_CELL_WIDTH: f32 = 5.;
  80const DEBUG_LINE_HEIGHT: f32 = 5.;
  81
  82///Upward flowing events, for changing the title and such
  83#[derive(Clone, Copy, Debug)]
  84pub enum Event {
  85    TitleChanged,
  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    Copy,
 104}
 105
 106///A translation struct for Alacritty to communicate with us from their event loop
 107#[derive(Clone)]
 108pub struct ZedListener(UnboundedSender<AlacTermEvent>);
 109
 110impl EventListener for ZedListener {
 111    fn send_event(&self, event: AlacTermEvent) {
 112        self.0.unbounded_send(event).ok();
 113    }
 114}
 115
 116#[derive(Clone, Copy, Debug)]
 117pub struct TerminalSize {
 118    cell_width: f32,
 119    line_height: f32,
 120    height: f32,
 121    width: f32,
 122}
 123
 124impl TerminalSize {
 125    pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
 126        TerminalSize {
 127            cell_width,
 128            line_height,
 129            width: size.x(),
 130            height: size.y(),
 131        }
 132    }
 133
 134    pub fn num_lines(&self) -> usize {
 135        (self.height / self.line_height).floor() as usize
 136    }
 137
 138    pub fn num_columns(&self) -> usize {
 139        (self.width / self.cell_width).floor() as usize
 140    }
 141
 142    pub fn height(&self) -> f32 {
 143        self.height
 144    }
 145
 146    pub fn width(&self) -> f32 {
 147        self.width
 148    }
 149
 150    pub fn cell_width(&self) -> f32 {
 151        self.cell_width
 152    }
 153
 154    pub fn line_height(&self) -> f32 {
 155        self.line_height
 156    }
 157}
 158impl Default for TerminalSize {
 159    fn default() -> Self {
 160        TerminalSize::new(
 161            DEBUG_LINE_HEIGHT,
 162            DEBUG_CELL_WIDTH,
 163            vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
 164        )
 165    }
 166}
 167
 168impl From<TerminalSize> for WindowSize {
 169    fn from(val: TerminalSize) -> Self {
 170        WindowSize {
 171            num_lines: val.num_lines() as u16,
 172            num_cols: val.num_columns() as u16,
 173            cell_width: val.cell_width() as u16,
 174            cell_height: val.line_height() as u16,
 175        }
 176    }
 177}
 178
 179impl Dimensions for TerminalSize {
 180    /// Note: this is supposed to be for the back buffer's length,
 181    /// but we exclusively use it to resize the terminal, which does not
 182    /// use this method. We still have to implement it for the trait though,
 183    /// hence, this comment.
 184    fn total_lines(&self) -> usize {
 185        self.screen_lines()
 186    }
 187
 188    fn screen_lines(&self) -> usize {
 189        self.num_lines()
 190    }
 191
 192    fn columns(&self) -> usize {
 193        self.num_columns()
 194    }
 195}
 196
 197#[derive(Error, Debug)]
 198pub struct TerminalError {
 199    pub directory: Option<PathBuf>,
 200    pub shell: Option<Shell>,
 201    pub source: std::io::Error,
 202}
 203
 204impl TerminalError {
 205    pub fn fmt_directory(&self) -> String {
 206        self.directory
 207            .clone()
 208            .map(|path| {
 209                match path
 210                    .into_os_string()
 211                    .into_string()
 212                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
 213                {
 214                    Ok(s) => s,
 215                    Err(s) => s,
 216                }
 217            })
 218            .unwrap_or_else(|| {
 219                let default_dir =
 220                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
 221                match default_dir {
 222                    Some(dir) => format!("<none specified, using home directory> {}", dir),
 223                    None => "<none specified, could not find home directory>".to_string(),
 224                }
 225            })
 226    }
 227
 228    pub fn shell_to_string(&self) -> Option<String> {
 229        self.shell.as_ref().map(|shell| match shell {
 230            Shell::System => "<system shell>".to_string(),
 231            Shell::Program(p) => p.to_string(),
 232            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 233        })
 234    }
 235
 236    pub fn fmt_shell(&self) -> String {
 237        self.shell
 238            .clone()
 239            .map(|shell| match shell {
 240                Shell::System => "<system defined shell>".to_string(),
 241
 242                Shell::Program(s) => s,
 243                Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 244            })
 245            .unwrap_or_else(|| "<none specified, using system defined shell>".to_string())
 246    }
 247}
 248
 249impl Display for TerminalError {
 250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 251        let dir_string: String = self.fmt_directory();
 252        let shell = self.fmt_shell();
 253
 254        write!(
 255            f,
 256            "Working directory: {} Shell command: `{}`, IOError: {}",
 257            dir_string, shell, self.source
 258        )
 259    }
 260}
 261
 262pub struct TerminalBuilder {
 263    terminal: Terminal,
 264    events_rx: UnboundedReceiver<AlacTermEvent>,
 265}
 266
 267impl TerminalBuilder {
 268    pub fn new(
 269        working_directory: Option<PathBuf>,
 270        shell: Option<Shell>,
 271        env: Option<HashMap<String, String>>,
 272        initial_size: TerminalSize,
 273        blink_settings: Option<TerminalBlink>,
 274        alternate_scroll: &AlternateScroll,
 275    ) -> Result<TerminalBuilder> {
 276        let pty_config = {
 277            let alac_shell = shell.clone().and_then(|shell| match shell {
 278                Shell::System => None,
 279                Shell::Program(program) => Some(Program::Just(program)),
 280                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
 281            });
 282
 283            PtyConfig {
 284                shell: alac_shell,
 285                working_directory: working_directory.clone(),
 286                hold: false,
 287            }
 288        };
 289
 290        let mut env = env.unwrap_or_default();
 291
 292        //TODO: Properly set the current locale,
 293        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
 294
 295        let alac_scrolling = Scrolling::default();
 296        // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
 297
 298        let config = Config {
 299            pty_config: pty_config.clone(),
 300            env,
 301            scrolling: alac_scrolling,
 302            ..Default::default()
 303        };
 304
 305        setup_env(&config);
 306
 307        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
 308        //TODO: Remove with a bounded sender which can be dispatched on &self
 309        let (events_tx, events_rx) = unbounded();
 310        //Set up the terminal...
 311        let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
 312
 313        //Start off blinking if we need to
 314        if let Some(TerminalBlink::On) = blink_settings {
 315            term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
 316        }
 317
 318        //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
 319        if let AlternateScroll::Off = alternate_scroll {
 320            term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
 321        }
 322
 323        let term = Arc::new(FairMutex::new(term));
 324
 325        //Setup the pty...
 326        let pty = match tty::new(&pty_config, initial_size.into(), None) {
 327            Ok(pty) => pty,
 328            Err(error) => {
 329                bail!(TerminalError {
 330                    directory: working_directory,
 331                    shell,
 332                    source: error,
 333                });
 334            }
 335        };
 336
 337        let fd = pty.file().as_raw_fd();
 338        let shell_pid = pty.child().id();
 339
 340        //And connect them together
 341        let event_loop = EventLoop::new(
 342            term.clone(),
 343            ZedListener(events_tx.clone()),
 344            pty,
 345            pty_config.hold,
 346            false,
 347        );
 348
 349        //Kick things off
 350        let pty_tx = event_loop.channel();
 351        let _io_thread = event_loop.spawn();
 352
 353        let terminal = Terminal {
 354            pty_tx: Notifier(pty_tx),
 355            term,
 356            events: VecDeque::with_capacity(10), //Should never get this high.
 357            last_content: Default::default(),
 358            cur_size: initial_size,
 359            last_mouse: None,
 360            matches: Vec::new(),
 361            last_synced: Instant::now(),
 362            sync_task: None,
 363            selection_head: None,
 364            shell_fd: fd as u32,
 365            shell_pid,
 366            foreground_process_info: None,
 367            breadcrumb_text: String::new(),
 368            scroll_px: 0.,
 369        };
 370
 371        Ok(TerminalBuilder {
 372            terminal,
 373            events_rx,
 374        })
 375    }
 376
 377    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
 378        //Event loop
 379        cx.spawn_weak(|this, mut cx| async move {
 380            use futures::StreamExt;
 381
 382            while let Some(event) = self.events_rx.next().await {
 383                this.upgrade(&cx)?.update(&mut cx, |this, cx| {
 384                    //Process the first event immediately for lowered latency
 385                    this.process_event(&event, cx);
 386                });
 387
 388                'outer: loop {
 389                    let mut events = vec![];
 390                    let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
 391
 392                    loop {
 393                        futures::select_biased! {
 394                            _ = timer => break,
 395                            event = self.events_rx.next() => {
 396                                if let Some(event) = event {
 397                                    events.push(event);
 398                                    if events.len() > 100 {
 399                                        break;
 400                                    }
 401                                } else {
 402                                    break;
 403                                }
 404                            },
 405                        }
 406                    }
 407
 408                    if events.is_empty() {
 409                        smol::future::yield_now().await;
 410                        break 'outer;
 411                    } else {
 412                        this.upgrade(&cx)?.update(&mut cx, |this, cx| {
 413                            for event in events {
 414                                this.process_event(&event, cx);
 415                            }
 416                        });
 417                        smol::future::yield_now().await;
 418                    }
 419                }
 420            }
 421
 422            Some(())
 423        })
 424        .detach();
 425
 426        self.terminal
 427    }
 428}
 429
 430#[derive(Debug, Clone)]
 431struct IndexedCell {
 432    point: Point,
 433    cell: Cell,
 434}
 435
 436impl Deref for IndexedCell {
 437    type Target = Cell;
 438
 439    #[inline]
 440    fn deref(&self) -> &Cell {
 441        &self.cell
 442    }
 443}
 444
 445#[derive(Clone)]
 446pub struct TerminalContent {
 447    cells: Vec<IndexedCell>,
 448    mode: TermMode,
 449    display_offset: usize,
 450    selection_text: Option<String>,
 451    selection: Option<SelectionRange>,
 452    cursor: RenderableCursor,
 453    cursor_char: char,
 454}
 455
 456impl Default for TerminalContent {
 457    fn default() -> Self {
 458        TerminalContent {
 459            cells: Default::default(),
 460            mode: Default::default(),
 461            display_offset: Default::default(),
 462            selection_text: Default::default(),
 463            selection: Default::default(),
 464            cursor: RenderableCursor {
 465                shape: alacritty_terminal::ansi::CursorShape::Block,
 466                point: Point::new(Line(0), Column(0)),
 467            },
 468            cursor_char: Default::default(),
 469        }
 470    }
 471}
 472
 473pub struct Terminal {
 474    pty_tx: Notifier,
 475    term: Arc<FairMutex<Term<ZedListener>>>,
 476    events: VecDeque<InternalEvent>,
 477    last_mouse: Option<(Point, AlacDirection)>,
 478    pub matches: Vec<RangeInclusive<Point>>,
 479    cur_size: TerminalSize,
 480    last_content: TerminalContent,
 481    last_synced: Instant,
 482    sync_task: Option<Task<()>>,
 483    selection_head: Option<Point>,
 484    breadcrumb_text: String,
 485    shell_pid: u32,
 486    shell_fd: u32,
 487    foreground_process_info: Option<LocalProcessInfo>,
 488    scroll_px: f32,
 489}
 490
 491impl Terminal {
 492    fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
 493        match event {
 494            AlacTermEvent::Title(title) => {
 495                self.breadcrumb_text = title.to_string();
 496            }
 497            AlacTermEvent::ResetTitle => {
 498                self.breadcrumb_text = String::new();
 499            }
 500            AlacTermEvent::ClipboardStore(_, data) => {
 501                cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
 502            }
 503            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
 504                &cx.read_from_clipboard()
 505                    .map(|ci| ci.text().to_string())
 506                    .unwrap_or_else(|| "".to_string()),
 507            )),
 508            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
 509            AlacTermEvent::TextAreaSizeRequest(format) => {
 510                self.write_to_pty(format(self.cur_size.into()))
 511            }
 512            AlacTermEvent::CursorBlinkingChange => {
 513                cx.emit(Event::BlinkChanged);
 514            }
 515            AlacTermEvent::Bell => {
 516                cx.emit(Event::Bell);
 517            }
 518            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
 519            AlacTermEvent::MouseCursorDirty => {
 520                //NOOP, Handled in render
 521            }
 522            AlacTermEvent::Wakeup => {
 523                cx.emit(Event::Wakeup);
 524
 525                if self.update_process_info() {
 526                    cx.emit(Event::TitleChanged)
 527                }
 528            }
 529            AlacTermEvent::ColorRequest(idx, fun_ptr) => {
 530                self.events
 531                    .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
 532            }
 533        }
 534    }
 535
 536    /// Update the cached process info, returns whether the Zed-relevant info has changed
 537    fn update_process_info(&mut self) -> bool {
 538        let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
 539        if pid < 0 {
 540            pid = self.shell_pid as i32;
 541        }
 542
 543        if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
 544            let res = self
 545                .foreground_process_info
 546                .as_ref()
 547                .map(|old_info| {
 548                    process_info.cwd != old_info.cwd || process_info.name != old_info.name
 549                })
 550                .unwrap_or(true);
 551
 552            self.foreground_process_info = Some(process_info.clone());
 553
 554            res
 555        } else {
 556            false
 557        }
 558    }
 559
 560    ///Takes events from Alacritty and translates them to behavior on this view
 561    fn process_terminal_event(
 562        &mut self,
 563        event: &InternalEvent,
 564        term: &mut Term<ZedListener>,
 565        cx: &mut ModelContext<Self>,
 566    ) {
 567        match event {
 568            InternalEvent::ColorRequest(index, format) => {
 569                let color = term.colors()[*index].unwrap_or_else(|| {
 570                    let term_style = &cx.global::<Settings>().theme.terminal;
 571                    to_alac_rgb(get_color_at_index(index, &term_style.colors))
 572                });
 573                self.write_to_pty(format(color))
 574            }
 575            InternalEvent::Resize(new_size) => {
 576                self.cur_size = *new_size;
 577
 578                self.pty_tx.0.send(Msg::Resize((*new_size).into())).ok();
 579
 580                term.resize(*new_size);
 581            }
 582            InternalEvent::Clear => {
 583                self.write_to_pty("\x0c".to_string());
 584                term.clear_screen(ClearMode::Saved);
 585            }
 586            InternalEvent::Scroll(scroll) => {
 587                term.scroll_display(*scroll);
 588            }
 589            InternalEvent::SetSelection(selection) => {
 590                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
 591
 592                if let Some((_, head)) = selection {
 593                    self.selection_head = Some(*head);
 594                }
 595                cx.emit(Event::SelectionsChanged)
 596            }
 597            InternalEvent::UpdateSelection(position) => {
 598                if let Some(mut selection) = term.selection.take() {
 599                    let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
 600                    let side = mouse_side(*position, self.cur_size);
 601
 602                    selection.update(point, side);
 603                    term.selection = Some(selection);
 604
 605                    self.selection_head = Some(point);
 606                    cx.emit(Event::SelectionsChanged)
 607                }
 608            }
 609
 610            InternalEvent::Copy => {
 611                if let Some(txt) = term.selection_to_string() {
 612                    cx.write_to_clipboard(ClipboardItem::new(txt))
 613                }
 614            }
 615            InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
 616        }
 617    }
 618
 619    pub fn last_content(&self) -> &TerminalContent {
 620        &self.last_content
 621    }
 622
 623    //To test:
 624    //- Activate match on terminal (scrolling and selection)
 625    //- Editor search snapping behavior
 626
 627    pub fn activate_match(&mut self, index: usize) {
 628        if let Some(search_match) = self.matches.get(index).cloned() {
 629            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
 630
 631            self.events
 632                .push_back(InternalEvent::ScrollToPoint(*search_match.start()));
 633        }
 634    }
 635
 636    fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
 637        self.events
 638            .push_back(InternalEvent::SetSelection(selection));
 639    }
 640
 641    pub fn copy(&mut self) {
 642        self.events.push_back(InternalEvent::Copy);
 643    }
 644
 645    pub fn clear(&mut self) {
 646        self.events.push_back(InternalEvent::Clear)
 647    }
 648
 649    ///Resize the terminal and the PTY.
 650    pub fn set_size(&mut self, new_size: TerminalSize) {
 651        self.events.push_back(InternalEvent::Resize(new_size))
 652    }
 653
 654    ///Write the Input payload to the tty.
 655    fn write_to_pty(&self, input: String) {
 656        self.pty_tx.notify(input.into_bytes());
 657    }
 658
 659    pub fn input(&mut self, input: String) {
 660        self.events
 661            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
 662        self.events.push_back(InternalEvent::SetSelection(None));
 663
 664        self.write_to_pty(input);
 665    }
 666
 667    pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
 668        let esc = to_esc_str(keystroke, &self.last_content.mode);
 669        if let Some(esc) = esc {
 670            self.input(esc);
 671            true
 672        } else {
 673            false
 674        }
 675    }
 676
 677    ///Paste text into the terminal
 678    pub fn paste(&mut self, text: &str) {
 679        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
 680            format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
 681        } else {
 682            text.replace("\r\n", "\r").replace('\n', "\r")
 683        };
 684        self.input(paste_text)
 685    }
 686
 687    pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
 688        let term = self.term.clone();
 689
 690        let mut terminal = if let Some(term) = term.try_lock_unfair() {
 691            term
 692        } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
 693            term.lock_unfair() //It's been too long, force block
 694        } else if let None = self.sync_task {
 695            //Skip this frame
 696            let delay = cx.background().timer(Duration::from_millis(16));
 697            self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
 698                delay.await;
 699                cx.update(|cx| {
 700                    if let Some(handle) = weak_handle.upgrade(cx) {
 701                        handle.update(cx, |terminal, cx| {
 702                            terminal.sync_task.take();
 703                            cx.notify();
 704                        });
 705                    }
 706                });
 707            }));
 708            return;
 709        } else {
 710            //No lock and delayed rendering already scheduled, nothing to do
 711            return;
 712        };
 713
 714        if self.update_process_info() {
 715            cx.emit(Event::TitleChanged);
 716        }
 717
 718        //Note that the ordering of events matters for event processing
 719        while let Some(e) = self.events.pop_front() {
 720            self.process_terminal_event(&e, &mut terminal, cx)
 721        }
 722
 723        self.last_content = Self::make_content(&terminal);
 724        self.last_synced = Instant::now();
 725    }
 726
 727    fn make_content(term: &Term<ZedListener>) -> TerminalContent {
 728        let content = term.renderable_content();
 729        TerminalContent {
 730            cells: content
 731                .display_iter
 732                //TODO: Add this once there's a way to retain empty lines
 733                // .filter(|ic| {
 734                //     !ic.flags.contains(Flags::HIDDEN)
 735                //         && !(ic.bg == Named(NamedColor::Background)
 736                //             && ic.c == ' '
 737                //             && !ic.flags.contains(Flags::INVERSE))
 738                // })
 739                .map(|ic| IndexedCell {
 740                    point: ic.point,
 741                    cell: ic.cell.clone(),
 742                })
 743                .collect::<Vec<IndexedCell>>(),
 744            mode: content.mode,
 745            display_offset: content.display_offset,
 746            selection_text: term.selection_to_string(),
 747            selection: content.selection,
 748            cursor: content.cursor,
 749            cursor_char: term.grid()[content.cursor.point].c,
 750        }
 751    }
 752
 753    pub fn focus_in(&self) {
 754        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
 755            self.write_to_pty("\x1b[I".to_string());
 756        }
 757    }
 758
 759    pub fn focus_out(&self) {
 760        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
 761            self.write_to_pty("\x1b[O".to_string());
 762        }
 763    }
 764
 765    pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
 766        match self.last_mouse {
 767            Some((old_point, old_side)) => {
 768                if old_point == point && old_side == side {
 769                    false
 770                } else {
 771                    self.last_mouse = Some((point, side));
 772                    true
 773                }
 774            }
 775            None => {
 776                self.last_mouse = Some((point, side));
 777                true
 778            }
 779        }
 780    }
 781
 782    pub fn mouse_mode(&self, shift: bool) -> bool {
 783        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
 784    }
 785
 786    pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
 787        let position = e.position.sub(origin);
 788
 789        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
 790        let side = mouse_side(position, self.cur_size);
 791
 792        if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
 793            if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
 794                self.pty_tx.notify(bytes);
 795            }
 796        }
 797    }
 798
 799    pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
 800        let position = e.position.sub(origin);
 801
 802        if !self.mouse_mode(e.shift) {
 803            // Alacritty has the same ordering, of first updating the selection
 804            // then scrolling 15ms later
 805            self.events
 806                .push_back(InternalEvent::UpdateSelection(position));
 807
 808            // Doesn't make sense to scroll the alt screen
 809            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
 810                let scroll_delta = match self.drag_line_delta(e) {
 811                    Some(value) => value,
 812                    None => return,
 813                };
 814
 815                let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
 816
 817                self.events
 818                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
 819                self.events
 820                    .push_back(InternalEvent::UpdateSelection(position))
 821            }
 822        }
 823    }
 824
 825    fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
 826        //TODO: Why do these need to be doubled? Probably the same problem that the IME has
 827        let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
 828        let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
 829        let scroll_delta = if e.position.y() < top {
 830            (top - e.position.y()).powf(1.1)
 831        } else if e.position.y() > bottom {
 832            -((e.position.y() - bottom).powf(1.1))
 833        } else {
 834            return None; //Nothing to do
 835        };
 836        Some(scroll_delta)
 837    }
 838
 839    pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
 840        let position = e.position.sub(origin);
 841        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
 842        let side = mouse_side(position, self.cur_size);
 843
 844        if self.mouse_mode(e.shift) {
 845            if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
 846                self.pty_tx.notify(bytes);
 847            }
 848        } else if e.button == MouseButton::Left {
 849            self.events.push_back(InternalEvent::SetSelection(Some((
 850                Selection::new(SelectionType::Simple, point, side),
 851                point,
 852            ))));
 853        }
 854    }
 855
 856    pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
 857        let position = e.position.sub(origin);
 858
 859        if !self.mouse_mode(e.shift) {
 860            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
 861            let side = mouse_side(position, self.cur_size);
 862
 863            let selection_type = match e.click_count {
 864                0 => return, //This is a release
 865                1 => Some(SelectionType::Simple),
 866                2 => Some(SelectionType::Semantic),
 867                3 => Some(SelectionType::Lines),
 868                _ => None,
 869            };
 870
 871            let selection =
 872                selection_type.map(|selection_type| Selection::new(selection_type, point, side));
 873
 874            if let Some(sel) = selection {
 875                self.events
 876                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
 877            }
 878        }
 879    }
 880
 881    pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
 882        let position = e.position.sub(origin);
 883        if self.mouse_mode(e.shift) {
 884            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
 885
 886            if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
 887                self.pty_tx.notify(bytes);
 888            }
 889        } else if e.button == MouseButton::Left {
 890            // Seems pretty standard to automatically copy on mouse_up for terminals,
 891            // so let's do that here
 892            self.copy();
 893        }
 894        self.last_mouse = None;
 895    }
 896
 897    ///Scroll the terminal
 898    pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
 899        let mouse_mode = self.mouse_mode(e.shift);
 900
 901        if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
 902            if mouse_mode {
 903                let point = mouse_point(
 904                    e.position.sub(origin),
 905                    self.cur_size,
 906                    self.last_content.display_offset,
 907                );
 908
 909                if let Some(scrolls) =
 910                    scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
 911                {
 912                    for scroll in scrolls {
 913                        self.pty_tx.notify(scroll);
 914                    }
 915                };
 916            } else if self
 917                .last_content
 918                .mode
 919                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
 920                && !e.shift
 921            {
 922                self.pty_tx.notify(alt_scroll(scroll_lines))
 923            } else {
 924                if scroll_lines != 0 {
 925                    let scroll = AlacScroll::Delta(scroll_lines);
 926
 927                    self.events.push_back(InternalEvent::Scroll(scroll));
 928                }
 929            }
 930        }
 931    }
 932
 933    fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
 934        let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
 935
 936        match e.phase {
 937            /* Reset scroll state on started */
 938            Some(gpui::TouchPhase::Started) => {
 939                self.scroll_px = 0.;
 940                None
 941            }
 942            /* Calculate the appropriate scroll lines */
 943            Some(gpui::TouchPhase::Moved) => {
 944                let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
 945
 946                self.scroll_px += e.delta.y() * scroll_multiplier;
 947
 948                let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
 949
 950                // Whenever we hit the edges, reset our stored scroll to 0
 951                // so we can respond to changes in direction quickly
 952                self.scroll_px %= self.cur_size.height;
 953
 954                Some(new_offset - old_offset)
 955            }
 956            /* Fall back to delta / line_height */
 957            None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
 958            _ => None,
 959        }
 960    }
 961
 962    pub fn find_matches(
 963        &mut self,
 964        query: project::search::SearchQuery,
 965        cx: &mut ModelContext<Self>,
 966    ) -> Task<Vec<RangeInclusive<Point>>> {
 967        let term = self.term.clone();
 968        cx.background().spawn(async move {
 969            let searcher = match query {
 970                project::search::SearchQuery::Text { query, .. } => {
 971                    RegexSearch::new(query.as_ref())
 972                }
 973                project::search::SearchQuery::Regex { query, .. } => {
 974                    RegexSearch::new(query.as_ref())
 975                }
 976            };
 977
 978            if searcher.is_err() {
 979                return Vec::new();
 980            }
 981            let searcher = searcher.unwrap();
 982
 983            let term = term.lock();
 984
 985            make_search_matches(&term, &searcher).collect()
 986        })
 987    }
 988}
 989
 990impl Drop for Terminal {
 991    fn drop(&mut self) {
 992        self.pty_tx.0.send(Msg::Shutdown).ok();
 993    }
 994}
 995
 996impl Entity for Terminal {
 997    type Event = Event;
 998}
 999
1000fn make_selection(range: &RangeInclusive<Point>) -> Selection {
1001    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1002    selection.update(*range.end(), AlacDirection::Right);
1003    selection
1004}
1005
1006/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
1007/// Iterate over all visible regex matches.
1008fn make_search_matches<'a, T>(
1009    term: &'a Term<T>,
1010    regex: &'a RegexSearch,
1011) -> impl Iterator<Item = Match> + 'a {
1012    let viewport_start = Line(-(term.grid().display_offset() as i32));
1013    let viewport_end = viewport_start + term.bottommost_line();
1014    let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
1015    let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
1016    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1017    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1018
1019    RegexIter::new(start, end, AlacDirection::Right, term, regex)
1020        .skip_while(move |rm| rm.end().line < viewport_start)
1021        .take_while(move |rm| rm.start().line <= viewport_end)
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    pub mod terminal_test_context;
1027}