connection.rs

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