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