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