terminal.rs

  1pub mod connected_el;
  2pub mod connected_view;
  3pub mod mappings;
  4pub mod modal;
  5pub mod terminal_view;
  6
  7use alacritty_terminal::{
  8    ansi::{ClearMode, Handler},
  9    config::{Config, Program, PtyConfig, Scrolling},
 10    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
 11    event_loop::{EventLoop, Msg, Notifier},
 12    grid::{Dimensions, Scroll},
 13    index::{Direction, Point},
 14    selection::{Selection, SelectionType},
 15    sync::FairMutex,
 16    term::{RenderableContent, TermMode},
 17    tty::{self, setup_env},
 18    Term,
 19};
 20use anyhow::{bail, Result};
 21
 22use futures::{
 23    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
 24    FutureExt,
 25};
 26
 27use modal::deploy_modal;
 28use settings::{Settings, Shell};
 29use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc, time::Duration};
 30use thiserror::Error;
 31
 32use gpui::{
 33    geometry::vector::{vec2f, Vector2F},
 34    keymap::Keystroke,
 35    ClipboardItem, Entity, ModelContext, MutableAppContext,
 36};
 37
 38use crate::mappings::{
 39    colors::{get_color_at_index, to_alac_rgb},
 40    keys::to_esc_str,
 41};
 42
 43///Initialize and register all of our action handlers
 44pub fn init(cx: &mut MutableAppContext) {
 45    cx.add_action(deploy_modal);
 46
 47    terminal_view::init(cx);
 48    connected_view::init(cx);
 49}
 50
 51const DEBUG_TERMINAL_WIDTH: f32 = 500.;
 52const DEBUG_TERMINAL_HEIGHT: f32 = 30.; //This needs to be wide enough that the CI & a local dev's prompt can fill the whole space.
 53const DEBUG_CELL_WIDTH: f32 = 5.;
 54const DEBUG_LINE_HEIGHT: f32 = 5.;
 55// const MAX_FRAME_RATE: f32 = 60.;
 56// const BACK_BUFFER_SIZE: usize = 5000;
 57
 58///Upward flowing events, for changing the title and such
 59#[derive(Clone, Copy, Debug)]
 60pub enum Event {
 61    TitleChanged,
 62    CloseTerminal,
 63    Activate,
 64    Bell,
 65    Wakeup,
 66}
 67
 68#[derive(Clone, Debug)]
 69enum InternalEvent {
 70    TermEvent(AlacTermEvent),
 71    Resize(TerminalSize),
 72    Clear,
 73    Scroll(Scroll),
 74    SetSelection(Option<Selection>),
 75    UpdateSelection((Point, Direction)),
 76    Copy,
 77}
 78
 79///A translation struct for Alacritty to communicate with us from their event loop
 80#[derive(Clone)]
 81pub struct ZedListener(UnboundedSender<AlacTermEvent>);
 82
 83impl EventListener for ZedListener {
 84    fn send_event(&self, event: AlacTermEvent) {
 85        self.0.unbounded_send(event).ok();
 86    }
 87}
 88
 89#[derive(Clone, Copy, Debug)]
 90pub struct TerminalSize {
 91    cell_width: f32,
 92    line_height: f32,
 93    height: f32,
 94    width: f32,
 95}
 96
 97impl TerminalSize {
 98    pub fn new(line_height: f32, cell_width: f32, size: Vector2F) -> Self {
 99        TerminalSize {
100            cell_width,
101            line_height,
102            width: size.x(),
103            height: size.y(),
104        }
105    }
106
107    pub fn num_lines(&self) -> usize {
108        (self.height / self.line_height).floor() as usize
109    }
110
111    pub fn num_columns(&self) -> usize {
112        (self.width / self.cell_width).floor() as usize
113    }
114
115    pub fn height(&self) -> f32 {
116        self.height
117    }
118
119    pub fn width(&self) -> f32 {
120        self.width
121    }
122
123    pub fn cell_width(&self) -> f32 {
124        self.cell_width
125    }
126
127    pub fn line_height(&self) -> f32 {
128        self.line_height
129    }
130}
131impl Default for TerminalSize {
132    fn default() -> Self {
133        TerminalSize::new(
134            DEBUG_LINE_HEIGHT,
135            DEBUG_CELL_WIDTH,
136            vec2f(DEBUG_TERMINAL_WIDTH, DEBUG_TERMINAL_HEIGHT),
137        )
138    }
139}
140
141impl Into<WindowSize> for TerminalSize {
142    fn into(self) -> WindowSize {
143        WindowSize {
144            num_lines: self.num_lines() as u16,
145            num_cols: self.num_columns() as u16,
146            cell_width: self.cell_width() as u16,
147            cell_height: self.line_height() as u16,
148        }
149    }
150}
151
152impl Dimensions for TerminalSize {
153    fn total_lines(&self) -> usize {
154        self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
155    }
156
157    fn screen_lines(&self) -> usize {
158        self.num_lines()
159    }
160
161    fn columns(&self) -> usize {
162        self.num_columns()
163    }
164}
165
166#[derive(Error, Debug)]
167pub struct TerminalError {
168    pub directory: Option<PathBuf>,
169    pub shell: Option<Shell>,
170    pub source: std::io::Error,
171}
172
173impl TerminalError {
174    pub fn fmt_directory(&self) -> String {
175        self.directory
176            .clone()
177            .map(|path| {
178                match path
179                    .into_os_string()
180                    .into_string()
181                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
182                {
183                    Ok(s) => s,
184                    Err(s) => s,
185                }
186            })
187            .unwrap_or_else(|| {
188                let default_dir =
189                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
190                match default_dir {
191                    Some(dir) => format!("<none specified, using home directory> {}", dir),
192                    None => "<none specified, could not find home directory>".to_string(),
193                }
194            })
195    }
196
197    pub fn shell_to_string(&self) -> Option<String> {
198        self.shell.as_ref().map(|shell| match shell {
199            Shell::System => "<system shell>".to_string(),
200            Shell::Program(p) => p.to_string(),
201            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
202        })
203    }
204
205    pub fn fmt_shell(&self) -> String {
206        self.shell
207            .clone()
208            .map(|shell| match shell {
209                Shell::System => {
210                    let mut buf = [0; 1024];
211                    let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
212
213                    match pw {
214                        Some(pw) => format!("<system defined shell> {}", pw.shell),
215                        None => "<could not access the password file>".to_string(),
216                    }
217                }
218                Shell::Program(s) => s,
219                Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
220            })
221            .unwrap_or_else(|| {
222                let mut buf = [0; 1024];
223                let pw = alacritty_unix::get_pw_entry(&mut buf).ok();
224                match pw {
225                    Some(pw) => {
226                        format!("<none specified, using system defined shell> {}", pw.shell)
227                    }
228                    None => "<none specified, could not access the password file> {}".to_string(),
229                }
230            })
231    }
232}
233
234impl Display for TerminalError {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        let dir_string: String = self.fmt_directory();
237        let shell = self.fmt_shell();
238
239        write!(
240            f,
241            "Working directory: {} Shell command: `{}`, IOError: {}",
242            dir_string, shell, self.source
243        )
244    }
245}
246
247pub struct TerminalBuilder {
248    terminal: Terminal,
249    events_rx: UnboundedReceiver<AlacTermEvent>,
250}
251
252impl TerminalBuilder {
253    pub fn new(
254        working_directory: Option<PathBuf>,
255        shell: Option<Shell>,
256        env: Option<HashMap<String, String>>,
257        initial_size: TerminalSize,
258    ) -> Result<TerminalBuilder> {
259        let pty_config = {
260            let alac_shell = shell.clone().and_then(|shell| match shell {
261                Shell::System => None,
262                Shell::Program(program) => Some(Program::Just(program)),
263                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
264            });
265
266            PtyConfig {
267                shell: alac_shell,
268                working_directory: working_directory.clone(),
269                hold: false,
270            }
271        };
272
273        let mut env = env.unwrap_or_else(|| HashMap::new());
274
275        //TODO: Properly set the current locale,
276        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
277
278        let alac_scrolling = Scrolling::default();
279        // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
280
281        let config = Config {
282            pty_config: pty_config.clone(),
283            env,
284            scrolling: alac_scrolling,
285            ..Default::default()
286        };
287
288        setup_env(&config);
289
290        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
291        let (events_tx, events_rx) = unbounded();
292        //Set up the terminal...
293        let term = Term::new(&config, &initial_size, ZedListener(events_tx.clone()));
294        let term = Arc::new(FairMutex::new(term));
295
296        //Setup the pty...
297        let pty = match tty::new(&pty_config, initial_size.clone().into(), None) {
298            Ok(pty) => pty,
299            Err(error) => {
300                bail!(TerminalError {
301                    directory: working_directory,
302                    shell,
303                    source: error,
304                });
305            }
306        };
307
308        let shell_txt = {
309            match shell {
310                Some(Shell::System) | None => {
311                    let mut buf = [0; 1024];
312                    let pw = alacritty_unix::get_pw_entry(&mut buf).unwrap();
313                    pw.shell.to_string()
314                }
315                Some(Shell::Program(program)) => program,
316                Some(Shell::WithArguments { program, args }) => {
317                    format!("{} {}", program, args.join(" "))
318                }
319            }
320        };
321
322        //And connect them together
323        let event_loop = EventLoop::new(
324            term.clone(),
325            ZedListener(events_tx.clone()),
326            pty,
327            pty_config.hold,
328            false,
329        );
330
331        //Kick things off
332        let pty_tx = event_loop.channel();
333        let _io_thread = event_loop.spawn();
334
335        let terminal = Terminal {
336            pty_tx: Notifier(pty_tx),
337            term,
338            events: vec![],
339            title: shell_txt.clone(),
340            default_title: shell_txt,
341            last_mode: TermMode::NONE,
342            cur_size: initial_size,
343            // utilization: 0.,
344        };
345
346        Ok(TerminalBuilder {
347            terminal,
348            events_rx,
349        })
350    }
351
352    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
353        //Event loop
354        cx.spawn_weak(|this, mut cx| async move {
355            use futures::StreamExt;
356
357            while let Some(event) = self.events_rx.next().await {
358                this.upgrade(&cx)?.update(&mut cx, |this, cx| {
359                    //Process the first event immediately for lowered latency
360                    this.process_event(&event, cx);
361                });
362
363                'outer: loop {
364                    let mut events = vec![];
365                    let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
366
367                    loop {
368                        futures::select_biased! {
369                            _ = timer => break,
370                            event = self.events_rx.next() => {
371                                if let Some(event) = event {
372                                    events.push(event);
373                                    if events.len() > 100 {
374                                        break;
375                                    }
376                                } else {
377                                    break;
378                                }
379                            },
380                        }
381                    }
382
383                    if events.len() == 0 {
384                        smol::future::yield_now().await;
385                        break 'outer;
386                    } else {
387                        this.upgrade(&cx)?.update(&mut cx, |this, cx| {
388                            for event in events {
389                                this.process_event(&event, cx);
390                            }
391                        });
392                        smol::future::yield_now().await;
393                    }
394                }
395            }
396
397            Some(())
398        })
399        .detach();
400
401        // //Render loop
402        // cx.spawn_weak(|this, mut cx| async move {
403        //     loop {
404        //         let utilization = match this.upgrade(&cx) {
405        //             Some(this) => this.update(&mut cx, |this, cx| {
406        //                 cx.notify();
407        //                 this.utilization()
408        //             }),
409        //             None => break,
410        //         };
411
412        //         let utilization = (1. - utilization).clamp(0.1, 1.);
413        //         let delay = cx.background().timer(Duration::from_secs_f32(
414        //             1.0 / (Terminal::default_fps() * utilization),
415        //         ));
416
417        //         delay.await;
418        //     }
419        // })
420        // .detach();
421
422        self.terminal
423    }
424}
425
426pub struct Terminal {
427    pty_tx: Notifier,
428    term: Arc<FairMutex<Term<ZedListener>>>,
429    events: Vec<InternalEvent>,
430    default_title: String,
431    title: String,
432    cur_size: TerminalSize,
433    last_mode: TermMode,
434    //Percentage, between 0 and 1
435    // utilization: f32,
436}
437
438impl Terminal {
439    // fn default_fps() -> f32 {
440    //     MAX_FRAME_RATE
441    // }
442
443    // fn utilization(&self) -> f32 {
444    //     self.utilization
445    // }
446
447    fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
448        match event {
449            AlacTermEvent::Title(title) => {
450                self.title = title.to_string();
451                cx.emit(Event::TitleChanged);
452            }
453            AlacTermEvent::ResetTitle => {
454                self.title = self.default_title.clone();
455                cx.emit(Event::TitleChanged);
456            }
457            AlacTermEvent::ClipboardStore(_, data) => {
458                cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
459            }
460            AlacTermEvent::ClipboardLoad(_, format) => self.notify_pty(format(
461                &cx.read_from_clipboard()
462                    .map(|ci| ci.text().to_string())
463                    .unwrap_or("".to_string()),
464            )),
465            AlacTermEvent::PtyWrite(out) => self.notify_pty(out.clone()),
466            AlacTermEvent::TextAreaSizeRequest(format) => {
467                self.notify_pty(format(self.cur_size.clone().into()))
468            }
469            AlacTermEvent::CursorBlinkingChange => {
470                //TODO whatever state we need to set to get the cursor blinking
471            }
472            AlacTermEvent::Bell => {
473                cx.emit(Event::Bell);
474            }
475            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
476            AlacTermEvent::MouseCursorDirty => {
477                //NOOP, Handled in render
478            }
479            AlacTermEvent::Wakeup => {
480                cx.emit(Event::Wakeup);
481                cx.notify();
482            }
483            AlacTermEvent::ColorRequest(_, _) => {
484                self.events.push(InternalEvent::TermEvent(event.clone()))
485            }
486        }
487    }
488
489    // fn process_events(&mut self, events: Vec<AlacTermEvent>, cx: &mut ModelContext<Self>) {
490    //     for event in events.into_iter() {
491    //         self.process_event(&event, cx);
492    //     }
493    // }
494
495    ///Takes events from Alacritty and translates them to behavior on this view
496    fn process_terminal_event(
497        &mut self,
498        event: &InternalEvent,
499        term: &mut Term<ZedListener>,
500        cx: &mut ModelContext<Self>,
501    ) {
502        // TODO: Handle is_self_focused in subscription on terminal view
503        match event {
504            InternalEvent::TermEvent(term_event) => match term_event {
505                //Needs to lock
506                AlacTermEvent::ColorRequest(index, format) => {
507                    let color = term.colors()[*index].unwrap_or_else(|| {
508                        let term_style = &cx.global::<Settings>().theme.terminal;
509                        to_alac_rgb(get_color_at_index(index, &term_style.colors))
510                    });
511                    self.notify_pty(format(color))
512                }
513                _ => {} //Other events are handled in the event loop
514            },
515            InternalEvent::Resize(new_size) => {
516                self.cur_size = new_size.clone();
517
518                self.pty_tx
519                    .0
520                    .send(Msg::Resize(new_size.clone().into()))
521                    .ok();
522
523                term.resize(*new_size);
524            }
525            InternalEvent::Clear => {
526                self.notify_pty("\x0c".to_string());
527                term.clear_screen(ClearMode::Saved);
528            }
529            InternalEvent::Scroll(scroll) => term.scroll_display(*scroll),
530            InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
531            InternalEvent::UpdateSelection((point, side)) => {
532                if let Some(mut selection) = term.selection.take() {
533                    selection.update(*point, *side);
534                    term.selection = Some(selection);
535                }
536            }
537
538            InternalEvent::Copy => {
539                if let Some(txt) = term.selection_to_string() {
540                    cx.write_to_clipboard(ClipboardItem::new(txt))
541                }
542            }
543        }
544    }
545
546    pub fn notify_pty(&self, txt: String) {
547        self.pty_tx.notify(txt.into_bytes());
548    }
549
550    ///Write the Input payload to the tty.
551    pub fn write_to_pty(&mut self, input: String) {
552        self.pty_tx.notify(input.into_bytes());
553    }
554
555    ///Resize the terminal and the PTY.
556    pub fn set_size(&mut self, new_size: TerminalSize) {
557        self.events.push(InternalEvent::Resize(new_size.into()))
558    }
559
560    pub fn clear(&mut self) {
561        self.events.push(InternalEvent::Clear)
562    }
563
564    pub fn try_keystroke(&self, keystroke: &Keystroke) -> bool {
565        let esc = to_esc_str(keystroke, &self.last_mode);
566        if let Some(esc) = esc {
567            self.notify_pty(esc);
568            true
569        } else {
570            false
571        }
572    }
573
574    ///Paste text into the terminal
575    pub fn paste(&self, text: &str) {
576        if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
577            self.notify_pty("\x1b[200~".to_string());
578            self.notify_pty(text.replace('\x1b', "").to_string());
579            self.notify_pty("\x1b[201~".to_string());
580        } else {
581            self.notify_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
582        }
583    }
584
585    pub fn copy(&mut self) {
586        self.events.push(InternalEvent::Copy);
587    }
588
589    pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
590    where
591        F: FnOnce(RenderableContent, char) -> T,
592    {
593        let m = self.term.clone(); //Arc clone
594        let mut term = m.lock();
595
596        while let Some(e) = self.events.pop() {
597            self.process_terminal_event(&e, &mut term, cx)
598        }
599
600        // self.utilization = Self::estimate_utilization(term.take_last_processed_bytes());
601        self.last_mode = term.mode().clone();
602
603        let content = term.renderable_content();
604
605        let cursor_text = term.grid()[content.cursor.point].c;
606
607        f(content, cursor_text)
608    }
609
610    // fn estimate_utilization(last_processed: usize) -> f32 {
611    //     let buffer_utilization = (last_processed as f32 / (READ_BUFFER_SIZE as f32)).clamp(0., 1.);
612
613    //     //Scale result to bias low, then high
614    //     buffer_utilization * buffer_utilization
615    // }
616
617    ///Scroll the terminal
618    pub fn scroll(&mut self, scroll: Scroll) {
619        self.events.push(InternalEvent::Scroll(scroll));
620    }
621
622    pub fn click(&mut self, point: Point, side: Direction, clicks: usize) {
623        let selection_type = match clicks {
624            0 => return, //This is a release
625            1 => Some(SelectionType::Simple),
626            2 => Some(SelectionType::Semantic),
627            3 => Some(SelectionType::Lines),
628            _ => None,
629        };
630
631        let selection =
632            selection_type.map(|selection_type| Selection::new(selection_type, point, side));
633
634        self.events.push(InternalEvent::SetSelection(selection));
635    }
636
637    pub fn drag(&mut self, point: Point, side: Direction) {
638        self.events
639            .push(InternalEvent::UpdateSelection((point, side)));
640    }
641
642    ///TODO: Check if the mouse_down-then-click assumption holds, so this code works as expected
643    pub fn mouse_down(&mut self, point: Point, side: Direction) {
644        self.events
645            .push(InternalEvent::SetSelection(Some(Selection::new(
646                SelectionType::Simple,
647                point,
648                side,
649            ))));
650    }
651}
652
653impl Drop for Terminal {
654    fn drop(&mut self) {
655        self.pty_tx.0.send(Msg::Shutdown).ok();
656    }
657}
658
659impl Entity for Terminal {
660    type Event = Event;
661}
662
663#[cfg(test)]
664mod tests {
665    pub mod terminal_test_context;
666}
667
668//TODO Move this around and clean up the code
669mod alacritty_unix {
670    use alacritty_terminal::config::Program;
671    use gpui::anyhow::{bail, Result};
672    use libc;
673    use std::ffi::CStr;
674    use std::mem::MaybeUninit;
675    use std::ptr;
676
677    #[derive(Debug)]
678    pub struct Passwd<'a> {
679        _name: &'a str,
680        _dir: &'a str,
681        pub shell: &'a str,
682    }
683
684    /// Return a Passwd struct with pointers into the provided buf.
685    ///
686    /// # Unsafety
687    ///
688    /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen.
689    pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result<Passwd<'_>> {
690        // Create zeroed passwd struct.
691        let mut entry: MaybeUninit<libc::passwd> = MaybeUninit::uninit();
692
693        let mut res: *mut libc::passwd = ptr::null_mut();
694
695        // Try and read the pw file.
696        let uid = unsafe { libc::getuid() };
697        let status = unsafe {
698            libc::getpwuid_r(
699                uid,
700                entry.as_mut_ptr(),
701                buf.as_mut_ptr() as *mut _,
702                buf.len(),
703                &mut res,
704            )
705        };
706        let entry = unsafe { entry.assume_init() };
707
708        if status < 0 {
709            bail!("getpwuid_r failed");
710        }
711
712        if res.is_null() {
713            bail!("pw not found");
714        }
715
716        // Sanity check.
717        assert_eq!(entry.pw_uid, uid);
718
719        // Build a borrowed Passwd struct.
720        Ok(Passwd {
721            _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() },
722            _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() },
723            shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() },
724        })
725    }
726
727    #[cfg(target_os = "macos")]
728    pub fn _default_shell(pw: &Passwd<'_>) -> Program {
729        let shell_name = pw.shell.rsplit('/').next().unwrap();
730        let argv = vec![
731            String::from("-c"),
732            format!("exec -a -{} {}", shell_name, pw.shell),
733        ];
734
735        Program::WithArgs {
736            program: "/bin/bash".to_owned(),
737            args: argv,
738        }
739    }
740
741    #[cfg(not(target_os = "macos"))]
742    pub fn default_shell(pw: &Passwd<'_>) -> Program {
743        Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned()))
744    }
745}