model.rs

  1use alacritty_terminal::{
  2    ansi::{ClearMode, Handler},
  3    config::{Config, Program, PtyConfig},
  4    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
  5    event_loop::{EventLoop, Msg, Notifier},
  6    grid::Scroll,
  7    index::{Direction, Point},
  8    selection::{Selection, SelectionType},
  9    sync::FairMutex,
 10    term::{test::TermSize, RenderableContent, TermMode},
 11    tty::{self, setup_env},
 12    Term,
 13};
 14use anyhow::{bail, Result};
 15use futures::{
 16    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
 17    StreamExt,
 18};
 19use settings::{Settings, Shell};
 20use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc};
 21use thiserror::Error;
 22
 23use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
 24
 25use crate::{
 26    connected_el::TermDimensions,
 27    mappings::{
 28        colors::{get_color_at_index, to_alac_rgb},
 29        keys::to_esc_str,
 30    },
 31};
 32
 33const DEFAULT_TITLE: &str = "Terminal";
 34
 35///Upward flowing events, for changing the title and such
 36#[derive(Copy, Clone, Debug)]
 37pub enum Event {
 38    TitleChanged,
 39    CloseTerminal,
 40    Activate,
 41    Wakeup,
 42    Bell,
 43    KeyInput,
 44}
 45
 46///A translation struct for Alacritty to communicate with us from their event loop
 47#[derive(Clone)]
 48pub struct ZedListener(UnboundedSender<AlacTermEvent>);
 49
 50impl EventListener for ZedListener {
 51    fn send_event(&self, event: AlacTermEvent) {
 52        self.0.unbounded_send(event).ok();
 53    }
 54}
 55
 56#[derive(Error, Debug)]
 57pub struct TerminalError {
 58    pub directory: Option<PathBuf>,
 59    pub shell: Option<Shell>,
 60    pub source: std::io::Error,
 61}
 62
 63impl TerminalError {
 64    pub fn fmt_directory(&self) -> String {
 65        self.directory
 66            .clone()
 67            .map(|path| {
 68                match path
 69                    .into_os_string()
 70                    .into_string()
 71                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
 72                {
 73                    Ok(s) => s,
 74                    Err(s) => s,
 75                }
 76            })
 77            .unwrap_or_else(|| {
 78                let default_dir =
 79                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
 80                match default_dir {
 81                    Some(dir) => format!("<none specified, using home directory> {}", dir),
 82                    None => "<none specified, could not find home directory>".to_string(),
 83                }
 84            })
 85    }
 86
 87    pub fn shell_to_string(&self) -> Option<String> {
 88        self.shell.as_ref().map(|shell| match shell {
 89            Shell::System => "<system shell>".to_string(),
 90            Shell::Program(p) => p.to_string(),
 91            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 92        })
 93    }
 94
 95    pub fn fmt_shell(&self) -> String {
 96        self.shell
 97            .clone()
 98            .map(|shell| match shell {
 99                Shell::System => {
100                    let mut buf = [0; 1024];
101                    let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
102
103                    match pw {
104                        Some(pw) => format!("<system defined shell> {}", pw.shell),
105                        None => "<could not access the password file>".to_string(),
106                    }
107                }
108                Shell::Program(s) => s,
109                Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
110            })
111            .unwrap_or_else(|| {
112                let mut buf = [0; 1024];
113                let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
114                match pw {
115                    Some(pw) => {
116                        format!("<none specified, using system defined shell> {}", pw.shell)
117                    }
118                    None => "<none specified, could not access the password file> {}".to_string(),
119                }
120            })
121    }
122}
123
124impl Display for TerminalError {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        let dir_string: String = self.fmt_directory();
127
128        let shell = self.fmt_shell();
129
130        write!(
131            f,
132            "Working directory: {} Shell command: `{}`, IOError: {}",
133            dir_string, shell, self.source
134        )
135    }
136}
137
138pub struct TerminalBuilder {
139    terminal: Terminal,
140    events_rx: UnboundedReceiver<AlacTermEvent>,
141}
142
143impl TerminalBuilder {
144    pub fn new(
145        working_directory: Option<PathBuf>,
146        shell: Option<Shell>,
147        env: Option<HashMap<String, String>>,
148        initial_size: TermDimensions,
149    ) -> Result<TerminalBuilder> {
150        let pty_config = {
151            let alac_shell = shell.clone().and_then(|shell| match shell {
152                Shell::System => None,
153                Shell::Program(program) => Some(Program::Just(program)),
154                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
155            });
156
157            PtyConfig {
158                shell: alac_shell,
159                working_directory: working_directory.clone(),
160                hold: false,
161            }
162        };
163
164        let mut env = env.unwrap_or_else(|| HashMap::new());
165
166        //TODO: Properly set the current locale,
167        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
168
169        let config = Config {
170            pty_config: pty_config.clone(),
171            env,
172            ..Default::default()
173        };
174
175        setup_env(&config);
176
177        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
178        let (events_tx, events_rx) = unbounded();
179
180        //Set up the terminal...
181        let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
182        let term = Arc::new(FairMutex::new(term));
183
184        //Setup the pty...
185        let pty = match tty::new(&pty_config, initial_size.into(), None) {
186            Ok(pty) => pty,
187            Err(error) => {
188                bail!(TerminalError {
189                    directory: working_directory,
190                    shell,
191                    source: error,
192                });
193            }
194        };
195
196        let shell_txt = {
197            match shell {
198                Some(Shell::System) | None => {
199                    let mut buf = [0; 1024];
200                    let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
201                    pw.shell.to_string()
202                }
203                Some(Shell::Program(program)) => program,
204                Some(Shell::WithArguments { program, args }) => {
205                    format!("{} {}", program, args.join(" "))
206                }
207            }
208        };
209
210        //And connect them together
211        let event_loop = EventLoop::new(
212            term.clone(),
213            ZedListener(events_tx.clone()),
214            pty,
215            pty_config.hold,
216            false,
217        );
218
219        //Kick things off
220        let pty_tx = event_loop.channel();
221        let _io_thread = event_loop.spawn();
222
223        let terminal = Terminal {
224            pty_tx: Notifier(pty_tx),
225            term,
226            title: shell_txt.to_string(),
227        };
228
229        Ok(TerminalBuilder {
230            terminal,
231            events_rx,
232        })
233    }
234
235    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
236        cx.spawn_weak(|this, mut cx| async move {
237            //Listen for terminal events
238            while let Some(event) = self.events_rx.next().await {
239                match this.upgrade(&cx) {
240                    Some(this) => {
241                        this.update(&mut cx, |this, cx| {
242                            this.process_terminal_event(event, cx);
243
244                            cx.notify();
245                        });
246                    }
247                    None => break,
248                }
249            }
250        })
251        .detach();
252
253        self.terminal
254    }
255}
256
257pub struct Terminal {
258    pty_tx: Notifier,
259    term: Arc<FairMutex<Term<ZedListener>>>,
260    pub title: String,
261}
262
263impl Terminal {
264    ///Takes events from Alacritty and translates them to behavior on this view
265    fn process_terminal_event(
266        &mut self,
267        event: alacritty_terminal::event::Event,
268        cx: &mut ModelContext<Terminal>,
269    ) {
270        match event {
271            // TODO: Handle is_self_focused in subscription on terminal view
272            AlacTermEvent::Wakeup => {
273                cx.emit(Event::Wakeup);
274            }
275            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
276            AlacTermEvent::MouseCursorDirty => {
277                //Calculate new cursor style.
278                //TODO: alacritty/src/input.rs:L922-L939
279                //Check on correctly handling mouse events for terminals
280                cx.platform().set_cursor_style(CursorStyle::Arrow); //???
281            }
282            AlacTermEvent::Title(title) => {
283                self.title = title;
284                cx.emit(Event::TitleChanged);
285            }
286            AlacTermEvent::ResetTitle => {
287                self.title = DEFAULT_TITLE.to_string();
288                cx.emit(Event::TitleChanged);
289            }
290            AlacTermEvent::ClipboardStore(_, data) => {
291                cx.write_to_clipboard(ClipboardItem::new(data))
292            }
293            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
294                &cx.read_from_clipboard()
295                    .map(|ci| ci.text().to_string())
296                    .unwrap_or("".to_string()),
297            )),
298            AlacTermEvent::ColorRequest(index, format) => {
299                let color = self.term.lock().colors()[index].unwrap_or_else(|| {
300                    let term_style = &cx.global::<Settings>().theme.terminal;
301                    to_alac_rgb(get_color_at_index(&index, &term_style.colors))
302                });
303                self.write_to_pty(format(color))
304            }
305            AlacTermEvent::CursorBlinkingChange => {
306                //TODO: Set a timer to blink the cursor on and off
307            }
308            AlacTermEvent::Bell => {
309                cx.emit(Event::Bell);
310            }
311            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
312            AlacTermEvent::TextAreaSizeRequest(_) => println!("Received text area resize request"),
313        }
314    }
315
316    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
317    pub fn write_to_pty(&self, input: String) {
318        self.write_bytes_to_pty(input.into_bytes());
319    }
320
321    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
322    fn write_bytes_to_pty(&self, input: Vec<u8>) {
323        self.term.lock().scroll_display(Scroll::Bottom);
324        self.pty_tx.notify(input);
325    }
326
327    ///Resize the terminal and the PTY. This locks the terminal.
328    pub fn set_size(&self, new_size: WindowSize) {
329        self.pty_tx.0.send(Msg::Resize(new_size)).ok();
330
331        let term_size = TermSize::new(new_size.num_cols as usize, new_size.num_lines as usize);
332        self.term.lock().resize(term_size);
333    }
334
335    pub fn clear(&self) {
336        self.write_to_pty("\x0c".into());
337        self.term.lock().clear_screen(ClearMode::Saved);
338    }
339
340    pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool {
341        let guard = self.term.lock();
342        let mode = guard.mode();
343        let esc = to_esc_str(keystroke, mode);
344        drop(guard);
345        if esc.is_some() {
346            self.write_to_pty(esc.unwrap());
347            true
348        } else {
349            false
350        }
351    }
352
353    ///Paste text into the terminal
354    pub fn paste(&self, text: &str) {
355        if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
356            self.write_to_pty("\x1b[200~".to_string());
357            self.write_to_pty(text.replace('\x1b', "").to_string());
358            self.write_to_pty("\x1b[201~".to_string());
359        } else {
360            self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
361        }
362    }
363
364    pub fn copy(&self) -> Option<String> {
365        let term = self.term.lock();
366        term.selection_to_string()
367    }
368
369    ///Takes the selection out of the terminal
370    pub fn take_selection(&self) -> Option<Selection> {
371        self.term.lock().selection.take()
372    }
373    ///Sets the selection object on the terminal
374    pub fn set_selection(&self, sel: Option<Selection>) {
375        self.term.lock().selection = sel;
376    }
377
378    pub fn render_lock<F, T>(&self, new_size: Option<TermDimensions>, f: F) -> T
379    where
380        F: FnOnce(RenderableContent, char) -> T,
381    {
382        if let Some(new_size) = new_size {
383            self.pty_tx.0.send(Msg::Resize(new_size.into())).ok(); //Give the PTY a chance to react to the new size
384                                                                   //TODO: Is this bad for performance?
385        }
386
387        let mut term = self.term.lock(); //Lock
388
389        if let Some(new_size) = new_size {
390            term.resize(new_size); //Reflow
391        }
392
393        let content = term.renderable_content();
394        let cursor_text = term.grid()[content.cursor.point].c;
395
396        f(content, cursor_text)
397    }
398
399    pub fn get_display_offset(&self) -> usize {
400        self.term.lock().renderable_content().display_offset
401    }
402
403    ///Scroll the terminal
404    pub fn scroll(&self, scroll: Scroll) {
405        self.term.lock().scroll_display(scroll)
406    }
407
408    pub fn click(&self, point: Point, side: Direction, clicks: usize) {
409        let selection_type = match clicks {
410            0 => return, //This is a release
411            1 => Some(SelectionType::Simple),
412            2 => Some(SelectionType::Semantic),
413            3 => Some(SelectionType::Lines),
414            _ => None,
415        };
416
417        let selection =
418            selection_type.map(|selection_type| Selection::new(selection_type, point, side));
419
420        self.set_selection(selection);
421    }
422
423    pub fn drag(&self, point: Point, side: Direction) {
424        if let Some(mut selection) = self.take_selection() {
425            selection.update(point, side);
426            self.set_selection(Some(selection));
427        }
428    }
429
430    pub fn mouse_down(&self, point: Point, side: Direction) {
431        self.set_selection(Some(Selection::new(SelectionType::Simple, point, side)));
432    }
433}
434
435impl Drop for Terminal {
436    fn drop(&mut self) {
437        self.pty_tx.0.send(Msg::Shutdown).ok();
438    }
439}
440
441impl Entity for Terminal {
442    type Event = Event;
443}
444
445//TODO Move this around
446mod alacritty_unix {
447    use alacritty_terminal::config::Program;
448    use gpui::anyhow::{bail, Result};
449    use libc;
450    use std::ffi::CStr;
451    use std::mem::MaybeUninit;
452    use std::ptr;
453
454    #[derive(Debug)]
455    pub struct Passwd<'a> {
456        _name: &'a str,
457        _dir: &'a str,
458        pub shell: &'a str,
459    }
460
461    /// Return a Passwd struct with pointers into the provided buf.
462    ///
463    /// # Unsafety
464    ///
465    /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
466    pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
467        // Create zeroed passwd struct.
468        let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
469
470        let mut res: *mut libc::passwd = ptr::null_mut();
471
472        // Try and read the pw file.
473        let uid = unsafe { libc::getuid() };
474        let status = unsafe {
475            libc::getpwuid_r(
476                uid,
477                entry.as_mut_ptr(),
478                buf.as_mut_ptr() as *mut _,
479                buf.len(),
480                &mut res,
481            )
482        };
483        let entry = unsafe { entry.assume_init() };
484
485        if status < 0 {
486            bail!("getpwuid_r failed");
487        }
488
489        if res.is_null() {
490            bail!("pw not found");
491        }
492
493        // Sanity check.
494        assert_eq!(entry.pw_uid, uid);
495
496        // Build a borrowed Passwd struct.
497        Ok(Passwd {
498            _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
499            _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
500            shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
501        })
502    }
503
504    #[cfg(target_os = "macos")]
505    pub fn _default_shell(pw: &Passwd<'_>) -> Program {
506        let shell_name = pw.shell.rsplit('/').next().unwrap();
507        let argv = vec![
508            String::from("-c"),
509            format!("exec -a -{} {}", shell_name, pw.shell),
510        ];
511
512        Program::WithArgs {
513            program: "/bin/bash".to_owned(),
514            args: argv,
515        }
516    }
517
518    #[cfg(not(target_os = "macos"))]
519    pub fn default_shell(pw: &Passwd<'_>) -> Program {
520        Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
521    }
522}