terminal.rs

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