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