keeping both...

Mikayla Maki created

Change summary

crates/terminal/src/model.rs | 535 ++++++++++++++++++++++++++++++++++++++
1 file changed, 535 insertions(+)

Detailed changes

crates/terminal/src/model.rs 🔗

@@ -0,0 +1,535 @@
+use alacritty_terminal::{
+    ansi::{ClearMode, Handler},
+    config::{Config, Program, PtyConfig},
+    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
+    event_loop::{EventLoop, Msg, Notifier},
+    grid::Scroll,
+    index::{Direction, Point},
+    selection::{Selection, SelectionType},
+    sync::FairMutex,
+    term::{test::TermSize, RenderableContent, TermMode},
+    tty::{self, setup_env},
+    Term,
+};
+use anyhow::{bail, Result};
+use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
+use settings::{Settings, Shell};
+use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration};
+use thiserror::Error;
+
+use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
+
+use crate::{
+    connected_el::TermDimensions,
+    mappings::{
+        colors::{get_color_at_index, to_alac_rgb},
+        keys::to_esc_str,
+    },
+};
+
+const DEFAULT_TITLE: &str = "Terminal";
+
+///Upward flowing events, for changing the title and such
+#[derive(Copy, Clone, Debug)]
+pub enum Event {
+    TitleChanged,
+    CloseTerminal,
+    Activate,
+    Wakeup,
+    Bell,
+    KeyInput,
+}
+
+///A translation struct for Alacritty to communicate with us from their event loop
+#[derive(Clone)]
+pub struct ZedListener(UnboundedSender<AlacTermEvent>);
+
+impl EventListener for ZedListener {
+    fn send_event(&self, event: AlacTermEvent) {
+        self.0.unbounded_send(event).ok();
+    }
+}
+
+#[derive(Error, Debug)]
+pub struct TerminalError {
+    pub directory: Option<PathBuf>,
+    pub shell: Option<Shell>,
+    pub source: std::io::Error,
+}
+
+impl TerminalError {
+    pub fn fmt_directory(&self) -> String {
+        self.directory
+            .clone()
+            .map(|path| {
+                match path
+                    .into_os_string()
+                    .into_string()
+                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
+                {
+                    Ok(s) => s,
+                    Err(s) => s,
+                }
+            })
+            .unwrap_or_else(|| {
+                let default_dir =
+                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
+                match default_dir {
+                    Some(dir) => format!("<none specified, using home directory> {}", dir),
+                    None => "<none specified, could not find home directory>".to_string(),
+                }
+            })
+    }
+
+    pub fn shell_to_string(&self) -> Option<String> {
+        self.shell.as_ref().map(|shell| match shell {
+            Shell::System => "<system shell>".to_string(),
+            Shell::Program(p) => p.to_string(),
+            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+        })
+    }
+
+    pub fn fmt_shell(&self) -> String {
+        self.shell
+            .clone()
+            .map(|shell| match shell {
+                Shell::System => {
+                    let mut buf = [0; 1024];
+                    let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
+
+                    match pw {
+                        Some(pw) => format!("<system defined shell> {}", pw.shell),
+                        None => "<could not access the password file>".to_string(),
+                    }
+                }
+                Shell::Program(s) => s,
+                Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
+            })
+            .unwrap_or_else(|| {
+                let mut buf = [0; 1024];
+                let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
+                match pw {
+                    Some(pw) => {
+                        format!("<none specified, using system defined shell> {}", pw.shell)
+                    }
+                    None => "<none specified, could not access the password file> {}".to_string(),
+                }
+            })
+    }
+}
+
+impl Display for TerminalError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let dir_string: String = self.fmt_directory();
+
+        let shell = self.fmt_shell();
+
+        write!(
+            f,
+            "Working directory: {} Shell command: `{}`, IOError: {}",
+            dir_string, shell, self.source
+        )
+    }
+}
+
+pub struct TerminalBuilder {
+    terminal: Terminal,
+    events_rx: UnboundedReceiver<AlacTermEvent>,
+}
+
+impl TerminalBuilder {
+    pub fn new(
+        working_directory: Option<PathBuf>,
+        shell: Option<Shell>,
+        env: Option<HashMap<String, String>>,
+        initial_size: TermDimensions,
+    ) -> Result<TerminalBuilder> {
+        let pty_config = {
+            let alac_shell = shell.clone().and_then(|shell| match shell {
+                Shell::System => None,
+                Shell::Program(program) => Some(Program::Just(program)),
+                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
+            });
+
+            PtyConfig {
+                shell: alac_shell,
+                working_directory: working_directory.clone(),
+                hold: false,
+            }
+        };
+
+        let mut env = env.unwrap_or_else(|| HashMap::new());
+
+        //TODO: Properly set the current locale,
+        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
+
+        let config = Config {
+            pty_config: pty_config.clone(),
+            env,
+            ..Default::default()
+        };
+
+        setup_env(&config);
+
+        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
+        let (events_tx, events_rx) = unbounded();
+
+        //Set up the terminal...
+        let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
+        let term = Arc::new(FairMutex::new(term));
+
+        //Setup the pty...
+        let pty = match tty::new(&pty_config, initial_size.into(), None) {
+            Ok(pty) => pty,
+            Err(error) => {
+                bail!(TerminalError {
+                    directory: working_directory,
+                    shell,
+                    source: error,
+                });
+            }
+        };
+
+        let shell_txt = {
+            match shell {
+                Some(Shell::System) | None => {
+                    let mut buf = [0; 1024];
+                    let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
+                    pw.shell.to_string()
+                }
+                Some(Shell::Program(program)) => program,
+                Some(Shell::WithArguments { program, args }) => {
+                    format!("{} {}", program, args.join(" "))
+                }
+            }
+        };
+
+        //And connect them together
+        let event_loop = EventLoop::new(
+            term.clone(),
+            ZedListener(events_tx.clone()),
+            pty,
+            pty_config.hold,
+            false,
+        );
+
+        //Kick things off
+        let pty_tx = event_loop.channel();
+        let _io_thread = event_loop.spawn();
+
+        let terminal = Terminal {
+            pty_tx: Notifier(pty_tx),
+            term,
+            title: shell_txt.to_string(),
+        };
+
+        Ok(TerminalBuilder {
+            terminal,
+            events_rx,
+        })
+    }
+
+    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
+        cx.spawn_weak(|this, mut cx| async move {
+            'outer: loop {
+                let delay = cx.background().timer(Duration::from_secs_f32(1.0 / 30.));
+
+                let mut events = vec![];
+
+                loop {
+                    match self.events_rx.try_next() {
+                        //Have a buffered event
+                        Ok(Some(e)) => events.push(e),
+                        //Ran out of buffered events
+                        Ok(None) => break,
+                        //Channel closed, exit
+                        Err(_) => break 'outer,
+                    }
+                }
+
+                match this.upgrade(&cx) {
+                    Some(this) => {
+                        this.update(&mut cx, |this, cx| {
+                            for event in events {
+                                this.process_terminal_event(event, cx);
+                            }
+                        });
+                    }
+                    None => break 'outer,
+                }
+
+                delay.await;
+            }
+        })
+        .detach();
+
+        self.terminal
+    }
+}
+
+pub struct Terminal {
+    pty_tx: Notifier,
+    term: Arc<FairMutex<Term<ZedListener>>>,
+    pub title: String,
+}
+
+impl Terminal {
+    ///Takes events from Alacritty and translates them to behavior on this view
+    fn process_terminal_event(
+        &mut self,
+        event: alacritty_terminal::event::Event,
+        cx: &mut ModelContext<Terminal>,
+    ) {
+        match event {
+            // TODO: Handle is_self_focused in subscription on terminal view
+            AlacTermEvent::Wakeup => {
+                cx.emit(Event::Wakeup);
+            }
+            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
+            AlacTermEvent::MouseCursorDirty => {
+                //Calculate new cursor style.
+                //TODO: alacritty/src/input.rs:L922-L939
+                //Check on correctly handling mouse events for terminals
+                cx.platform().set_cursor_style(CursorStyle::Arrow); //???
+            }
+            AlacTermEvent::Title(title) => {
+                self.title = title;
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::ResetTitle => {
+                self.title = DEFAULT_TITLE.to_string();
+                cx.emit(Event::TitleChanged);
+            }
+            AlacTermEvent::ClipboardStore(_, data) => {
+                cx.write_to_clipboard(ClipboardItem::new(data))
+            }
+            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
+                &cx.read_from_clipboard()
+                    .map(|ci| ci.text().to_string())
+                    .unwrap_or("".to_string()),
+            )),
+            AlacTermEvent::ColorRequest(index, format) => {
+                let color = self.term.lock().colors()[index].unwrap_or_else(|| {
+                    let term_style = &cx.global::<Settings>().theme.terminal;
+                    to_alac_rgb(get_color_at_index(&index, &term_style.colors))
+                });
+                self.write_to_pty(format(color))
+            }
+            AlacTermEvent::CursorBlinkingChange => {
+                //TODO: Set a timer to blink the cursor on and off
+            }
+            AlacTermEvent::Bell => {
+                cx.emit(Event::Bell);
+            }
+            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
+            AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"),
+        }
+    }
+
+    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
+    pub fn write_to_pty(&self, input: String) {
+        self.write_bytes_to_pty(input.into_bytes());
+    }
+
+    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
+    fn write_bytes_to_pty(&self, input: Vec<u8>) {
+        self.term.lock().scroll_display(Scroll::Bottom);
+        self.pty_tx.notify(input);
+    }
+
+    ///Resize the terminal and the PTY. This locks the terminal.
+    pub fn set_size(&self, new_size: WindowSize) {
+        self.pty_tx.0.send(Msg::Resize(new_size)).ok();
+
+        let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize);
+        self.term.lock().resize(term_size);
+    }
+
+    pub fn clear(&self) {
+        self.write_to_pty("\x0c".into());
+        self.term.lock().clear_screen(ClearMode::Saved);
+    }
+
+    pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool {
+        let guard = self.term.lock();
+        let mode = guard.mode();
+        let esc = to_esc_str(keystroke, mode);
+        drop(guard);
+        if esc.is_some() {
+            self.write_to_pty(esc.unwrap());
+            true
+        } else {
+            false
+        }
+    }
+
+    ///Paste text into the terminal
+    pub fn paste(&self, text: &str) {
+        if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
+            self.write_to_pty("\x1b[200~".to_string());
+            self.write_to_pty(text.replace('\x1b', "").to_string());
+            self.write_to_pty("\x1b[201~".to_string());
+        } else {
+            self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
+        }
+    }
+
+    pub fn copy(&self) -> Option<String> {
+        let term = self.term.lock();
+        term.selection_to_string()
+    }
+
+    ///Takes the selection out of the terminal
+    pub fn take_selection(&self) -> Option<Selection> {
+        self.term.lock().selection.take()
+    }
+    ///Sets the selection object on the terminal
+    pub fn set_selection(&self, sel: Option<Selection>) {
+        self.term.lock().selection = sel;
+    }
+
+    pub fn render_lock<F, T>(&self, new_size: Option<TermDimensions>, f: F) -> T
+    where
+        F: FnOnce(RenderableContent, char) -> T,
+    {
+        if let Some(new_size) = new_size {
+            self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size
+                                                                   //TODO: Is this bad for performance?
+        }
+
+        let mut term = self.term.lock(); //Lock
+
+        if let Some(new_size) = new_size {
+            term.resize(new_size); //Reflow
+        }
+
+        let content = term.renderable_content();
+        let cursor_text = term.grid()[content.cursor.point].c;
+
+        f(content, cursor_text)
+    }
+
+    pub fn get_display_offset(&self) -> usize {
+        self.term.lock().renderable_content().display_offset
+    }
+
+    ///Scroll the terminal
+    pub fn scroll(&self, scroll: Scroll) {
+        self.term.lock().scroll_display(scroll)
+    }
+
+    pub fn click(&self, point: Point, side: Direction, clicks: usize) {
+        let selection_type = match clicks {
+            0 => return, //This is a release
+            1 => Some(SelectionType::Simple),
+            2 => Some(SelectionType::Semantic),
+            3 => Some(SelectionType::Lines),
+            _ => None,
+        };
+
+        let selection =
+            selection_type.map(|selection_type| Selection::new(selection_type, point, side));
+
+        self.set_selection(selection);
+    }
+
+    pub fn drag(&self, point: Point, side: Direction) {
+        if let Some(mut selection) = self.take_selection() {
+            selection.update(point, side);
+            self.set_selection(Some(selection));
+        }
+    }
+
+    pub fn mouse_down(&self, point: Point, side: Direction) {
+        self.set_selection(Some(Selection::new(SelectionType::Simple, point, side)));
+    }
+}
+
+impl Drop for Terminal {
+    fn drop(&mut self) {
+        self.pty_tx.0.send(Msg::Shutdown).ok();
+    }
+}
+
+impl Entity for Terminal {
+    type Event = Event;
+}
+
+//TODO Move this around
+mod alacritty_unix {
+    use alacritty_terminal::config::Program;
+    use gpui::anyhow::{bail, Result};
+    use libc;
+    use std::ffi::CStr;
+    use std::mem::MaybeUninit;
+    use std::ptr;
+
+    #[derive(Debug)]
+    pub struct Passwd<'a> {
+        _name: &'a str,
+        _dir: &'a str,
+        pub shell: &'a str,
+    }
+
+    /// Return a Passwd struct with pointers into the provided buf.
+    ///
+    /// # Unsafety
+    ///
+    /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
+    pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
+        // Create zeroed passwd struct.
+        let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
+
+        let mut res: *mut libc::passwd = ptr::null_mut();
+
+        // Try and read the pw file.
+        let uid = unsafe { libc::getuid() };
+        let status = unsafe {
+            libc::getpwuid_r(
+                uid,
+                entry.as_mut_ptr(),
+                buf.as_mut_ptr() as *mut _,
+                buf.len(),
+                &mut res,
+            )
+        };
+        let entry = unsafe { entry.assume_init() };
+
+        if status < 0 {
+            bail!("getpwuid_r failed");
+        }
+
+        if res.is_null() {
+            bail!("pw not found");
+        }
+
+        // Sanity check.
+        assert_eq!(entry.pw_uid, uid);
+
+        // Build a borrowed Passwd struct.
+        Ok(Passwd {
+            _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
+            _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
+            shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
+        })
+    }
+
+    #[cfg(target_os = "macos")]
+    pub fn _default_shell(pw: &Passwd<'_>) -> Program {
+        let shell_name = pw.shell.rsplit('/').next().unwrap();
+        let argv = vec![
+            String::from("-c"),
+            format!("exec -a -{} {}", shell_name, pw.shell),
+        ];
+
+        Program::WithArgs {
+            program: "/bin/bash".to_owned(),
+            args: argv,
+        }
+    }
+
+    #[cfg(not(target_os = "macos"))]
+    pub fn default_shell(pw: &Passwd<'_>) -> Program {
+        Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
+    }
+}