1use std::time::Duration;
2
3use alacritty_terminal::term::TermMode;
4use context_menu::{ContextMenu, ContextMenuItem};
5use gpui::{
6 actions,
7 elements::{ChildView, ParentElement, Stack},
8 geometry::vector::Vector2F,
9 impl_internal_actions,
10 keymap::Keystroke,
11 AnyViewHandle, AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View,
12 ViewContext, ViewHandle,
13};
14use settings::{Settings, TerminalBlink};
15use smol::Timer;
16use workspace::pane;
17
18use crate::{connected_el::TerminalEl, Event, Terminal};
19
20const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
21
22///Event to transmit the scroll from the element to the view
23#[derive(Clone, Debug, PartialEq)]
24pub struct ScrollTerminal(pub i32);
25
26#[derive(Clone, PartialEq)]
27pub struct DeployContextMenu {
28 pub position: Vector2F,
29}
30
31actions!(
32 terminal,
33 [
34 Up,
35 Down,
36 CtrlC,
37 Escape,
38 Enter,
39 Clear,
40 Copy,
41 Paste,
42 ShowCharacterPalette,
43 ]
44);
45impl_internal_actions!(project_panel, [DeployContextMenu]);
46
47pub fn init(cx: &mut MutableAppContext) {
48 //Global binding overrrides
49 cx.add_action(ConnectedView::ctrl_c);
50 cx.add_action(ConnectedView::up);
51 cx.add_action(ConnectedView::down);
52 cx.add_action(ConnectedView::escape);
53 cx.add_action(ConnectedView::enter);
54 //Useful terminal views
55 cx.add_action(ConnectedView::deploy_context_menu);
56 cx.add_action(ConnectedView::copy);
57 cx.add_action(ConnectedView::paste);
58 cx.add_action(ConnectedView::clear);
59 cx.add_action(ConnectedView::show_character_palette);
60}
61
62///A terminal view, maintains the PTY's file handles and communicates with the terminal
63pub struct ConnectedView {
64 terminal: ModelHandle<Terminal>,
65 has_new_content: bool,
66 //Currently using iTerm bell, show bell emoji in tab until input is received
67 has_bell: bool,
68 // Only for styling purposes. Doesn't effect behavior
69 modal: bool,
70 context_menu: ViewHandle<ContextMenu>,
71 blink_state: bool,
72 blinking_on: bool,
73 blinking_paused: bool,
74 blink_epoch: usize,
75}
76
77impl ConnectedView {
78 pub fn from_terminal(
79 terminal: ModelHandle<Terminal>,
80 modal: bool,
81 cx: &mut ViewContext<Self>,
82 ) -> Self {
83 cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
84 cx.subscribe(&terminal, |this, _, event, cx| match event {
85 Event::Wakeup => {
86 if !cx.is_self_focused() {
87 this.has_new_content = true;
88 cx.notify();
89 cx.emit(Event::Wakeup);
90 }
91 }
92 Event::Bell => {
93 this.has_bell = true;
94 cx.emit(Event::Wakeup);
95 }
96 Event::BlinkChanged => this.blinking_on = !this.blinking_on,
97 _ => cx.emit(*event),
98 })
99 .detach();
100
101 Self {
102 terminal,
103 has_new_content: true,
104 has_bell: false,
105 modal,
106 context_menu: cx.add_view(ContextMenu::new),
107 blink_state: true,
108 blinking_on: false,
109 blinking_paused: false,
110 blink_epoch: 0,
111 }
112 }
113
114 pub fn handle(&self) -> ModelHandle<Terminal> {
115 self.terminal.clone()
116 }
117
118 pub fn has_new_content(&self) -> bool {
119 self.has_new_content
120 }
121
122 pub fn has_bell(&self) -> bool {
123 self.has_bell
124 }
125
126 pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
127 self.has_bell = false;
128 cx.emit(Event::Wakeup);
129 }
130
131 pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
132 let menu_entries = vec![
133 ContextMenuItem::item("Clear Buffer", Clear),
134 ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
135 ];
136
137 self.context_menu
138 .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
139
140 cx.notify();
141 }
142
143 fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
144 if !self
145 .terminal
146 .read(cx)
147 .last_mode
148 .contains(TermMode::ALT_SCREEN)
149 {
150 cx.show_character_palette();
151 } else {
152 self.terminal.update(cx, |term, _| {
153 term.try_keystroke(&Keystroke::parse("ctrl-cmd-space").unwrap())
154 });
155 }
156 }
157
158 fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
159 self.terminal.update(cx, |term, _| term.clear());
160 cx.notify();
161 }
162
163 pub fn should_show_cursor(
164 &self,
165 focused: bool,
166 cx: &mut gpui::RenderContext<'_, Self>,
167 ) -> bool {
168 //Don't blink the cursor when not focused, blinking is disabled, or paused
169 if !focused
170 || !self.blinking_on
171 || self.blinking_paused
172 || self
173 .terminal
174 .read(cx)
175 .last_mode
176 .contains(TermMode::ALT_SCREEN)
177 {
178 return true;
179 }
180
181 let setting = {
182 let settings = cx.global::<Settings>();
183 settings
184 .terminal_overrides
185 .blinking
186 .clone()
187 .unwrap_or(TerminalBlink::TerminalControlled)
188 };
189
190 match setting {
191 //If the user requested to never blink, don't blink it.
192 TerminalBlink::Off => true,
193 //If the terminal is controlling it, check terminal mode
194 TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
195 }
196 }
197
198 fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
199 if epoch == self.blink_epoch && !self.blinking_paused {
200 self.blink_state = !self.blink_state;
201 cx.notify();
202
203 let epoch = self.next_blink_epoch();
204 cx.spawn(|this, mut cx| {
205 let this = this.downgrade();
206 async move {
207 Timer::after(CURSOR_BLINK_INTERVAL).await;
208 if let Some(this) = this.upgrade(&cx) {
209 this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
210 }
211 }
212 })
213 .detach();
214 }
215 }
216
217 pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
218 self.blink_state = true;
219 cx.notify();
220
221 let epoch = self.next_blink_epoch();
222 cx.spawn(|this, mut cx| {
223 let this = this.downgrade();
224 async move {
225 Timer::after(CURSOR_BLINK_INTERVAL).await;
226 if let Some(this) = this.upgrade(&cx) {
227 this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
228 }
229 }
230 })
231 .detach();
232 }
233
234 fn next_blink_epoch(&mut self) -> usize {
235 self.blink_epoch += 1;
236 self.blink_epoch
237 }
238
239 fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
240 if epoch == self.blink_epoch {
241 self.blinking_paused = false;
242 self.blink_cursors(epoch, cx);
243 }
244 }
245
246 ///Attempt to paste the clipboard into the terminal
247 fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
248 self.terminal.update(cx, |term, _| term.copy())
249 }
250
251 ///Attempt to paste the clipboard into the terminal
252 fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
253 if let Some(item) = cx.read_from_clipboard() {
254 self.terminal
255 .update(cx, |terminal, _cx| terminal.paste(item.text()));
256 }
257 }
258
259 ///Synthesize the keyboard event corresponding to 'up'
260 fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
261 self.clear_bel(cx);
262 self.terminal.update(cx, |term, _| {
263 term.try_keystroke(&Keystroke::parse("up").unwrap())
264 });
265 }
266
267 ///Synthesize the keyboard event corresponding to 'down'
268 fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
269 self.clear_bel(cx);
270 self.terminal.update(cx, |term, _| {
271 term.try_keystroke(&Keystroke::parse("down").unwrap())
272 });
273 }
274
275 ///Synthesize the keyboard event corresponding to 'ctrl-c'
276 fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
277 self.clear_bel(cx);
278 self.terminal.update(cx, |term, _| {
279 term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())
280 });
281 }
282
283 ///Synthesize the keyboard event corresponding to 'escape'
284 fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
285 self.clear_bel(cx);
286 self.terminal.update(cx, |term, _| {
287 term.try_keystroke(&Keystroke::parse("escape").unwrap())
288 });
289 }
290
291 ///Synthesize the keyboard event corresponding to 'enter'
292 fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
293 self.clear_bel(cx);
294 self.terminal.update(cx, |term, _| {
295 term.try_keystroke(&Keystroke::parse("enter").unwrap())
296 });
297 }
298}
299
300impl View for ConnectedView {
301 fn ui_name() -> &'static str {
302 "Terminal"
303 }
304
305 fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
306 let terminal_handle = self.terminal.clone().downgrade();
307
308 let self_id = cx.view_id();
309 let focused = cx
310 .focused_view_id(cx.window_id())
311 .filter(|view_id| *view_id == self_id)
312 .is_some();
313
314 Stack::new()
315 .with_child(
316 TerminalEl::new(
317 cx.handle(),
318 terminal_handle,
319 self.modal,
320 focused,
321 self.should_show_cursor(focused, cx),
322 )
323 .contained()
324 .boxed(),
325 )
326 .with_child(ChildView::new(&self.context_menu).boxed())
327 .boxed()
328 }
329
330 fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
331 self.has_new_content = false;
332 self.terminal.read(cx).focus_in();
333 self.blink_cursors(self.blink_epoch, cx);
334 cx.notify();
335 }
336
337 fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
338 self.terminal.read(cx).focus_out();
339 cx.notify();
340 }
341
342 //IME stuff
343 fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
344 if self
345 .terminal
346 .read(cx)
347 .last_mode
348 .contains(TermMode::ALT_SCREEN)
349 {
350 None
351 } else {
352 Some(0..0)
353 }
354 }
355
356 fn replace_text_in_range(
357 &mut self,
358 _: Option<std::ops::Range<usize>>,
359 text: &str,
360 cx: &mut ViewContext<Self>,
361 ) {
362 self.terminal.update(cx, |terminal, _| {
363 terminal.input(text.into());
364 });
365 }
366
367 fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
368 let mut context = Self::default_keymap_context();
369 if self.modal {
370 context.set.insert("ModalTerminal".into());
371 }
372 let mode = self.terminal.read(cx).last_mode;
373 context.map.insert(
374 "screen".to_string(),
375 (if mode.contains(TermMode::ALT_SCREEN) {
376 "alt"
377 } else {
378 "normal"
379 })
380 .to_string(),
381 );
382
383 if mode.contains(TermMode::APP_CURSOR) {
384 context.set.insert("DECCKM".to_string());
385 }
386 if mode.contains(TermMode::APP_KEYPAD) {
387 context.set.insert("DECPAM".to_string());
388 }
389 //Note the ! here
390 if !mode.contains(TermMode::APP_KEYPAD) {
391 context.set.insert("DECPNM".to_string());
392 }
393 if mode.contains(TermMode::SHOW_CURSOR) {
394 context.set.insert("DECTCEM".to_string());
395 }
396 if mode.contains(TermMode::LINE_WRAP) {
397 context.set.insert("DECAWM".to_string());
398 }
399 if mode.contains(TermMode::ORIGIN) {
400 context.set.insert("DECOM".to_string());
401 }
402 if mode.contains(TermMode::INSERT) {
403 context.set.insert("IRM".to_string());
404 }
405 //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
406 if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
407 context.set.insert("LNM".to_string());
408 }
409 if mode.contains(TermMode::FOCUS_IN_OUT) {
410 context.set.insert("report_focus".to_string());
411 }
412 if mode.contains(TermMode::ALTERNATE_SCROLL) {
413 context.set.insert("alternate_scroll".to_string());
414 }
415 if mode.contains(TermMode::BRACKETED_PASTE) {
416 context.set.insert("bracketed_paste".to_string());
417 }
418 if mode.intersects(TermMode::MOUSE_MODE) {
419 context.set.insert("any_mouse_reporting".to_string());
420 }
421 {
422 let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
423 "click"
424 } else if mode.contains(TermMode::MOUSE_DRAG) {
425 "drag"
426 } else if mode.contains(TermMode::MOUSE_MOTION) {
427 "motion"
428 } else {
429 "off"
430 };
431 context
432 .map
433 .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
434 }
435 {
436 let format = if mode.contains(TermMode::SGR_MOUSE) {
437 "sgr"
438 } else if mode.contains(TermMode::UTF8_MOUSE) {
439 "utf8"
440 } else {
441 "normal"
442 };
443 context
444 .map
445 .insert("mouse_format".to_string(), format.to_string());
446 }
447 context
448 }
449}