terminal_view.rs

  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}