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