terminal_view.rs

  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}