terminal.rs

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