1pub mod mappings;
2pub mod terminal_container_view;
3pub mod terminal_element;
4pub mod terminal_view;
5
6use alacritty_terminal::{
7 ansi::{ClearMode, Handler},
8 config::{Config, Program, PtyConfig, Scrolling},
9 event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
10 event_loop::{EventLoop, Msg, Notifier},
11 grid::{Dimensions, Scroll as AlacScroll},
12 index::{Column, Direction as AlacDirection, Line, Point},
13 selection::{Selection, SelectionRange, SelectionType},
14 sync::FairMutex,
15 term::{
16 cell::Cell,
17 color::Rgb,
18 search::{Match, RegexIter, RegexSearch},
19 RenderableCursor, TermMode,
20 },
21 tty::{self, setup_env},
22 Term,
23};
24use anyhow::{bail, Result};
25
26use futures::{
27 channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
28 FutureExt,
29};
30
31use mappings::mouse::{
32 alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
33};
34
35use procinfo::LocalProcessInfo;
36use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
37
38use std::{
39 collections::{HashMap, VecDeque},
40 fmt::Display,
41 ops::{Deref, RangeInclusive, Sub},
42 os::unix::prelude::AsRawFd,
43 path::PathBuf,
44 sync::Arc,
45 time::{Duration, Instant},
46};
47use thiserror::Error;
48
49use gpui::{
50 geometry::vector::{vec2f, Vector2F},
51 keymap::Keystroke,
52 scene::{
53 ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
54 },
55 ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
56};
57
58use crate::mappings::{
59 colors::{get_color_at_index, to_alac_rgb},
60 keys::to_esc_str,
61};
62
63///Initialize and register all of our action handlers
64pub fn init(cx: &mut MutableAppContext) {
65 terminal_view::init(cx);
66 terminal_container_view::init(cx);
67}
68
69///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
70///Scroll multiplier that is set to 3 by default. This will be removed when I
71///Implement scroll bars.
72const SCROLL_MULTIPLIER: f32 = 4.;
73// const MAX_SEARCH_LINES: usize = 100;
74const DEBUG_TERMINAL_WIDTH: f32 = 500.;
75const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
76const DEBUG_CELL_WIDTH: f32 = 5.;
77const DEBUG_LINE_HEIGHT: f32 = 5.;
78
79///Upward flowing events, for changing the title and such
80#[derive(Clone, Copy, Debug)]
81pub enum Event {
82 TitleChanged,
83 BreadcrumbsChanged,
84 CloseTerminal,
85 Bell,
86 Wakeup,
87 BlinkChanged,
88 SelectionsChanged,
89}
90
91#[derive(Clone)]
92enum InternalEvent {
93 ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
94 Resize(TerminalSize),
95 Clear,
96 // FocusNextMatch,
97 Scroll(AlacScroll),
98 ScrollToPoint(Point),
99 SetSelection(Option<(Selection, Point)>),
100 UpdateSelection(Vector2F),
101 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 cell_width: f32,
117 line_height: f32,
118 height: f32,
119 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: Option<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) -> Option<String> {
227 self.shell.as_ref().map(|shell| match 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 self.shell
236 .clone()
237 .map(|shell| match shell {
238 Shell::System => "<system defined shell>".to_string(),
239
240 Shell::Program(s) => s,
241 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
242 })
243 .unwrap_or_else(|| "<none specified, using system defined shell>".to_string())
244 }
245}
246
247impl Display for TerminalError {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 let dir_string: String = self.fmt_directory();
250 let shell = self.fmt_shell();
251
252 write!(
253 f,
254 "Working directory: {} Shell command: `{}`, IOError: {}",
255 dir_string, shell, self.source
256 )
257 }
258}
259
260pub struct TerminalBuilder {
261 terminal: Terminal,
262 events_rx: UnboundedReceiver<AlacTermEvent>,
263}
264
265impl TerminalBuilder {
266 pub fn new(
267 working_directory: Option<PathBuf>,
268 shell: Option<Shell>,
269 env: Option<HashMap<String, String>>,
270 initial_size: TerminalSize,
271 blink_settings: Option<TerminalBlink>,
272 alternate_scroll: &AlternateScroll,
273 window_id: usize,
274 ) -> Result<TerminalBuilder> {
275 let pty_config = {
276 let alac_shell = shell.clone().and_then(|shell| match shell {
277 Shell::System => None,
278 Shell::Program(program) => Some(Program::Just(program)),
279 Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
280 });
281
282 PtyConfig {
283 shell: alac_shell,
284 working_directory: working_directory.clone(),
285 hold: false,
286 }
287 };
288
289 let mut env = env.unwrap_or_default();
290
291 //TODO: Properly set the current locale,
292 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
293
294 let alac_scrolling = Scrolling::default();
295 // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
296
297 let config = Config {
298 pty_config: pty_config.clone(),
299 env,
300 scrolling: alac_scrolling,
301 ..Default::default()
302 };
303
304 setup_env(&config);
305
306 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
307 //TODO: Remove with a bounded sender which can be dispatched on &self
308 let (events_tx, events_rx) = unbounded();
309 //Set up the terminal...
310 let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
311
312 //Start off blinking if we need to
313 if let Some(TerminalBlink::On) = blink_settings {
314 term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
315 }
316
317 //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
318 if let AlternateScroll::Off = alternate_scroll {
319 term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
320 }
321
322 let term = Arc::new(FairMutex::new(term));
323
324 //Setup the pty...
325 let pty = match tty::new(&pty_config, initial_size.into(), window_id as u64) {
326 Ok(pty) => pty,
327 Err(error) => {
328 bail!(TerminalError {
329 directory: working_directory,
330 shell,
331 source: error,
332 });
333 }
334 };
335
336 let fd = pty.file().as_raw_fd();
337 let shell_pid = pty.child().id();
338
339 //And connect them together
340 let event_loop = EventLoop::new(
341 term.clone(),
342 ZedListener(events_tx.clone()),
343 pty,
344 pty_config.hold,
345 false,
346 );
347
348 //Kick things off
349 let pty_tx = event_loop.channel();
350 let _io_thread = event_loop.spawn();
351
352 let terminal = Terminal {
353 pty_tx: Notifier(pty_tx),
354 term,
355 events: VecDeque::with_capacity(10), //Should never get this high.
356 last_content: Default::default(),
357 cur_size: initial_size,
358 last_mouse: None,
359 matches: Vec::new(),
360 last_synced: Instant::now(),
361 sync_task: None,
362 selection_head: None,
363 shell_fd: fd as u32,
364 shell_pid,
365 foreground_process_info: None,
366 breadcrumb_text: String::new(),
367 scroll_px: 0.,
368 };
369
370 Ok(TerminalBuilder {
371 terminal,
372 events_rx,
373 })
374 }
375
376 pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
377 //Event loop
378 cx.spawn_weak(|this, mut cx| async move {
379 use futures::StreamExt;
380
381 while let Some(event) = self.events_rx.next().await {
382 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
383 //Process the first event immediately for lowered latency
384 this.process_event(&event, cx);
385 });
386
387 'outer: loop {
388 let mut events = vec![];
389 let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
390
391 loop {
392 futures::select_biased! {
393 _ = timer => break,
394 event = self.events_rx.next() => {
395 if let Some(event) = event {
396 events.push(event);
397 if events.len() > 100 {
398 break;
399 }
400 } else {
401 break;
402 }
403 },
404 }
405 }
406
407 if events.is_empty() {
408 smol::future::yield_now().await;
409 break 'outer;
410 } else {
411 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
412 for event in events {
413 this.process_event(&event, cx);
414 }
415 });
416 smol::future::yield_now().await;
417 }
418 }
419 }
420
421 Some(())
422 })
423 .detach();
424
425 self.terminal
426 }
427}
428
429#[derive(Debug, Clone)]
430struct IndexedCell {
431 point: Point,
432 cell: Cell,
433}
434
435impl Deref for IndexedCell {
436 type Target = Cell;
437
438 #[inline]
439 fn deref(&self) -> &Cell {
440 &self.cell
441 }
442}
443
444#[derive(Clone)]
445pub struct TerminalContent {
446 cells: Vec<IndexedCell>,
447 mode: TermMode,
448 display_offset: usize,
449 selection_text: Option<String>,
450 selection: Option<SelectionRange>,
451 cursor: RenderableCursor,
452 cursor_char: char,
453}
454
455impl Default for TerminalContent {
456 fn default() -> Self {
457 TerminalContent {
458 cells: Default::default(),
459 mode: Default::default(),
460 display_offset: Default::default(),
461 selection_text: Default::default(),
462 selection: Default::default(),
463 cursor: RenderableCursor {
464 shape: alacritty_terminal::ansi::CursorShape::Block,
465 point: Point::new(Line(0), Column(0)),
466 },
467 cursor_char: Default::default(),
468 }
469 }
470}
471
472pub struct Terminal {
473 pty_tx: Notifier,
474 term: Arc<FairMutex<Term<ZedListener>>>,
475 events: VecDeque<InternalEvent>,
476 last_mouse: Option<(Point, AlacDirection)>,
477 pub matches: Vec<RangeInclusive<Point>>,
478 cur_size: TerminalSize,
479 last_content: TerminalContent,
480 last_synced: Instant,
481 sync_task: Option<Task<()>>,
482 selection_head: Option<Point>,
483 breadcrumb_text: String,
484 shell_pid: u32,
485 shell_fd: u32,
486 foreground_process_info: Option<LocalProcessInfo>,
487 scroll_px: f32,
488}
489
490impl Terminal {
491 fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
492 match event {
493 AlacTermEvent::Title(title) => {
494 self.breadcrumb_text = title.to_string();
495 cx.emit(Event::BreadcrumbsChanged);
496 }
497 AlacTermEvent::ResetTitle => {
498 self.breadcrumb_text = String::new();
499 cx.emit(Event::BreadcrumbsChanged);
500 }
501 AlacTermEvent::ClipboardStore(_, data) => {
502 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
503 }
504 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
505 &cx.read_from_clipboard()
506 .map(|ci| ci.text().to_string())
507 .unwrap_or_else(|| "".to_string()),
508 )),
509 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
510 AlacTermEvent::TextAreaSizeRequest(format) => {
511 self.write_to_pty(format(self.cur_size.into()))
512 }
513 AlacTermEvent::CursorBlinkingChange => {
514 cx.emit(Event::BlinkChanged);
515 }
516 AlacTermEvent::Bell => {
517 cx.emit(Event::Bell);
518 }
519 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
520 AlacTermEvent::MouseCursorDirty => {
521 //NOOP, Handled in render
522 }
523 AlacTermEvent::Wakeup => {
524 cx.emit(Event::Wakeup);
525
526 if self.update_process_info() {
527 cx.emit(Event::TitleChanged)
528 }
529 }
530 AlacTermEvent::ColorRequest(idx, fun_ptr) => {
531 self.events
532 .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
533 }
534 }
535 }
536
537 /// Update the cached process info, returns whether the Zed-relevant info has changed
538 fn update_process_info(&mut self) -> bool {
539 let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
540 if pid < 0 {
541 pid = self.shell_pid as i32;
542 }
543
544 if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
545 let res = self
546 .foreground_process_info
547 .as_ref()
548 .map(|old_info| {
549 process_info.cwd != old_info.cwd || process_info.name != old_info.name
550 })
551 .unwrap_or(true);
552
553 self.foreground_process_info = Some(process_info.clone());
554
555 res
556 } else {
557 false
558 }
559 }
560
561 ///Takes events from Alacritty and translates them to behavior on this view
562 fn process_terminal_event(
563 &mut self,
564 event: &InternalEvent,
565 term: &mut Term<ZedListener>,
566 cx: &mut ModelContext<Self>,
567 ) {
568 match event {
569 InternalEvent::ColorRequest(index, format) => {
570 let color = term.colors()[*index].unwrap_or_else(|| {
571 let term_style = &cx.global::<Settings>().theme.terminal;
572 to_alac_rgb(get_color_at_index(index, &term_style.colors))
573 });
574 self.write_to_pty(format(color))
575 }
576 InternalEvent::Resize(mut new_size) => {
577 new_size.height = f32::max(new_size.line_height, new_size.height);
578 new_size.width = f32::max(new_size.cell_width, new_size.width);
579
580 self.cur_size = new_size.clone();
581
582 self.pty_tx.0.send(Msg::Resize((new_size).into())).ok();
583
584 // When this resize happens
585 // We go from 737px -> 703px height
586 // This means there is 1 less line
587 // that means the delta is 1
588 // That means the selection is rotated by -1
589
590 term.resize(new_size);
591 }
592 InternalEvent::Clear => {
593 self.write_to_pty("\x0c".to_string());
594 term.clear_screen(ClearMode::Saved);
595 }
596 InternalEvent::Scroll(scroll) => {
597 term.scroll_display(*scroll);
598 }
599 InternalEvent::SetSelection(selection) => {
600 term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
601
602 if let Some((_, head)) = selection {
603 self.selection_head = Some(*head);
604 }
605 cx.emit(Event::SelectionsChanged)
606 }
607 InternalEvent::UpdateSelection(position) => {
608 if let Some(mut selection) = term.selection.take() {
609 let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
610 let side = mouse_side(*position, self.cur_size);
611
612 selection.update(point, side);
613 term.selection = Some(selection);
614
615 self.selection_head = Some(point);
616 cx.emit(Event::SelectionsChanged)
617 }
618 }
619
620 InternalEvent::Copy => {
621 if let Some(txt) = term.selection_to_string() {
622 cx.write_to_clipboard(ClipboardItem::new(txt))
623 }
624 }
625 InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
626 }
627 }
628
629 pub fn last_content(&self) -> &TerminalContent {
630 &self.last_content
631 }
632
633 //To test:
634 //- Activate match on terminal (scrolling and selection)
635 //- Editor search snapping behavior
636
637 pub fn activate_match(&mut self, index: usize) {
638 if let Some(search_match) = self.matches.get(index).cloned() {
639 self.set_selection(Some((make_selection(&search_match), *search_match.end())));
640
641 self.events
642 .push_back(InternalEvent::ScrollToPoint(*search_match.start()));
643 }
644 }
645
646 fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
647 self.events
648 .push_back(InternalEvent::SetSelection(selection));
649 }
650
651 pub fn copy(&mut self) {
652 self.events.push_back(InternalEvent::Copy);
653 }
654
655 pub fn clear(&mut self) {
656 self.events.push_back(InternalEvent::Clear)
657 }
658
659 ///Resize the terminal and the PTY.
660 pub fn set_size(&mut self, new_size: TerminalSize) {
661 self.events.push_back(InternalEvent::Resize(new_size))
662 }
663
664 ///Write the Input payload to the tty.
665 fn write_to_pty(&self, input: String) {
666 self.pty_tx.notify(input.into_bytes());
667 }
668
669 pub fn input(&mut self, input: String) {
670 self.events
671 .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
672 self.events.push_back(InternalEvent::SetSelection(None));
673
674 self.write_to_pty(input);
675 }
676
677 pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
678 let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
679 if let Some(esc) = esc {
680 self.input(esc);
681 true
682 } else {
683 false
684 }
685 }
686
687 ///Paste text into the terminal
688 pub fn paste(&mut self, text: &str) {
689 let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
690 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
691 } else {
692 text.replace("\r\n", "\r").replace('\n', "\r")
693 };
694 self.input(paste_text)
695 }
696
697 pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
698 let term = self.term.clone();
699
700 let mut terminal = if let Some(term) = term.try_lock_unfair() {
701 term
702 } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
703 term.lock_unfair() //It's been too long, force block
704 } else if let None = self.sync_task {
705 //Skip this frame
706 let delay = cx.background().timer(Duration::from_millis(16));
707 self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
708 delay.await;
709 cx.update(|cx| {
710 if let Some(handle) = weak_handle.upgrade(cx) {
711 handle.update(cx, |terminal, cx| {
712 terminal.sync_task.take();
713 cx.notify();
714 });
715 }
716 });
717 }));
718 return;
719 } else {
720 //No lock and delayed rendering already scheduled, nothing to do
721 return;
722 };
723
724 if self.update_process_info() {
725 cx.emit(Event::TitleChanged);
726 }
727
728 //Note that the ordering of events matters for event processing
729 while let Some(e) = self.events.pop_front() {
730 self.process_terminal_event(&e, &mut terminal, cx)
731 }
732
733 self.last_content = Self::make_content(&terminal);
734 self.last_synced = Instant::now();
735 }
736
737 fn make_content(term: &Term<ZedListener>) -> TerminalContent {
738 let content = term.renderable_content();
739 TerminalContent {
740 cells: content
741 .display_iter
742 //TODO: Add this once there's a way to retain empty lines
743 // .filter(|ic| {
744 // !ic.flags.contains(Flags::HIDDEN)
745 // && !(ic.bg == Named(NamedColor::Background)
746 // && ic.c == ' '
747 // && !ic.flags.contains(Flags::INVERSE))
748 // })
749 .map(|ic| IndexedCell {
750 point: ic.point,
751 cell: ic.cell.clone(),
752 })
753 .collect::<Vec<IndexedCell>>(),
754 mode: content.mode,
755 display_offset: content.display_offset,
756 selection_text: term.selection_to_string(),
757 selection: content.selection,
758 cursor: content.cursor,
759 cursor_char: term.grid()[content.cursor.point].c,
760 }
761 }
762
763 pub fn focus_in(&self) {
764 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
765 self.write_to_pty("\x1b[I".to_string());
766 }
767 }
768
769 pub fn focus_out(&self) {
770 if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
771 self.write_to_pty("\x1b[O".to_string());
772 }
773 }
774
775 pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
776 match self.last_mouse {
777 Some((old_point, old_side)) => {
778 if old_point == point && old_side == side {
779 false
780 } else {
781 self.last_mouse = Some((point, side));
782 true
783 }
784 }
785 None => {
786 self.last_mouse = Some((point, side));
787 true
788 }
789 }
790 }
791
792 pub fn mouse_mode(&self, shift: bool) -> bool {
793 self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
794 }
795
796 pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
797 let position = e.position.sub(origin);
798
799 let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
800 let side = mouse_side(position, self.cur_size);
801
802 if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
803 if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
804 self.pty_tx.notify(bytes);
805 }
806 }
807 }
808
809 pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
810 let position = e.position.sub(origin);
811
812 if !self.mouse_mode(e.shift) {
813 // Alacritty has the same ordering, of first updating the selection
814 // then scrolling 15ms later
815 self.events
816 .push_back(InternalEvent::UpdateSelection(position));
817
818 // Doesn't make sense to scroll the alt screen
819 if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
820 let scroll_delta = match self.drag_line_delta(e) {
821 Some(value) => value,
822 None => return,
823 };
824
825 let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
826
827 self.events
828 .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
829 self.events
830 .push_back(InternalEvent::UpdateSelection(position))
831 }
832 }
833 }
834
835 fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
836 //TODO: Why do these need to be doubled? Probably the same problem that the IME has
837 let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
838 let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
839 let scroll_delta = if e.position.y() < top {
840 (top - e.position.y()).powf(1.1)
841 } else if e.position.y() > bottom {
842 -((e.position.y() - bottom).powf(1.1))
843 } else {
844 return None; //Nothing to do
845 };
846 Some(scroll_delta)
847 }
848
849 pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
850 let position = e.position.sub(origin);
851 let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
852 let side = mouse_side(position, self.cur_size);
853
854 if self.mouse_mode(e.shift) {
855 if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
856 self.pty_tx.notify(bytes);
857 }
858 } else if e.button == MouseButton::Left {
859 self.events.push_back(InternalEvent::SetSelection(Some((
860 Selection::new(SelectionType::Simple, point, side),
861 point,
862 ))));
863 }
864 }
865
866 pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
867 let position = e.position.sub(origin);
868
869 if !self.mouse_mode(e.shift) {
870 let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
871 let side = mouse_side(position, self.cur_size);
872
873 let selection_type = match e.click_count {
874 0 => return, //This is a release
875 1 => Some(SelectionType::Simple),
876 2 => Some(SelectionType::Semantic),
877 3 => Some(SelectionType::Lines),
878 _ => None,
879 };
880
881 let selection =
882 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
883
884 if let Some(sel) = selection {
885 self.events
886 .push_back(InternalEvent::SetSelection(Some((sel, point))));
887 }
888 }
889 }
890
891 pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
892 let position = e.position.sub(origin);
893 if self.mouse_mode(e.shift) {
894 let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
895
896 if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
897 self.pty_tx.notify(bytes);
898 }
899 } else if e.button == MouseButton::Left {
900 // Seems pretty standard to automatically copy on mouse_up for terminals,
901 // so let's do that here
902 self.copy();
903 }
904 self.last_mouse = None;
905 }
906
907 ///Scroll the terminal
908 pub fn scroll_wheel(&mut self, e: ScrollWheelRegionEvent, origin: Vector2F) {
909 let mouse_mode = self.mouse_mode(e.shift);
910
911 if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
912 if mouse_mode {
913 let point = mouse_point(
914 e.position.sub(origin),
915 self.cur_size,
916 self.last_content.display_offset,
917 );
918
919 if let Some(scrolls) =
920 scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
921 {
922 for scroll in scrolls {
923 self.pty_tx.notify(scroll);
924 }
925 };
926 } else if self
927 .last_content
928 .mode
929 .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
930 && !e.shift
931 {
932 self.pty_tx.notify(alt_scroll(scroll_lines))
933 } else {
934 if scroll_lines != 0 {
935 let scroll = AlacScroll::Delta(scroll_lines);
936
937 self.events.push_back(InternalEvent::Scroll(scroll));
938 }
939 }
940 }
941 }
942
943 fn determine_scroll_lines(
944 &mut self,
945 e: &ScrollWheelRegionEvent,
946 mouse_mode: bool,
947 ) -> Option<i32> {
948 let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
949
950 match e.phase {
951 /* Reset scroll state on started */
952 Some(gpui::TouchPhase::Started) => {
953 self.scroll_px = 0.;
954 None
955 }
956 /* Calculate the appropriate scroll lines */
957 Some(gpui::TouchPhase::Moved) => {
958 let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
959
960 self.scroll_px += e.delta.y() * scroll_multiplier;
961
962 let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
963
964 // Whenever we hit the edges, reset our stored scroll to 0
965 // so we can respond to changes in direction quickly
966 self.scroll_px %= self.cur_size.height;
967
968 Some(new_offset - old_offset)
969 }
970 /* Fall back to delta / line_height */
971 None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
972 _ => None,
973 }
974 }
975
976 pub fn find_matches(
977 &mut self,
978 query: project::search::SearchQuery,
979 cx: &mut ModelContext<Self>,
980 ) -> Task<Vec<RangeInclusive<Point>>> {
981 let term = self.term.clone();
982 cx.background().spawn(async move {
983 let searcher = match query {
984 project::search::SearchQuery::Text { query, .. } => {
985 RegexSearch::new(query.as_ref())
986 }
987 project::search::SearchQuery::Regex { query, .. } => {
988 RegexSearch::new(query.as_ref())
989 }
990 };
991
992 if searcher.is_err() {
993 return Vec::new();
994 }
995 let searcher = searcher.unwrap();
996
997 let term = term.lock();
998
999 all_search_matches(&term, &searcher).collect()
1000 })
1001 }
1002}
1003
1004impl Drop for Terminal {
1005 fn drop(&mut self) {
1006 self.pty_tx.0.send(Msg::Shutdown).ok();
1007 }
1008}
1009
1010impl Entity for Terminal {
1011 type Event = Event;
1012}
1013
1014fn make_selection(range: &RangeInclusive<Point>) -> Selection {
1015 let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1016 selection.update(*range.end(), AlacDirection::Right);
1017 selection
1018}
1019
1020/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
1021/// Iterate over all visible regex matches.
1022// fn visible_search_matches<'a, T>(
1023// term: &'a Term<T>,
1024// regex: &'a RegexSearch,
1025// ) -> impl Iterator<Item = Match> + 'a {
1026// let viewport_start = Line(-(term.grid().display_offset() as i32));
1027// let viewport_end = viewport_start + term.bottommost_line();
1028// let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
1029// let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
1030// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1031// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1032
1033// RegexIter::new(start, end, AlacDirection::Right, term, regex)
1034// .skip_while(move |rm| rm.end().line < viewport_start)
1035// .take_while(move |rm| rm.start().line <= viewport_end)
1036// }
1037
1038fn all_search_matches<'a, T>(
1039 term: &'a Term<T>,
1040 regex: &'a RegexSearch,
1041) -> impl Iterator<Item = Match> + 'a {
1042 let start = Point::new(term.grid().topmost_line(), Column(0));
1043 let end = Point::new(term.grid().bottommost_line(), term.grid().last_column());
1044 RegexIter::new(start, end, AlacDirection::Right, term, regex)
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 pub mod terminal_test_context;
1050}