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