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