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