terminal.rs

  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}