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