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