terminal.rs

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