terminal.rs

  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}