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