1mod keymappings;
2
3use alacritty_terminal::{
4 ansi::{ClearMode, Handler},
5 config::{Config, Program, PtyConfig},
6 event::{Event as AlacTermEvent, Notify},
7 event_loop::{EventLoop, Msg, Notifier},
8 grid::Scroll,
9 sync::FairMutex,
10 term::{SizeInfo, TermMode},
11 tty::{self, setup_env},
12 Term,
13};
14use futures::{channel::mpsc::unbounded, StreamExt};
15use settings::{Settings, Shell};
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::keymappings::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 shell: Option<Shell>,
50 env_vars: Option<Vec<(String, String)>>,
51 initial_size: SizeInfo,
52 cx: &mut ModelContext<Self>,
53 ) -> TerminalConnection {
54 let pty_config = {
55 let shell = shell.and_then(|shell| match shell {
56 Shell::System => None,
57 Shell::Program(program) => Some(Program::Just(program)),
58 Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
59 });
60
61 PtyConfig {
62 shell,
63 working_directory: working_directory.clone(),
64 hold: false,
65 }
66 };
67
68 let mut env: HashMap<String, String> = HashMap::new();
69 if let Some(envs) = env_vars {
70 for (var, val) in envs {
71 env.insert(var, val);
72 }
73 }
74
75 //TODO: Properly set the current locale,
76 env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
77
78 let config = Config {
79 pty_config: pty_config.clone(),
80 env,
81 ..Default::default()
82 };
83
84 setup_env(&config);
85
86 //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
87 let (events_tx, mut events_rx) = unbounded();
88
89 //Set up the terminal...
90 let term = Term::new(&config, initial_size, ZedListener(events_tx.clone()));
91 let term = Arc::new(FairMutex::new(term));
92
93 //Setup the pty...
94 let pty = {
95 if let Some(pty) = tty::new(&pty_config, &initial_size, None).ok() {
96 pty
97 } else {
98 let pty_config = PtyConfig {
99 shell: None,
100 working_directory: working_directory.clone(),
101 ..Default::default()
102 };
103
104 tty::new(&pty_config, &initial_size, None)
105 .expect("Failed with default shell too :(")
106 }
107 };
108
109 //And connect them together
110 let event_loop = EventLoop::new(
111 term.clone(),
112 ZedListener(events_tx.clone()),
113 pty,
114 pty_config.hold,
115 false,
116 );
117
118 //Kick things off
119 let pty_tx = event_loop.channel();
120 let _io_thread = event_loop.spawn();
121
122 cx.spawn_weak(|this, mut cx| async move {
123 //Listen for terminal events
124 while let Some(event) = events_rx.next().await {
125 match this.upgrade(&cx) {
126 Some(this) => {
127 this.update(&mut cx, |this, cx| {
128 this.process_terminal_event(event, cx);
129 cx.notify();
130 });
131 }
132 None => break,
133 }
134 }
135 })
136 .detach();
137
138 TerminalConnection {
139 pty_tx: Notifier(pty_tx),
140 term,
141 title: DEFAULT_TITLE.to_string(),
142 associated_directory: working_directory,
143 }
144 }
145
146 ///Takes events from Alacritty and translates them to behavior on this view
147 fn process_terminal_event(
148 &mut self,
149 event: alacritty_terminal::event::Event,
150 cx: &mut ModelContext<Self>,
151 ) {
152 match event {
153 // TODO: Handle is_self_focused in subscription on terminal view
154 AlacTermEvent::Wakeup => {
155 cx.emit(Event::Wakeup);
156 }
157 AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
158 AlacTermEvent::MouseCursorDirty => {
159 //Calculate new cursor style.
160 //TODO: alacritty/src/input.rs:L922-L939
161 //Check on correctly handling mouse events for terminals
162 cx.platform().set_cursor_style(CursorStyle::Arrow); //???
163 }
164 AlacTermEvent::Title(title) => {
165 self.title = title;
166 cx.emit(Event::TitleChanged);
167 }
168 AlacTermEvent::ResetTitle => {
169 self.title = DEFAULT_TITLE.to_string();
170 cx.emit(Event::TitleChanged);
171 }
172 AlacTermEvent::ClipboardStore(_, data) => {
173 cx.write_to_clipboard(ClipboardItem::new(data))
174 }
175 AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
176 &cx.read_from_clipboard()
177 .map(|ci| ci.text().to_string())
178 .unwrap_or("".to_string()),
179 )),
180 AlacTermEvent::ColorRequest(index, format) => {
181 let color = self.term.lock().colors()[index].unwrap_or_else(|| {
182 let term_style = &cx.global::<Settings>().theme.terminal;
183 to_alac_rgb(get_color_at_index(&index, &term_style.colors))
184 });
185 self.write_to_pty(format(color))
186 }
187 AlacTermEvent::CursorBlinkingChange => {
188 //TODO: Set a timer to blink the cursor on and off
189 }
190 AlacTermEvent::Bell => {
191 cx.emit(Event::Bell);
192 }
193 AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
194 }
195 }
196
197 ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
198 pub fn write_to_pty(&mut self, input: String) {
199 self.write_bytes_to_pty(input.into_bytes());
200 }
201
202 ///Write the Input payload to the tty. This locks the terminal so we can scroll it.
203 fn write_bytes_to_pty(&mut self, input: Vec<u8>) {
204 self.term.lock().scroll_display(Scroll::Bottom);
205 self.pty_tx.notify(input);
206 }
207
208 ///Resize the terminal and the PTY. This locks the terminal.
209 pub fn set_size(&mut self, new_size: SizeInfo) {
210 self.pty_tx.0.send(Msg::Resize(new_size)).ok();
211 self.term.lock().resize(new_size);
212 }
213
214 pub fn clear(&mut self) {
215 self.write_to_pty("\x0c".into());
216 self.term.lock().clear_screen(ClearMode::Saved);
217 }
218
219 pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
220 let guard = self.term.lock();
221 let mode = guard.mode();
222 let esc = to_esc_str(keystroke, mode);
223 drop(guard);
224 if esc.is_some() {
225 self.write_to_pty(esc.unwrap());
226 true
227 } else {
228 false
229 }
230 }
231
232 ///Paste text into the terminal
233 pub fn paste(&mut self, text: &str) {
234 if self.term.lock().mode().contains(TermMode::BRACKETED_PASTE) {
235 self.write_to_pty("\x1b[200~".to_string());
236 self.write_to_pty(text.replace('\x1b', "").to_string());
237 self.write_to_pty("\x1b[201~".to_string());
238 } else {
239 self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r"));
240 }
241 }
242}
243
244impl Drop for TerminalConnection {
245 fn drop(&mut self) {
246 self.pty_tx.0.send(Msg::Shutdown).ok();
247 }
248}
249
250impl Entity for TerminalConnection {
251 type Event = Event;
252}