1mod events;
2
3use alacritty_terminal::{
4 ansi::{ClearMode, Handler},
5 config::{Config, PtyConfig},
6 event::{Event as AlacTermEvent, Notify},
7 event_loop::{EventLoop, Msg, Notifier},
8 grid::Scroll,
9 sync::FairMutex,
10 term::SizeInfo,
11 tty::{self, setup_env},
12 Term,
13};
14use futures::{channel::mpsc::unbounded, StreamExt};
15use settings::Settings;
16use std::{collections::HashMap, path::PathBuf, sync::Arc};
17
18use gpui::{keymap::Keystroke, ClipboardItem, CursorStyle, Entity, ModelContext};
19
20use crate::{
21 color_translation::{get_color_at_index, to_alac_rgb},
22 ZedListener,
23};
24
25use self::events::to_esc_str;
26
27const DEFAULT_TITLE: &str = "Terminal";
28
29///Upward flowing events, for changing the title and such
30#[derive(Copy, Clone, Debug)]
31pub enum Event {
32 TitleChanged,
33 CloseTerminal,
34 Activate,
35 Wakeup,
36 Bell,
37}
38
39pub struct TerminalConnection {
40 pub pty_tx: Notifier,
41 pub term: Arc<FairMutex<Term<ZedListener>>>,
42 pub title: String,
43 pub associated_directory: Option<PathBuf>,
44}
45
46impl TerminalConnection {
47 pub fn new(
48 working_directory: Option<PathBuf>,
49 initial_size: SizeInfo,
50 cx: &mut ModelContext<Self>,
51 ) -> TerminalConnection {
52 let pty_config = PtyConfig {
53 shell: None, //Use the users default shell
54 working_directory: working_directory.clone(),
55 hold: false,
56 };
57
58 let mut env: HashMap<String, String> = HashMap::new();
59 //TODO: Properly set the current locale,
60 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
61
62 let config = Config {
63 pty_config: pty_config.clone(),
64 env,
65 ..Default::default()
66 };
67
68 setup_env(&config);
69
70 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
71 let (events_tx, mut events_rx) = unbounded();
72
73 //Set up the terminal...
74 let term = Term::new(&config, initial_size, ZedListener(events_tx.clone()));
75 let term = Arc::new(FairMutex::new(term));
76
77 //Setup the pty...
78 let pty = tty::new(&pty_config, &initial_size, None).expect("Could not create tty");
79
80 //And connect them together
81 let event_loop = EventLoop::new(
82 term.clone(),
83 ZedListener(events_tx.clone()),
84 pty,
85 pty_config.hold,
86 false,
87 );
88
89 //Kick things off
90 let pty_tx = event_loop.channel();
91 let _io_thread = event_loop.spawn();
92
93 cx.spawn_weak(|this, mut cx| async move {
94 //Listen for terminal events
95 while let Some(event) = events_rx.next().await {
96 match this.upgrade(&cx) {
97 Some(this) => {
98 this.update(&mut cx, |this, cx| {
99 this.process_terminal_event(event, cx);
100 cx.notify();
101 });
102 }
103 None => break,
104 }
105 }
106 })
107 .detach();
108
109 TerminalConnection {
110 pty_tx: Notifier(pty_tx),
111 term,
112 title: DEFAULT_TITLE.to_string(),
113 associated_directory: working_directory,
114 }
115 }
116
117 ///Takes events from Alacritty and translates them to behavior on this view
118 fn process_terminal_event(
119 &mut self,
120 event: alacritty_terminal::event::Event,
121 cx: &mut ModelContext<Self>,
122 ) {
123 match event {
124 // TODO: Handle is_self_focused in subscription on terminal view
125 AlacTermEvent::Wakeup => {
126 cx.emit(Event::Wakeup);
127 }
128 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
129 AlacTermEvent::MouseCursorDirty => {
130 //Calculate new cursor style.
131 //TODO: alacritty/src/input.rs:L922-L939
132 //Check on correctly handling mouse events for terminals
133 cx.platform().set_cursor_style(CursorStyle::Arrow); //???
134 }
135 AlacTermEvent::Title(title) => {
136 self.title = title;
137 cx.emit(Event::TitleChanged);
138 }
139 AlacTermEvent::ResetTitle => {
140 self.title = DEFAULT_TITLE.to_string();
141 cx.emit(Event::TitleChanged);
142 }
143 AlacTermEvent::ClipboardStore(_, data) => {
144 cx.write_to_clipboard(ClipboardItem::new(data))
145 }
146 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
147 &cx.read_from_clipboard()
148 .map(|ci| ci.text().to_string())
149 .unwrap_or("".to_string()),
150 )),
151 AlacTermEvent::ColorRequest(index, format) => {
152 let color = self.term.lock().colors()[index].unwrap_or_else(|| {
153 let term_style = &cx.global::<Settings>().theme.terminal;
154 to_alac_rgb(get_color_at_index(&index, &term_style.colors))
155 });
156 self.write_to_pty(format(color))
157 }
158 AlacTermEvent::CursorBlinkingChange => {
159 //TODO: Set a timer to blink the cursor on and off
160 }
161 AlacTermEvent::Bell => {
162 cx.emit(Event::Bell);
163 }
164 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
165 }
166 }
167
168 ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
169 pub fn write_to_pty(&mut self, input: String) {
170 self.write_bytes_to_pty(input.into_bytes());
171 }
172
173 ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
174 fn write_bytes_to_pty(&mut self, input: Vec<u8>) {
175 self.term.lock().scroll_display(Scroll::Bottom);
176 self.pty_tx.notify(input);
177 }
178
179 ///Resize the terminal and the PTY. This locks the terminal.
180 pub fn set_size(&mut self, new_size: SizeInfo) {
181 self.pty_tx.0.send(Msg::Resize(new_size)).ok();
182 self.term.lock().resize(new_size);
183 }
184
185 pub fn clear(&mut self) {
186 self.write_to_pty("\x0c".into());
187 self.term.lock().clear_screen(ClearMode::Saved);
188 }
189
190 pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
191 let guard = self.term.lock();
192 let mode = guard.mode();
193 let esc = to_esc_str(keystroke, mode);
194 drop(guard);
195 if esc.is_some() {
196 self.write_to_pty(esc.unwrap());
197 true
198 } else {
199 false
200 }
201 }
202}
203
204impl Drop for TerminalConnection {
205 fn drop(&mut self) {
206 self.pty_tx.0.send(Msg::Shutdown).ok();
207 }
208}
209
210impl Entity for TerminalConnection {
211 type Event = Event;
212}