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