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