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