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