connection.rs

  1mod keymappings;
  2
  3use alacritty_terminal::{
  4    ansi::{ClearMode, Handler},
  5    config::{Config, Program, PtyConfig},
  6    event::{Event as AlacTermEvent, Notify},
  7    event_loop::{EventLoop, Msg, Notifier},
  8    grid::Scroll,
  9    sync::FairMutex,
 10    term::{SizeInfo, TermMode},
 11    tty::{self, setup_env},
 12    Term,
 13};
 14use futures::{channel::mpsc::unbounded, StreamExt};
 15use settings::{Settings, Shell};
 16use std::{collections::HashMap, path::PathBuf, sync::Arc};
 17
 18use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
 19
 20use crate::{
 21    color_translation::{get_color_at_index, to_alac_rgb},
 22    ZedListener,
 23};
 24
 25use self::keymappings::to_esc_str;
 26
 27const DEFAULT_TITLE: &str = "Terminal";
 28
 29///Upward flowing events, for changing the title and such
 30#[derive(Copy, Clone, Debug)]
 31pub enum Event {
 32    TitleChanged,
 33    CloseTerminal,
 34    Activate,
 35    Wakeup,
 36    Bell,
 37}
 38
 39pub struct TerminalConnection {
 40    pub pty_tx: Notifier,
 41    pub term: Arc<FairMutex<Term<ZedListener>>>,
 42    pub title: String,
 43    pub associated_directory: Option<PathBuf>,
 44}
 45
 46impl TerminalConnection {
 47    pub fn new(
 48        working_directory: Option<PathBuf>,
 49        shell: Option<Shell>,
 50        env_vars: Option<Vec<(String, String)>>,
 51        initial_size: SizeInfo,
 52        cx: &mut ModelContext<Self>,
 53    ) -> TerminalConnection {
 54        let pty_config = {
 55            let shell = shell.and_then(|shell| match shell {
 56                Shell::System => None,
 57                Shell::Program(program) => Some(Program::Just(program)),
 58                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
 59            });
 60
 61            PtyConfig {
 62                shell,
 63                working_directory: working_directory.clone(),
 64                hold: false,
 65            }
 66        };
 67
 68        let mut env: HashMap<String, String> = HashMap::new();
 69        if let Some(envs) = env_vars {
 70            for (var, val) in envs {
 71                env.insert(var, val);
 72            }
 73        }
 74
 75        //TODO: Properly set the current locale,
 76        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
 77
 78        let config = Config {
 79            pty_config: pty_config.clone(),
 80            env,
 81            ..Default::default()
 82        };
 83
 84        setup_env(&config);
 85
 86        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
 87        let (events_tx, mut events_rx) = unbounded();
 88
 89        //Set up the terminal...
 90        let term = Term::new(&config, initial_size, ZedListener(events_tx.clone()));
 91        let term = Arc::new(FairMutex::new(term));
 92
 93        //Setup the pty...
 94        let pty = {
 95            if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() {
 96                pty
 97            } else {
 98                let pty_config = PtyConfig {
 99                    shell: None,
100                    working_directory: working_directory.clone(),
101                    ..Default::default()
102                };
103
104                tty::new(&pty_config, &initial_size, None)
105                    .expect("Failed with default shell too :(")
106            }
107        };
108
109        //And connect them together
110        let event_loop = EventLoop::new(
111            term.clone(),
112            ZedListener(events_tx.clone()),
113            pty,
114            pty_config.hold,
115            false,
116        );
117
118        //Kick things off
119        let pty_tx = event_loop.channel();
120        let _io_thread = event_loop.spawn();
121
122        cx.spawn_weak(|this, mut cx| async move {
123            //Listen for terminal events
124            while let Some(event) = events_rx.next().await {
125                match this.upgrade(&cx) {
126                    Some(this) => {
127                        this.update(&mut cx, |this, cx| {
128                            this.process_terminal_event(event, cx);
129                            cx.notify();
130                        });
131                    }
132                    None => break,
133                }
134            }
135        })
136        .detach();
137
138        TerminalConnection {
139            pty_tx: Notifier(pty_tx),
140            term,
141            title: DEFAULT_TITLE.to_string(),
142            associated_directory: working_directory,
143        }
144    }
145
146    ///Takes events from Alacritty and translates them to behavior on this view
147    fn process_terminal_event(
148        &mut self,
149        event: alacritty_terminal::event::Event,
150        cx: &mut ModelContext<Self>,
151    ) {
152        match event {
153            // TODO: Handle is_self_focused in subscription on terminal view
154            AlacTermEvent::Wakeup => {
155                cx.emit(Event::Wakeup);
156            }
157            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
158            AlacTermEvent::MouseCursorDirty => {
159                //Calculate new cursor style.
160                //TODO: alacritty/src/input.rs:L922-L939
161                //Check on correctly handling mouse events for terminals
162                cx.platform().set_cursor_style(CursorStyle::Arrow); //???
163            }
164            AlacTermEvent::Title(title) => {
165                self.title = title;
166                cx.emit(Event::TitleChanged);
167            }
168            AlacTermEvent::ResetTitle => {
169                self.title = DEFAULT_TITLE.to_string();
170                cx.emit(Event::TitleChanged);
171            }
172            AlacTermEvent::ClipboardStore(_, data) => {
173                cx.write_to_clipboard(ClipboardItem::new(data))
174            }
175            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
176                &cx.read_from_clipboard()
177                    .map(|ci| ci.text().to_string())
178                    .unwrap_or("".to_string()),
179            )),
180            AlacTermEvent::ColorRequest(index, format) => {
181                let color = self.term.lock().colors()[index].unwrap_or_else(|| {
182                    let term_style = &cx.global::<Settings>().theme.terminal;
183                    to_alac_rgb(get_color_at_index(&index, &term_style.colors))
184                });
185                self.write_to_pty(format(color))
186            }
187            AlacTermEvent::CursorBlinkingChange => {
188                //TODO: Set a timer to blink the cursor on and off
189            }
190            AlacTermEvent::Bell => {
191                cx.emit(Event::Bell);
192            }
193            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
194        }
195    }
196
197    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
198    pub fn write_to_pty(&mut self, input: String) {
199        self.write_bytes_to_pty(input.into_bytes());
200    }
201
202    ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
203    fn write_bytes_to_pty(&mut self, input: Vec<u8>) {
204        self.term.lock().scroll_display(Scroll::Bottom);
205        self.pty_tx.notify(input);
206    }
207
208    ///Resize the terminal and the PTY. This locks the terminal.
209    pub fn set_size(&mut self, new_size: SizeInfo) {
210        self.pty_tx.0.send(Msg::Resize(new_size)).ok();
211        self.term.lock().resize(new_size);
212    }
213
214    pub fn clear(&mut self) {
215        self.write_to_pty("\x0c".into());
216        self.term.lock().clear_screen(ClearMode::Saved);
217    }
218
219    pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
220        let guard = self.term.lock();
221        let mode = guard.mode();
222        let esc = to_esc_str(keystroke, mode);
223        drop(guard);
224        if esc.is_some() {
225            self.write_to_pty(esc.unwrap());
226            true
227        } else {
228            false
229        }
230    }
231
232    ///Paste text into the terminal
233    pub fn paste(&mut self, text: &str) {
234        if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
235            self.write_to_pty("\x1b[200~".to_string());
236            self.write_to_pty(text.replace('\x1b', "").to_string());
237            self.write_to_pty("\x1b[201~".to_string());
238        } else {
239            self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
240        }
241    }
242}
243
244impl Drop for TerminalConnection {
245    fn drop(&mut self) {
246        self.pty_tx.0.send(Msg::Shutdown).ok();
247    }
248}
249
250impl Entity for TerminalConnection {
251    type Event = Event;
252}