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