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