1pub mod mappings;
2pub mod modal;
3pub mod search;
4pub mod terminal_container_view;
5pub mod terminal_element;
6pub mod terminal_view;
7
8use alacritty_terminal::{
9 ansi::{ClearMode, Handler},
10 config::{Config, Program, PtyConfig, Scrolling},
11 event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
12 event_loop::{EventLoop, Msg, Notifier},
13 grid::{Dimensions, Scroll as AlacScroll},
14 index::{Direction, Point},
15 selection::{Selection, SelectionType},
16 sync::FairMutex,
17 term::{color::Rgb, search::RegexSearch, RenderableContent, TermMode},
18 tty::{self, setup_env},
19 Term,
20};
21use anyhow::{bail, Result};
22use futures::{
23 channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
24 FutureExt,
25};
26
27use mappings::mouse::{
28 alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
29};
30use modal::deploy_modal;
31use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
32use std::{
33 collections::{HashMap, VecDeque},
34 fmt::Display,
35 ops::Sub,
36 path::PathBuf,
37 sync::Arc,
38 time::Duration,
39};
40use thiserror::Error;
41
42use gpui::{
43 geometry::vector::{vec2f, Vector2F},
44 keymap::Keystroke,
45 scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
46 ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
47 ScrollWheelEvent,
48};
49
50use crate::mappings::{
51 colors::{get_color_at_index, to_alac_rgb},
52 keys::to_esc_str,
53};
54
55///Initialize and register all of our action handlers
56pub fn init(cx: &mut MutableAppContext) {
57 cx.add_action(deploy_modal);
58
59 terminal_view::init(cx);
60 terminal_container_view::init(cx);
61}
62
63///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
64///Scroll multiplier that is set to 3 by default. This will be removed when I
65///Implement scroll bars.
66const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
67// const ALACRITTY_SEARCH_LINE_LIMIT: usize = 1000;
68const SEARCH_FORWARD: Direction = Direction::Left;
69
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///Upward flowing events, for changing the title and such
76#[derive(Clone, Copy, Debug)]
77pub enum Event {
78 TitleChanged,
79 CloseTerminal,
80 Bell,
81 Wakeup,
82 BlinkChanged,
83}
84
85#[derive(Clone)]
86enum InternalEvent {
87 ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
88 Resize(TerminalSize),
89 Clear,
90 FocusNextMatch,
91 Scroll(AlacScroll),
92 SetSelection(Option<Selection>),
93 UpdateSelection(Vector2F),
94 Copy,
95}
96
97///A translation struct for Alacritty to communicate with us from their event loop
98#[derive(Clone)]
99pub struct ZedListener(UnboundedSender<AlacTermEvent>);
100
101impl EventListener for ZedListener {
102 fn send_event(&self, event: AlacTermEvent) {
103 self.0.unbounded_send(event).ok();
104 }
105}
106
107#[derive(Clone, Copy, Debug)]
108pub struct TerminalSize {
109 cell_width: f32,
110 line_height: f32,
111 height: f32,
112 width: f32,
113}
114
115impl TerminalSize {
116 pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
117 TerminalSize {
118 cell_width,
119 line_height,
120 width: size.x(),
121 height: size.y(),
122 }
123 }
124
125 pub fn num_lines(&self) -> usize {
126 (self.height / self.line_height).floor() as usize
127 }
128
129 pub fn num_columns(&self) -> usize {
130 (self.width / self.cell_width).floor() as usize
131 }
132
133 pub fn height(&self) -> f32 {
134 self.height
135 }
136
137 pub fn width(&self) -> f32 {
138 self.width
139 }
140
141 pub fn cell_width(&self) -> f32 {
142 self.cell_width
143 }
144
145 pub fn line_height(&self) -> f32 {
146 self.line_height
147 }
148}
149impl Default for TerminalSize {
150 fn default() -> Self {
151 TerminalSize::new(
152 DEBUG_LINE_HEIGHT,
153 DEBUG_CELL_WIDTH,
154 vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
155 )
156 }
157}
158
159impl From<TerminalSize> for WindowSize {
160 fn from(val: TerminalSize) -> Self {
161 WindowSize {
162 num_lines: val.num_lines() as u16,
163 num_cols: val.num_columns() as u16,
164 cell_width: val.cell_width() as u16,
165 cell_height: val.line_height() as u16,
166 }
167 }
168}
169
170impl Dimensions for TerminalSize {
171 /// Note: this is supposed to be for the back buffer's length,
172 /// but we exclusively use it to resize the terminal, which does not
173 /// use this method. We still have to implement it for the trait though,
174 /// hence, this comment.
175 fn total_lines(&self) -> usize {
176 self.screen_lines()
177 }
178
179 fn screen_lines(&self) -> usize {
180 self.num_lines()
181 }
182
183 fn columns(&self) -> usize {
184 self.num_columns()
185 }
186}
187
188#[derive(Error, Debug)]
189pub struct TerminalError {
190 pub directory: Option<PathBuf>,
191 pub shell: Option<Shell>,
192 pub source: std::io::Error,
193}
194
195impl TerminalError {
196 pub fn fmt_directory(&self) -> String {
197 self.directory
198 .clone()
199 .map(|path| {
200 match path
201 .into_os_string()
202 .into_string()
203 .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
204 {
205 Ok(s) => s,
206 Err(s) => s,
207 }
208 })
209 .unwrap_or_else(|| {
210 let default_dir =
211 dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
212 match default_dir {
213 Some(dir) => format!("<none specified, using home directory> {}", dir),
214 None => "<none specified, could not find home directory>".to_string(),
215 }
216 })
217 }
218
219 pub fn shell_to_string(&self) -> Option<String> {
220 self.shell.as_ref().map(|shell| match shell {
221 Shell::System => "<system shell>".to_string(),
222 Shell::Program(p) => p.to_string(),
223 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
224 })
225 }
226
227 pub fn fmt_shell(&self) -> String {
228 self.shell
229 .clone()
230 .map(|shell| match shell {
231 Shell::System => {
232 let mut buf = [0; 1024];
233 let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
234
235 match pw {
236 Some(pw) => format!("<system defined shell> {}", pw.shell),
237 None => "<could not access the password file>".to_string(),
238 }
239 }
240 Shell::Program(s) => s,
241 Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
242 })
243 .unwrap_or_else(|| {
244 let mut buf = [0; 1024];
245 let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
246 match pw {
247 Some(pw) => {
248 format!("<none specified, using system defined shell> {}", pw.shell)
249 }
250 None => "<none specified, could not access the password file> {}".to_string(),
251 }
252 })
253 }
254}
255
256impl Display for TerminalError {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 let dir_string: String = self.fmt_directory();
259 let shell = self.fmt_shell();
260
261 write!(
262 f,
263 "Working directory: {} Shell command: `{}`, IOError: {}",
264 dir_string, shell, self.source
265 )
266 }
267}
268
269pub struct TerminalBuilder {
270 terminal: Terminal,
271 events_rx: UnboundedReceiver<AlacTermEvent>,
272}
273
274impl TerminalBuilder {
275 pub fn new(
276 working_directory: Option<PathBuf>,
277 shell: Option<Shell>,
278 env: Option<HashMap<String, String>>,
279 initial_size: TerminalSize,
280 blink_settings: Option<TerminalBlink>,
281 alternate_scroll: &AlternateScroll,
282 ) -> Result<TerminalBuilder> {
283 let pty_config = {
284 let alac_shell = shell.clone().and_then(|shell| match shell {
285 Shell::System => None,
286 Shell::Program(program) => Some(Program::Just(program)),
287 Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
288 });
289
290 PtyConfig {
291 shell: alac_shell,
292 working_directory: working_directory.clone(),
293 hold: false,
294 }
295 };
296
297 let mut env = env.unwrap_or_default();
298
299 //TODO: Properly set the current locale,
300 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
301
302 let alac_scrolling = Scrolling::default();
303 // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
304
305 let config = Config {
306 pty_config: pty_config.clone(),
307 env,
308 scrolling: alac_scrolling,
309 ..Default::default()
310 };
311
312 setup_env(&config);
313
314 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
315 //TODO: Remove with a bounded sender which can be dispatched on &self
316 let (events_tx, events_rx) = unbounded();
317 //Set up the terminal...
318 let mut term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
319
320 //Start off blinking if we need to
321 if let Some(TerminalBlink::On) = blink_settings {
322 term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
323 }
324
325 //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
326 if let AlternateScroll::Off = alternate_scroll {
327 term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
328 }
329
330 let term = Arc::new(FairMutex::new(term));
331
332 //Setup the pty...
333 let pty = match tty::new(&pty_config, initial_size.into(), None) {
334 Ok(pty) => pty,
335 Err(error) => {
336 bail!(TerminalError {
337 directory: working_directory,
338 shell,
339 source: error,
340 });
341 }
342 };
343
344 let shell_txt = {
345 match shell {
346 Some(Shell::System) | None => {
347 let mut buf = [0; 1024];
348 let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
349 pw.shell.to_string()
350 }
351 Some(Shell::Program(program)) => program,
352 Some(Shell::WithArguments { program, args }) => {
353 format!("{} {}", program, args.join(" "))
354 }
355 }
356 };
357
358 //And connect them together
359 let event_loop = EventLoop::new(
360 term.clone(),
361 ZedListener(events_tx.clone()),
362 pty,
363 pty_config.hold,
364 false,
365 );
366
367 //Kick things off
368 let pty_tx = event_loop.channel();
369 let _io_thread = event_loop.spawn();
370
371 let terminal = Terminal {
372 pty_tx: Notifier(pty_tx),
373 term,
374 events: VecDeque::with_capacity(10), //Should never get this high.
375 title: shell_txt.clone(),
376 default_title: shell_txt,
377 last_mode: TermMode::NONE,
378 cur_size: initial_size,
379 last_mouse: None,
380 last_offset: 0,
381 searcher: None,
382 };
383
384 Ok(TerminalBuilder {
385 terminal,
386 events_rx,
387 })
388 }
389
390 pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
391 //Event loop
392 cx.spawn_weak(|this, mut cx| async move {
393 use futures::StreamExt;
394
395 while let Some(event) = self.events_rx.next().await {
396 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
397 //Process the first event immediately for lowered latency
398 this.process_event(&event, cx);
399 });
400
401 'outer: loop {
402 let mut events = vec![];
403 let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
404
405 loop {
406 futures::select_biased! {
407 _ = timer => break,
408 event = self.events_rx.next() => {
409 if let Some(event) = event {
410 events.push(event);
411 if events.len() > 100 {
412 break;
413 }
414 } else {
415 break;
416 }
417 },
418 }
419 }
420
421 if events.is_empty() {
422 smol::future::yield_now().await;
423 break 'outer;
424 } else {
425 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
426 for event in events {
427 this.process_event(&event, cx);
428 }
429 });
430 smol::future::yield_now().await;
431 }
432 }
433 }
434
435 Some(())
436 })
437 .detach();
438
439 self.terminal
440 }
441}
442
443pub struct Terminal {
444 pty_tx: Notifier,
445 term: Arc<FairMutex<Term<ZedListener>>>,
446 events: VecDeque<InternalEvent>,
447 default_title: String,
448 title: String,
449 cur_size: TerminalSize,
450 last_mode: TermMode,
451 last_offset: usize,
452 last_mouse: Option<(Point, Direction)>,
453 searcher: Option<(Option<RegexSearch>, Point)>,
454}
455
456impl Terminal {
457 fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
458 match event {
459 AlacTermEvent::Title(title) => {
460 self.title = title.to_string();
461 cx.emit(Event::TitleChanged);
462 }
463 AlacTermEvent::ResetTitle => {
464 self.title = self.default_title.clone();
465 cx.emit(Event::TitleChanged);
466 }
467 AlacTermEvent::ClipboardStore(_, data) => {
468 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
469 }
470 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
471 &cx.read_from_clipboard()
472 .map(|ci| ci.text().to_string())
473 .unwrap_or_else(|| "".to_string()),
474 )),
475 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
476 AlacTermEvent::TextAreaSizeRequest(format) => {
477 self.write_to_pty(format(self.cur_size.into()))
478 }
479 AlacTermEvent::CursorBlinkingChange => {
480 cx.emit(Event::BlinkChanged);
481 }
482 AlacTermEvent::Bell => {
483 cx.emit(Event::Bell);
484 }
485 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
486 AlacTermEvent::MouseCursorDirty => {
487 //NOOP, Handled in render
488 }
489 AlacTermEvent::Wakeup => {
490 cx.emit(Event::Wakeup);
491 cx.notify();
492 }
493 AlacTermEvent::ColorRequest(idx, fun_ptr) => {
494 self.events
495 .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
496 cx.notify(); //Immediately schedule a render to respond to the color request
497 }
498 }
499 }
500
501 ///Takes events from Alacritty and translates them to behavior on this view
502 fn process_terminal_event(
503 &mut self,
504 event: &InternalEvent,
505 term: &mut Term<ZedListener>,
506 cx: &mut ModelContext<Self>,
507 ) {
508 match event {
509 InternalEvent::ColorRequest(index, format) => {
510 let color = term.colors()[*index].unwrap_or_else(|| {
511 let term_style = &cx.global::<Settings>().theme.terminal;
512 to_alac_rgb(get_color_at_index(index, &term_style.colors))
513 });
514 self.write_to_pty(format(color))
515 }
516 InternalEvent::Resize(new_size) => {
517 self.cur_size = *new_size;
518
519 self.pty_tx.0.send(Msg::Resize((*new_size).into())).ok();
520
521 term.resize(*new_size);
522 }
523 InternalEvent::Clear => {
524 self.write_to_pty("\x0c".to_string());
525 term.clear_screen(ClearMode::Saved);
526 }
527 InternalEvent::Scroll(scroll) => {
528 term.scroll_display(*scroll);
529 }
530 InternalEvent::FocusNextMatch => {
531 if let Some((Some(searcher), origin)) = &self.searcher {
532 match term.search_next(searcher, *origin, SEARCH_FORWARD, Direction::Left, None)
533 {
534 Some(regex_match) => {
535 term.scroll_to_point(*regex_match.start());
536
537 //Focus is done with selections in zed
538 let focus = make_selection(*regex_match.start(), *regex_match.end());
539 term.selection = Some(focus);
540 }
541 None => {
542 //Clear focused match
543 term.selection = None;
544 }
545 }
546 }
547 }
548 InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
549 InternalEvent::UpdateSelection(position) => {
550 if let Some(mut selection) = term.selection.take() {
551 let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
552 let side = mouse_side(*position, self.cur_size);
553
554 selection.update(point, side);
555 term.selection = Some(selection);
556 }
557 }
558
559 InternalEvent::Copy => {
560 if let Some(txt) = term.selection_to_string() {
561 cx.write_to_clipboard(ClipboardItem::new(txt))
562 }
563 }
564 }
565 }
566
567 fn begin_select(&mut self, sel: Selection) {
568 self.events
569 .push_back(InternalEvent::SetSelection(Some(sel)));
570 }
571
572 fn continue_selection(&mut self, location: Vector2F) {
573 self.events
574 .push_back(InternalEvent::UpdateSelection(location))
575 }
576
577 fn end_select(&mut self) {
578 self.events.push_back(InternalEvent::SetSelection(None));
579 }
580
581 fn scroll(&mut self, scroll: AlacScroll) {
582 self.events.push_back(InternalEvent::Scroll(scroll));
583 }
584
585 fn focus_next_match(&mut self) {
586 self.events.push_back(InternalEvent::FocusNextMatch);
587 }
588
589 pub fn search(&mut self, search: &str) {
590 let new_searcher = RegexSearch::new(search).ok();
591 self.searcher = match (new_searcher, &self.searcher) {
592 //Nothing to do :(
593 (None, None) => None,
594 //No existing search, start a new one
595 (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())),
596 //Existing search, carry over origin
597 (new_searcher, Some((_, origin))) => Some((new_searcher, *origin)),
598 };
599
600 if let Some((Some(_), _)) = self.searcher {
601 self.focus_next_match();
602 }
603 }
604
605 fn viewport_origin(&mut self) -> Point {
606 let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1;
607 Point::new(viewport_top, alacritty_terminal::index::Column(0))
608 }
609
610 pub fn end_search(&mut self) {
611 self.searcher = None;
612 }
613
614 pub fn copy(&mut self) {
615 self.events.push_back(InternalEvent::Copy);
616 }
617
618 pub fn clear(&mut self) {
619 self.events.push_back(InternalEvent::Clear)
620 }
621
622 ///Resize the terminal and the PTY.
623 pub fn set_size(&mut self, new_size: TerminalSize) {
624 self.events.push_back(InternalEvent::Resize(new_size))
625 }
626
627 ///Write the Input payload to the tty.
628 fn write_to_pty(&self, input: String) {
629 self.pty_tx.notify(input.into_bytes());
630 }
631
632 pub fn input(&mut self, input: String) {
633 self.scroll(AlacScroll::Bottom);
634 self.end_select();
635 self.write_to_pty(input);
636 }
637
638 pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
639 let esc = to_esc_str(keystroke, &self.last_mode);
640 if let Some(esc) = esc {
641 self.input(esc);
642 true
643 } else {
644 false
645 }
646 }
647
648 ///Paste text into the terminal
649 pub fn paste(&mut self, text: &str) {
650 let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
651 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
652 } else {
653 text.replace("\r\n", "\r").replace('\n', "\r")
654 };
655 self.input(paste_text)
656 }
657
658 pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
659 where
660 F: FnOnce(RenderableContent, char, Option<RegexSearch>) -> T,
661 {
662 let m = self.term.clone(); //Arc clone
663 let mut term = m.lock();
664
665 //Note that this ordering matters for
666 while let Some(e) = self.events.pop_front() {
667 self.process_terminal_event(&e, &mut term, cx)
668 }
669
670 self.last_mode = *term.mode();
671
672 let content = term.renderable_content();
673
674 self.last_offset = content.display_offset;
675
676 let cursor_text = term.grid()[content.cursor.point].c;
677
678 f(
679 content,
680 cursor_text,
681 self.searcher.as_ref().and_then(|s| s.0.clone()),
682 )
683 }
684
685 pub fn focus_in(&self) {
686 if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
687 self.write_to_pty("\x1b[I".to_string());
688 }
689 }
690
691 pub fn focus_out(&self) {
692 if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
693 self.write_to_pty("\x1b[O".to_string());
694 }
695 }
696
697 pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
698 match self.last_mouse {
699 Some((old_point, old_side)) => {
700 if old_point == point && old_side == side {
701 false
702 } else {
703 self.last_mouse = Some((point, side));
704 true
705 }
706 }
707 None => {
708 self.last_mouse = Some((point, side));
709 true
710 }
711 }
712 }
713
714 pub fn mouse_mode(&self, shift: bool) -> bool {
715 self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
716 }
717
718 pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
719 let position = e.position.sub(origin);
720
721 let point = mouse_point(position, self.cur_size, self.last_offset);
722 let side = mouse_side(position, self.cur_size);
723
724 if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
725 if let Some(bytes) = mouse_moved_report(point, e, self.last_mode) {
726 self.pty_tx.notify(bytes);
727 }
728 }
729 }
730
731 pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
732 let position = e.position.sub(origin);
733
734 if !self.mouse_mode(e.shift) {
735 // Alacritty has the same ordering, of first updating the selection
736 // then scrolling 15ms later
737 self.continue_selection(position);
738
739 // Doesn't make sense to scroll the alt screen
740 if !self.last_mode.contains(TermMode::ALT_SCREEN) {
741 let scroll_delta = match self.drag_line_delta(e) {
742 Some(value) => value,
743 None => return,
744 };
745
746 let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
747 self.scroll(AlacScroll::Delta(scroll_lines));
748 self.continue_selection(position)
749 }
750 }
751 }
752
753 fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
754 //TODO: Why do these need to be doubled? Probably the same problem that the IME has
755 let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
756 let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
757 let scroll_delta = if e.position.y() < top {
758 (top - e.position.y()).powf(1.1)
759 } else if e.position.y() > bottom {
760 -((e.position.y() - bottom).powf(1.1))
761 } else {
762 return None; //Nothing to do
763 };
764 Some(scroll_delta)
765 }
766
767 pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
768 let position = e.position.sub(origin);
769 let point = mouse_point(position, self.cur_size, self.last_offset);
770 let side = mouse_side(position, self.cur_size);
771
772 if self.mouse_mode(e.shift) {
773 if let Some(bytes) = mouse_button_report(point, e, true, self.last_mode) {
774 self.pty_tx.notify(bytes);
775 }
776 } else if e.button == MouseButton::Left {
777 self.begin_select(Selection::new(SelectionType::Simple, point, side));
778 }
779 }
780
781 pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
782 let position = e.position.sub(origin);
783
784 if !self.mouse_mode(e.shift) {
785 let point = mouse_point(position, self.cur_size, self.last_offset);
786 let side = mouse_side(position, self.cur_size);
787
788 let selection_type = match e.click_count {
789 0 => return, //This is a release
790 1 => Some(SelectionType::Simple),
791 2 => Some(SelectionType::Semantic),
792 3 => Some(SelectionType::Lines),
793 _ => None,
794 };
795
796 let selection =
797 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
798
799 if let Some(sel) = selection {
800 self.begin_select(sel);
801 }
802 }
803 }
804
805 pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
806 let position = e.position.sub(origin);
807 if self.mouse_mode(e.shift) {
808 let point = mouse_point(position, self.cur_size, self.last_offset);
809
810 if let Some(bytes) = mouse_button_report(point, e, false, self.last_mode) {
811 self.pty_tx.notify(bytes);
812 }
813 } else if e.button == MouseButton::Left {
814 // Seems pretty standard to automatically copy on mouse_up for terminals,
815 // so let's do that here
816 self.copy();
817 }
818 self.last_mouse = None;
819 }
820
821 ///Scroll the terminal
822 pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
823 if self.mouse_mode(e.shift) {
824 //TODO: Currently this only sends the current scroll reports as they come in. Alacritty
825 //Sends the *entire* scroll delta on *every* scroll event, only resetting it when
826 //The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
827 //This would be consistent with a scroll model based on 'distance from origin'...
828 let scroll_lines = (e.delta.y() / self.cur_size.line_height) as i32;
829 let point = mouse_point(e.position.sub(origin), self.cur_size, self.last_offset);
830
831 if let Some(scrolls) = scroll_report(point, scroll_lines as i32, e, self.last_mode) {
832 for scroll in scrolls {
833 self.pty_tx.notify(scroll);
834 }
835 };
836 } else if self
837 .last_mode
838 .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
839 && !e.shift
840 {
841 //TODO: See above TODO, also applies here.
842 let scroll_lines =
843 ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
844
845 self.pty_tx.notify(alt_scroll(scroll_lines))
846 } else {
847 let scroll_lines =
848 ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
849 if scroll_lines != 0 {
850 let scroll = AlacScroll::Delta(scroll_lines);
851 self.scroll(scroll);
852 }
853 }
854 }
855}
856
857fn make_selection(from: Point, to: Point) -> Selection {
858 let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left);
859 focus.update(to, Direction::Right);
860 focus
861}
862
863impl Drop for Terminal {
864 fn drop(&mut self) {
865 self.pty_tx.0.send(Msg::Shutdown).ok();
866 }
867}
868
869impl Entity for Terminal {
870 type Event = Event;
871}
872
873#[cfg(test)]
874mod tests {
875 pub mod terminal_test_context;
876}
877
878//TODO Move this around and clean up the code
879mod alacritty_unix {
880 use alacritty_terminal::config::Program;
881 use gpui::anyhow::{bail, Result};
882
883 use std::ffi::CStr;
884 use std::mem::MaybeUninit;
885 use std::ptr;
886
887 #[derive(Debug)]
888 pub struct Passwd<'a> {
889 _name: &'a str,
890 _dir: &'a str,
891 pub shell: &'a str,
892 }
893
894 /// Return a Passwd struct with pointers into the provided buf.
895 ///
896 /// # Unsafety
897 ///
898 /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
899 pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
900 // Create zeroed passwd struct.
901 let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
902
903 let mut res: *mut libc::passwd = ptr::null_mut();
904
905 // Try and read the pw file.
906 let uid = unsafe { libc::getuid() };
907 let status = unsafe {
908 libc::getpwuid_r(
909 uid,
910 entry.as_mut_ptr(),
911 buf.as_mut_ptr() as *mut _,
912 buf.len(),
913 &mut res,
914 )
915 };
916 let entry = unsafe { entry.assume_init() };
917
918 if status < 0 {
919 bail!("getpwuid_r failed");
920 }
921
922 if res.is_null() {
923 bail!("pw not found");
924 }
925
926 // Sanity check.
927 assert_eq!(entry.pw_uid, uid);
928
929 // Build a borrowed Passwd struct.
930 Ok(Passwd {
931 _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
932 _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
933 shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
934 })
935 }
936
937 #[cfg(target_os = "macos")]
938 pub fn _default_shell(pw: &Passwd<'_>) -> Program {
939 let shell_name = pw.shell.rsplit('/').next().unwrap();
940 let argv = vec![
941 String::from("-c"),
942 format!("exec -a -{} {}", shell_name, pw.shell),
943 ];
944
945 Program::WithArgs {
946 program: "/bin/bash".to_owned(),
947 args: argv,
948 }
949 }
950
951 #[cfg(not(target_os = "macos"))]
952 pub fn default_shell(pw: &Passwd<'_>) -> Program {
953 Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
954 }
955}