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