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