connection.rs

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