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