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}