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