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