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