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