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