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::{Direction, Point},
14 selection::{Selection, SelectionType},
15 sync::FairMutex,
16 term::{search::RegexSearch, RenderableContent, TermMode},
17 tty::{self, setup_env},
18 Term,
19};
20use anyhow::{bail, Result};
21use futures::{
22 channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
23 FutureExt,
24};
25
26use mappings::mouse::{
27 alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
28};
29use modal::deploy_modal;
30use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
31use std::{
32 collections::{HashMap, VecDeque},
33 fmt::Display,
34 ops::Sub,
35 path::PathBuf,
36 sync::Arc,
37 time::Duration,
38};
39use thiserror::Error;
40
41use gpui::{
42 geometry::vector::{vec2f, Vector2F},
43 keymap::Keystroke,
44 scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
45 ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
46 ScrollWheelEvent,
47};
48
49use crate::mappings::{
50 colors::{get_color_at_index, to_alac_rgb},
51 keys::to_esc_str,
52};
53
54///Initialize and register all of our action handlers
55pub fn init(cx: &mut MutableAppContext) {
56 cx.add_action(deploy_modal);
57
58 terminal_view::init(cx);
59 terminal_container_view::init(cx);
60}
61
62///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
63///Scroll multiplier that is set to 3 by default. This will be removed when I
64///Implement scroll bars.
65const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
66// const ALACRITTY_SEARCH_LINE_LIMIT: usize = 1000;
67const SEARCH_FORWARD: Direction = Direction::Left;
68
69const DEBUG_TERMINAL_WIDTH: f32 = 500.;
70const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
71const DEBUG_CELL_WIDTH: f32 = 5.;
72const DEBUG_LINE_HEIGHT: f32 = 5.;
73
74///Upward flowing events, for changing the title and such
75#[derive(Clone, Copy, Debug)]
76pub enum Event {
77 TitleChanged,
78 CloseTerminal,
79 Bell,
80 Wakeup,
81 BlinkChanged,
82}
83
84#[derive(Clone, Debug)]
85enum Scroll {
86 AlacScroll(AlacScroll),
87 ToNextSearch,
88}
89
90#[derive(Clone, Debug)]
91enum InternalEvent {
92 TermEvent(AlacTermEvent),
93 Resize(TerminalSize),
94 Clear,
95 Scroll(Scroll),
96 SetSelection(Option<Selection>),
97 UpdateSelection(Vector2F),
98 Copy,
99}
100
101///A translation struct for Alacritty to communicate with us from their event loop
102#[derive(Clone)]
103pub struct ZedListener(UnboundedSender<AlacTermEvent>);
104
105impl EventListener for ZedListener {
106 fn send_event(&self, event: AlacTermEvent) {
107 self.0.unbounded_send(event).ok();
108 }
109}
110
111#[derive(Clone, Copy, Debug)]
112pub struct TerminalSize {
113 cell_width: f32,
114 line_height: f32,
115 height: f32,
116 width: f32,
117}
118
119impl TerminalSize {
120 pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
121 TerminalSize {
122 cell_width,
123 line_height,
124 width: size.x(),
125 height: size.y(),
126 }
127 }
128
129 pub fn num_lines(&self) -> usize {
130 (self.height / self.line_height).floor() as usize
131 }
132
133 pub fn num_columns(&self) -> usize {
134 (self.width / self.cell_width).floor() as usize
135 }
136
137 pub fn height(&self) -> f32 {
138 self.height
139 }
140
141 pub fn width(&self) -> f32 {
142 self.width
143 }
144
145 pub fn cell_width(&self) -> f32 {
146 self.cell_width
147 }
148
149 pub fn line_height(&self) -> f32 {
150 self.line_height
151 }
152}
153impl Default for TerminalSize {
154 fn default() -> Self {
155 TerminalSize::new(
156 DEBUG_LINE_HEIGHT,
157 DEBUG_CELL_WIDTH,
158 vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
159 )
160 }
161}
162
163impl From<TerminalSize> for WindowSize {
164 fn from(val: TerminalSize) -> Self {
165 WindowSize {
166 num_lines: val.num_lines() as u16,
167 num_cols: val.num_columns() as u16,
168 cell_width: val.cell_width() as u16,
169 cell_height: val.line_height() as u16,
170 }
171 }
172}
173
174impl Dimensions for TerminalSize {
175 fn total_lines(&self) -> usize {
176 self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
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 has_selection: false,
382 searcher: None,
383 };
384
385 Ok(TerminalBuilder {
386 terminal,
387 events_rx,
388 })
389 }
390
391 pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
392 //Event loop
393 cx.spawn_weak(|this, mut cx| async move {
394 use futures::StreamExt;
395
396 while let Some(event) = self.events_rx.next().await {
397 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
398 //Process the first event immediately for lowered latency
399 this.process_event(&event, cx);
400 });
401
402 'outer: loop {
403 let mut events = vec![];
404 let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
405
406 loop {
407 futures::select_biased! {
408 _ = timer => break,
409 event = self.events_rx.next() => {
410 if let Some(event) = event {
411 events.push(event);
412 if events.len() > 100 {
413 break;
414 }
415 } else {
416 break;
417 }
418 },
419 }
420 }
421
422 if events.is_empty() {
423 smol::future::yield_now().await;
424 break 'outer;
425 } else {
426 this.upgrade(&cx)?.update(&mut cx, |this, cx| {
427 for event in events {
428 this.process_event(&event, cx);
429 }
430 });
431 smol::future::yield_now().await;
432 }
433 }
434 }
435
436 Some(())
437 })
438 .detach();
439
440 self.terminal
441 }
442}
443
444pub struct Terminal {
445 pty_tx: Notifier,
446 term: Arc<FairMutex<Term<ZedListener>>>,
447 events: VecDeque<InternalEvent>,
448 default_title: String,
449 title: String,
450 cur_size: TerminalSize,
451 last_mode: TermMode,
452 last_offset: usize,
453 last_mouse: Option<(Point, Direction)>,
454 has_selection: bool,
455 searcher: Option<(Option<RegexSearch>, Point)>,
456}
457
458impl Terminal {
459 fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
460 match event {
461 AlacTermEvent::Title(title) => {
462 self.title = title.to_string();
463 cx.emit(Event::TitleChanged);
464 }
465 AlacTermEvent::ResetTitle => {
466 self.title = self.default_title.clone();
467 cx.emit(Event::TitleChanged);
468 }
469 AlacTermEvent::ClipboardStore(_, data) => {
470 cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
471 }
472 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
473 &cx.read_from_clipboard()
474 .map(|ci| ci.text().to_string())
475 .unwrap_or_else(|| "".to_string()),
476 )),
477 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
478 AlacTermEvent::TextAreaSizeRequest(format) => {
479 self.write_to_pty(format(self.cur_size.into()))
480 }
481 AlacTermEvent::CursorBlinkingChange => {
482 cx.emit(Event::BlinkChanged);
483 }
484 AlacTermEvent::Bell => {
485 cx.emit(Event::Bell);
486 }
487 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
488 AlacTermEvent::MouseCursorDirty => {
489 //NOOP, Handled in render
490 }
491 AlacTermEvent::Wakeup => {
492 cx.emit(Event::Wakeup);
493 cx.notify();
494 }
495 AlacTermEvent::ColorRequest(_, _) => self
496 .events
497 .push_back(InternalEvent::TermEvent(event.clone())),
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::TermEvent(term_event) => {
510 if let AlacTermEvent::ColorRequest(index, format) = term_event {
511 let color = term.colors()[*index].unwrap_or_else(|| {
512 let term_style = &cx.global::<Settings>().theme.terminal;
513 to_alac_rgb(get_color_at_index(index, &term_style.colors))
514 });
515 self.write_to_pty(format(color))
516 }
517 }
518 InternalEvent::Resize(new_size) => {
519 self.cur_size = *new_size;
520
521 self.pty_tx.0.send(Msg::Resize((*new_size).into())).ok();
522
523 term.resize(*new_size);
524 }
525 InternalEvent::Clear => {
526 self.write_to_pty("\x0c".to_string());
527 term.clear_screen(ClearMode::Saved);
528 }
529 InternalEvent::Scroll(Scroll::AlacScroll(scroll)) => {
530 term.scroll_display(*scroll);
531 }
532 InternalEvent::Scroll(Scroll::ToNextSearch) => {
533 if let Some((Some(searcher), origin)) = &self.searcher {
534 match term.search_next(searcher, *origin, SEARCH_FORWARD, Direction::Left, None)
535 {
536 Some(regex_match) => {
537 //Jump to spot
538 }
539 None => { /*reset state*/ }
540 }
541 }
542 }
543 InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
544 InternalEvent::UpdateSelection(position) => {
545 if let Some(mut selection) = term.selection.take() {
546 let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
547 let side = mouse_side(*position, self.cur_size);
548
549 selection.update(point, side);
550 term.selection = Some(selection);
551 }
552 }
553
554 InternalEvent::Copy => {
555 if let Some(txt) = term.selection_to_string() {
556 cx.write_to_clipboard(ClipboardItem::new(txt))
557 }
558 }
559 }
560 }
561
562 fn begin_select(&mut self, sel: Selection) {
563 self.has_selection = true;
564 self.events
565 .push_back(InternalEvent::SetSelection(Some(sel)));
566 }
567
568 fn continue_selection(&mut self, location: Vector2F) {
569 self.events
570 .push_back(InternalEvent::UpdateSelection(location))
571 }
572
573 fn end_select(&mut self) {
574 self.has_selection = false;
575 self.events.push_back(InternalEvent::SetSelection(None));
576 }
577
578 fn scroll(&mut self, scroll: AlacScroll) {
579 self.events
580 .push_back(InternalEvent::Scroll(Scroll::AlacScroll(scroll)));
581 }
582
583 fn scroll_to_next_search(&mut self) {
584 self.events
585 .push_back(InternalEvent::Scroll(Scroll::ToNextSearch));
586 }
587
588 pub fn search(&mut self, search: &str) {
589 let new_searcher = RegexSearch::new(search).ok();
590 self.searcher = match (new_searcher, &self.searcher) {
591 //Existing search, carry over origin
592 (Some(new_searcher), Some((_, origin))) => Some((Some(new_searcher), *origin)),
593 //No existing search, start a new one
594 (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())),
595 //Error creating a new search, carry over origin
596 (None, Some((_, origin))) => Some((None, *origin)),
597 //Nothing to do :(
598 (None, None) => None,
599 };
600
601 if let Some((Some(_), _)) = self.searcher {
602 self.scroll_to_next_search();
603 }
604 }
605
606 fn viewport_origin(&mut self) -> Point {
607 let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1;
608 Point::new(viewport_top, alacritty_terminal::index::Column(0))
609 }
610
611 pub fn end_search(&mut self) {
612 self.searcher = None;
613 }
614
615 pub fn copy(&mut self) {
616 self.events.push_back(InternalEvent::Copy);
617 }
618
619 pub fn clear(&mut self) {
620 self.events.push_back(InternalEvent::Clear)
621 }
622
623 ///Resize the terminal and the PTY.
624 pub fn set_size(&mut self, new_size: TerminalSize) {
625 self.events.push_back(InternalEvent::Resize(new_size))
626 }
627
628 ///Write the Input payload to the tty.
629 fn write_to_pty(&self, input: String) {
630 self.pty_tx.notify(input.into_bytes());
631 }
632
633 pub fn input(&mut self, input: String) {
634 self.scroll(AlacScroll::Bottom);
635 self.end_select();
636 self.write_to_pty(input);
637 }
638
639 pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
640 let esc = to_esc_str(keystroke, &self.last_mode);
641 if let Some(esc) = esc {
642 self.input(esc);
643 true
644 } else {
645 false
646 }
647 }
648
649 ///Paste text into the terminal
650 pub fn paste(&mut self, text: &str) {
651 let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
652 format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
653 } else {
654 text.replace("\r\n", "\r").replace('\n', "\r")
655 };
656 self.input(paste_text)
657 }
658
659 pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
660 where
661 F: FnOnce(RenderableContent, char) -> T,
662 {
663 let m = self.term.clone(); //Arc clone
664 let mut term = m.lock();
665
666 //Note that this ordering matters for
667 while let Some(e) = self.events.pop_front() {
668 self.process_terminal_event(&e, &mut term, cx)
669 }
670
671 self.last_mode = *term.mode();
672
673 let content = term.renderable_content();
674
675 // term.line_search_right(point)
676 // term.search_next(dfas, origin, direction, side, max_lines)
677
678 self.last_offset = content.display_offset;
679
680 let cursor_text = term.grid()[content.cursor.point].c;
681
682 f(content, cursor_text)
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
857impl Drop for Terminal {
858 fn drop(&mut self) {
859 self.pty_tx.0.send(Msg::Shutdown).ok();
860 }
861}
862
863impl Entity for Terminal {
864 type Event = Event;
865}
866
867#[cfg(test)]
868mod tests {
869 pub mod terminal_test_context;
870}
871
872//TODO Move this around and clean up the code
873mod alacritty_unix {
874 use alacritty_terminal::config::Program;
875 use gpui::anyhow::{bail, Result};
876
877 use std::ffi::CStr;
878 use std::mem::MaybeUninit;
879 use std::ptr;
880
881 #[derive(Debug)]
882 pub struct Passwd<'a> {
883 _name: &'a str,
884 _dir: &'a str,
885 pub shell: &'a str,
886 }
887
888 /// Return a Passwd struct with pointers into the provided buf.
889 ///
890 /// # Unsafety
891 ///
892 /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
893 pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
894 // Create zeroed passwd struct.
895 let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
896
897 let mut res: *mut libc::passwd = ptr::null_mut();
898
899 // Try and read the pw file.
900 let uid = unsafe { libc::getuid() };
901 let status = unsafe {
902 libc::getpwuid_r(
903 uid,
904 entry.as_mut_ptr(),
905 buf.as_mut_ptr() as *mut _,
906 buf.len(),
907 &mut res,
908 )
909 };
910 let entry = unsafe { entry.assume_init() };
911
912 if status < 0 {
913 bail!("getpwuid_r failed");
914 }
915
916 if res.is_null() {
917 bail!("pw not found");
918 }
919
920 // Sanity check.
921 assert_eq!(entry.pw_uid, uid);
922
923 // Build a borrowed Passwd struct.
924 Ok(Passwd {
925 _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
926 _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
927 shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
928 })
929 }
930
931 #[cfg(target_os = "macos")]
932 pub fn _default_shell(pw: &Passwd<'_>) -> Program {
933 let shell_name = pw.shell.rsplit('/').next().unwrap();
934 let argv = vec![
935 String::from("-c"),
936 format!("exec -a -{} {}", shell_name, pw.shell),
937 ];
938
939 Program::WithArgs {
940 program: "/bin/bash".to_owned(),
941 args: argv,
942 }
943 }
944
945 #[cfg(not(target_os = "macos"))]
946 pub fn default_shell(pw: &Passwd<'_>) -> Program {
947 Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
948 }
949}