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