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