terminal.rs

  1pub mod connected_el;
  2pub mod connected_view;
  3pub mod mappings;
  4pub mod modal;
  5pub mod terminal_view;
  6
  7#[cfg(test)]
  8use alacritty_terminal::term::cell::Cell;
  9#[cfg(test)]
 10use alacritty_terminal::Grid;
 11
 12use alacritty_terminal::{
 13    ansi::{ClearMode, Handler},
 14    config::{Config, Program, PtyConfig},
 15    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
 16    event_loop::{EventLoop, Msg, Notifier},
 17    grid::{Dimensions, Scroll},
 18    index::{Direction, Point},
 19    selection::{Selection, SelectionType},
 20    sync::FairMutex,
 21    term::{test::TermSize, RenderableContent, TermMode},
 22    tty::{self, setup_env},
 23    Term,
 24};
 25use anyhow::{bail, Result};
 26use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 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, CursorStyle, 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 DEFAULT_TITLE: &str = "Terminal";
 53
 54const DEBUG_TERMINAL_WIDTH: f32 = 100.;
 55const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space.
 56const DEBUG_CELL_WIDTH: f32 = 5.;
 57const DEBUG_LINE_HEIGHT: f32 = 5.;
 58
 59///Upward flowing events, for changing the title and such
 60#[derive(Copy, Clone, Debug)]
 61pub enum Event {
 62    TitleChanged,
 63    CloseTerminal,
 64    Activate,
 65    Wakeup,
 66    Bell,
 67    KeyInput,
 68}
 69
 70///A translation struct for Alacritty to communicate with us from their event loop
 71#[derive(Clone)]
 72pub struct ZedListener(UnboundedSender<AlacTermEvent>);
 73
 74impl EventListener for ZedListener {
 75    fn send_event(&self, event: AlacTermEvent) {
 76        self.0.unbounded_send(event).ok();
 77    }
 78}
 79
 80#[derive(Clone, Copy, Debug)]
 81pub struct TermDimensions {
 82    cell_width: f32,
 83    line_height: f32,
 84    height: f32,
 85    width: f32,
 86}
 87
 88impl TermDimensions {
 89    pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
 90        TermDimensions {
 91            cell_width,
 92            line_height,
 93            width: size.x(),
 94            height: size.y(),
 95        }
 96    }
 97
 98    pub fn num_lines(&self) -> usize {
 99        (self.height / self.line_height).floor() as usize
100    }
101
102    pub fn num_columns(&self) -> usize {
103        (self.width / self.cell_width).floor() as usize
104    }
105
106    pub fn height(&self) -> f32 {
107        self.height
108    }
109
110    pub fn width(&self) -> f32 {
111        self.width
112    }
113
114    pub fn cell_width(&self) -> f32 {
115        self.cell_width
116    }
117
118    pub fn line_height(&self) -> f32 {
119        self.line_height
120    }
121}
122impl Default for TermDimensions {
123    fn default() -> Self {
124        TermDimensions::new(
125            DEBUG_LINE_HEIGHT,
126            DEBUG_CELL_WIDTH,
127            vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
128        )
129    }
130}
131
132impl Into<WindowSize> for TermDimensions {
133    fn into(self) -> WindowSize {
134        WindowSize {
135            num_lines: self.num_lines() as u16,
136            num_cols: self.num_columns() as u16,
137            cell_width: self.cell_width() as u16,
138            cell_height: self.line_height() as u16,
139        }
140    }
141}
142
143impl Dimensions for TermDimensions {
144    fn total_lines(&self) -> usize {
145        self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
146    }
147
148    fn screen_lines(&self) -> usize {
149        self.num_lines()
150    }
151
152    fn columns(&self) -> usize {
153        self.num_columns()
154    }
155}
156
157#[derive(Error, Debug)]
158pub struct TerminalError {
159    pub directory: Option<PathBuf>,
160    pub shell: Option<Shell>,
161    pub source: std::io::Error,
162}
163
164impl TerminalError {
165    pub fn fmt_directory(&self) -> String {
166        self.directory
167            .clone()
168            .map(|path| {
169                match path
170                    .into_os_string()
171                    .into_string()
172                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
173                {
174                    Ok(s) => s,
175                    Err(s) => s,
176                }
177            })
178            .unwrap_or_else(|| {
179                let default_dir =
180                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
181                match default_dir {
182                    Some(dir) => format!("<none specified, using home directory> {}", dir),
183                    None => "<none specified, could not find home directory>".to_string(),
184                }
185            })
186    }
187
188    pub fn shell_to_string(&self) -> Option<String> {
189        self.shell.as_ref().map(|shell| match shell {
190            Shell::System => "<system shell>".to_string(),
191            Shell::Program(p) => p.to_string(),
192            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
193        })
194    }
195
196    pub fn fmt_shell(&self) -> String {
197        self.shell
198            .clone()
199            .map(|shell| match shell {
200                Shell::System => {
201                    let mut buf = [0; 1024];
202                    let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
203
204                    match pw {
205                        Some(pw) => format!("<system defined shell> {}", pw.shell),
206                        None => "<could not access the password file>".to_string(),
207                    }
208                }
209                Shell::Program(s) => s,
210                Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
211            })
212            .unwrap_or_else(|| {
213                let mut buf = [0; 1024];
214                let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
215                match pw {
216                    Some(pw) => {
217                        format!("<none specified, using system defined shell> {}", pw.shell)
218                    }
219                    None => "<none specified, could not access the password file> {}".to_string(),
220                }
221            })
222    }
223}
224
225impl Display for TerminalError {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        let dir_string: String = self.fmt_directory();
228        let shell = self.fmt_shell();
229
230        write!(
231            f,
232            "Working directory: {} Shell command: `{}`, IOError: {}",
233            dir_string, shell, self.source
234        )
235    }
236}
237
238pub struct TerminalBuilder {
239    terminal: Terminal,
240    events_rx: UnboundedReceiver<AlacTermEvent>,
241}
242
243impl TerminalBuilder {
244    pub fn new(
245        working_directory: Option<PathBuf>,
246        shell: Option<Shell>,
247        env: Option<HashMap<String, String>>,
248        initial_size: TermDimensions,
249    ) -> Result<TerminalBuilder> {
250        let pty_config = {
251            let alac_shell = shell.clone().and_then(|shell| match shell {
252                Shell::System => None,
253                Shell::Program(program) => Some(Program::Just(program)),
254                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
255            });
256
257            PtyConfig {
258                shell: alac_shell,
259                working_directory: working_directory.clone(),
260                hold: false,
261            }
262        };
263
264        let mut env = env.unwrap_or_else(|| HashMap::new());
265
266        //TODO: Properly set the current locale,
267        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
268
269        let config = Config {
270            pty_config: pty_config.clone(),
271            env,
272            ..Default::default()
273        };
274
275        setup_env(&config);
276
277        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
278        let (events_tx, events_rx) = unbounded();
279        //Set up the terminal...
280        let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
281        let term = Arc::new(FairMutex::new(term));
282
283        //Setup the pty...
284        let pty = match tty::new(&pty_config, initial_size.into(), None) {
285            Ok(pty) => pty,
286            Err(error) => {
287                bail!(TerminalError {
288                    directory: working_directory,
289                    shell,
290                    source: error,
291                });
292            }
293        };
294
295        let shell_txt = {
296            match shell {
297                Some(Shell::System) | None => {
298                    let mut buf = [0; 1024];
299                    let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
300                    pw.shell.to_string()
301                }
302                Some(Shell::Program(program)) => program,
303                Some(Shell::WithArguments { program, args }) => {
304                    format!("{} {}", program, args.join(" "))
305                }
306            }
307        };
308
309        //And connect them together
310        let event_loop = EventLoop::new(
311            term.clone(),
312            ZedListener(events_tx.clone()),
313            pty,
314            pty_config.hold,
315            false,
316        );
317
318        //Kick things off
319        let pty_tx = event_loop.channel();
320        let _io_thread = event_loop.spawn();
321
322        let terminal = Terminal {
323            pty_tx: Notifier(pty_tx),
324            term,
325            title: shell_txt.to_string(),
326            event_stack: vec![],
327        };
328
329        Ok(TerminalBuilder {
330            terminal,
331            events_rx,
332        })
333    }
334
335    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
336        cx.spawn_weak(|this, mut cx| async move {
337            'outer: loop {
338                //TODO: Pending GPUI updates, sync this to some higher, smarter system.
339                let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 60.));
340
341                let mut events = vec![];
342
343                loop {
344                    match self.events_rx.try_next() {
345                        //Have a buffered event
346                        Ok(Some(e)) => events.push(e),
347                        //Channel closed, exit
348                        Ok(None) => break 'outer,
349                        //Ran out of buffered events
350                        Err(_) => break,
351                    }
352                }
353
354                match this.upgrade(&cx) {
355                    Some(this) => {
356                        this.update(&mut cx, |this, cx| {
357                            this.push_events(events);
358                            cx.notify();
359                        });
360                    }
361                    None => break 'outer,
362                }
363
364                delay.await;
365            }
366        })
367        .detach();
368
369        self.terminal
370    }
371}
372
373pub struct Terminal {
374    pty_tx: Notifier,
375    term: Arc<FairMutex<Term<ZedListener>>>,
376    pub title: String,
377    event_stack: Vec<AlacTermEvent>,
378}
379
380impl Terminal {
381    fn push_events(&mut self, events: Vec<AlacTermEvent>) {
382        self.event_stack.extend(events)
383    }
384
385    ///Takes events from Alacritty and translates them to behavior on this view
386    fn process_terminal_event(
387        &mut self,
388        event: alacritty_terminal::event::Event,
389        term: &mut Term<ZedListener>,
390        cx: &mut ModelContext<Self>,
391    ) {
392        match event {
393            // TODO: Handle is_self_focused in subscription on terminal view
394            AlacTermEvent::Wakeup => {
395                cx.emit(Event::Wakeup);
396            }
397            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
398
399            AlacTermEvent::MouseCursorDirty => {
400                //Calculate new cursor style.
401                //TODO: alacritty/src/input.rs:L922-L939
402                //Check on correctly handling mouse events for terminals
403                cx.platform().set_cursor_style(CursorStyle::Arrow); //???
404            }
405            AlacTermEvent::Title(title) => {
406                self.title = title;
407                cx.emit(Event::TitleChanged);
408            }
409            AlacTermEvent::ResetTitle => {
410                self.title = DEFAULT_TITLE.to_string();
411                cx.emit(Event::TitleChanged);
412            }
413            AlacTermEvent::ClipboardStore(_, data) => {
414                cx.write_to_clipboard(ClipboardItem::new(data))
415            }
416
417            AlacTermEvent::ClipboardLoad(_, format) => self.pty_tx.notify(
418                format(
419                    &cx.read_from_clipboard()
420                        .map(|ci| ci.text().to_string())
421                        .unwrap_or("".to_string()),
422                )
423                .into_bytes(),
424            ),
425            AlacTermEvent::ColorRequest(index, format) => {
426                let color = term.colors()[index].unwrap_or_else(|| {
427                    let term_style = &cx.global::<Settings>().theme.terminal;
428                    to_alac_rgb(get_color_at_index(&index, &term_style.colors))
429                });
430                self.pty_tx.notify(format(color).into_bytes())
431            }
432            AlacTermEvent::CursorBlinkingChange => {
433                //TODO: Set a timer to blink the cursor on and off
434            }
435            AlacTermEvent::Bell => {
436                cx.emit(Event::Bell);
437            }
438            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
439            AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"),
440        }
441    }
442
443    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
444    pub fn write_to_pty(&mut self, input: String) {
445        self.event_stack.push(AlacTermEvent::PtyWrite(input))
446    }
447
448    ///Resize the terminal and the PTY. This locks the terminal.
449    pub fn set_size(&self, new_size: WindowSize) {
450        self.pty_tx.0.send(Msg::Resize(new_size)).ok();
451
452        let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize);
453        self.term.lock().resize(term_size);
454    }
455
456    pub fn clear(&mut self) {
457        self.write_to_pty("\x0c".into());
458        self.term.lock().clear_screen(ClearMode::Saved);
459    }
460
461    pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
462        let guard = self.term.lock();
463        let mode = guard.mode();
464        let esc = to_esc_str(keystroke, mode);
465        drop(guard);
466        if esc.is_some() {
467            self.write_to_pty(esc.unwrap());
468            true
469        } else {
470            false
471        }
472    }
473
474    ///Paste text into the terminal
475    pub fn paste(&mut self, text: &str) {
476        if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
477            self.write_to_pty("\x1b[200~".to_string());
478            self.write_to_pty(text.replace('\x1b', "").to_string());
479            self.write_to_pty("\x1b[201~".to_string());
480        } else {
481            self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
482        }
483    }
484
485    pub fn copy(&self) -> Option<String> {
486        let term = self.term.lock();
487        term.selection_to_string()
488    }
489
490    ///Takes the selection out of the terminal
491    pub fn take_selection(&self) -> Option<Selection> {
492        self.term.lock().selection.take()
493    }
494    ///Sets the selection object on the terminal
495    pub fn set_selection(&self, sel: Option<Selection>) {
496        self.term.lock().selection = sel;
497    }
498
499    pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
500    where
501        F: FnOnce(RenderableContent, char) -> T,
502    {
503        let m = self.term.clone(); //TODO avoid clone?
504        let mut term = m.lock(); //Lock
505
506        //TODO, handle resizes
507        // if let Some(new_size) = new_size {
508        //     self.pty_tx.0.send(Msg::Resize(new_size.into())).ok();
509        // }
510
511        // if let Some(new_size) = new_size {
512        //     term.resize(new_size); //Reflow
513        // }
514
515        for event in self
516            .event_stack
517            .iter()
518            .map(|event| event.clone())
519            .collect::<Vec<AlacTermEvent>>() //TODO avoid copy
520            .drain(..)
521        {
522            self.process_terminal_event(event, &mut term, cx)
523        }
524
525        let content = term.renderable_content();
526        let cursor_text = term.grid()[content.cursor.point].c;
527
528        f(content, cursor_text)
529    }
530
531    pub fn get_display_offset(&self) -> usize {
532        10
533        // self.term.lock().renderable_content().display_offset
534    }
535
536    ///Scroll the terminal
537    pub fn scroll(&self, _scroll: Scroll) {
538        // self.term.lock().scroll_display(scroll)
539    }
540
541    pub fn click(&self, point: Point, side: Direction, clicks: usize) {
542        let selection_type = match clicks {
543            0 => return, //This is a release
544            1 => Some(SelectionType::Simple),
545            2 => Some(SelectionType::Semantic),
546            3 => Some(SelectionType::Lines),
547            _ => None,
548        };
549
550        let selection =
551            selection_type.map(|selection_type| Selection::new(selection_type, point, side));
552
553        self.set_selection(selection);
554    }
555
556    pub fn drag(&self, point: Point, side: Direction) {
557        if let Some(mut selection) = self.take_selection() {
558            selection.update(point, side);
559            self.set_selection(Some(selection));
560        }
561    }
562
563    pub fn mouse_down(&self, point: Point, side: Direction) {
564        self.set_selection(Some(Selection::new(SelectionType::Simple, point, side)));
565    }
566
567    #[cfg(test)]
568    fn grid(&self) -> Grid<Cell> {
569        self.term.lock().grid().clone()
570    }
571}
572
573impl Drop for Terminal {
574    fn drop(&mut self) {
575        self.pty_tx.0.send(Msg::Shutdown).ok();
576    }
577}
578
579impl Entity for Terminal {
580    type Event = Event;
581}
582
583#[cfg(test)]
584mod tests {
585    pub mod terminal_test_context;
586
587    use gpui::TestAppContext;
588
589    use crate::tests::terminal_test_context::TerminalTestContext;
590
591    ///Basic integration test, can we get the terminal to show up, execute a command,
592    //and produce noticable output?
593    #[gpui::test(retries = 5)]
594    async fn test_terminal(cx: &mut TestAppContext) {
595        let mut cx = TerminalTestContext::new(cx, true);
596
597        cx.execute_and_wait("expr 3 + 4", |content, _cx| content.contains("7"))
598            .await;
599    }
600}
601
602//TODO Move this around and clean up the code
603mod alacritty_unix {
604    use alacritty_terminal::config::Program;
605    use gpui::anyhow::{bail, Result};
606    use libc;
607    use std::ffi::CStr;
608    use std::mem::MaybeUninit;
609    use std::ptr;
610
611    #[derive(Debug)]
612    pub struct Passwd<'a> {
613        _name: &'a str,
614        _dir: &'a str,
615        pub shell: &'a str,
616    }
617
618    /// Return a Passwd struct with pointers into the provided buf.
619    ///
620    /// # Unsafety
621    ///
622    /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
623    pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
624        // Create zeroed passwd struct.
625        let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
626
627        let mut res: *mut libc::passwd = ptr::null_mut();
628
629        // Try and read the pw file.
630        let uid = unsafe { libc::getuid() };
631        let status = unsafe {
632            libc::getpwuid_r(
633                uid,
634                entry.as_mut_ptr(),
635                buf.as_mut_ptr() as *mut _,
636                buf.len(),
637                &mut res,
638            )
639        };
640        let entry = unsafe { entry.assume_init() };
641
642        if status < 0 {
643            bail!("getpwuid_r failed");
644        }
645
646        if res.is_null() {
647            bail!("pw not found");
648        }
649
650        // Sanity check.
651        assert_eq!(entry.pw_uid, uid);
652
653        // Build a borrowed Passwd struct.
654        Ok(Passwd {
655            _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
656            _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
657            shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
658        })
659    }
660
661    #[cfg(target_os = "macos")]
662    pub fn _default_shell(pw: &Passwd<'_>) -> Program {
663        let shell_name = pw.shell.rsplit('/').next().unwrap();
664        let argv = vec![
665            String::from("-c"),
666            format!("exec -a -{} {}", shell_name, pw.shell),
667        ];
668
669        Program::WithArgs {
670            program: "/bin/bash".to_owned(),
671            args: argv,
672        }
673    }
674
675    #[cfg(not(target_os = "macos"))]
676    pub fn default_shell(pw: &Passwd<'_>) -> Program {
677        Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
678    }
679}