terminal.rs

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