connected_view.rs

  1use alacritty_terminal::term::TermMode;
  2use context_menu::{ContextMenu, ContextMenuItem};
  3use gpui::{
  4    actions,
  5    elements::{ChildView, ParentElement, Stack},
  6    geometry::vector::Vector2F,
  7    impl_internal_actions,
  8    keymap::Keystroke,
  9    AppContext, Element, ElementBox, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
 10};
 11use workspace::pane;
 12
 13use crate::{connected_el::TerminalEl, Event, Terminal};
 14
 15///Event to transmit the scroll from the element to the view
 16#[derive(Clone, Debug, PartialEq)]
 17pub struct ScrollTerminal(pub i32);
 18
 19#[derive(Clone, PartialEq)]
 20pub struct DeployContextMenu {
 21    pub position: Vector2F,
 22}
 23
 24actions!(
 25    terminal,
 26    [Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,]
 27);
 28impl_internal_actions!(project_panel, [DeployContextMenu]);
 29
 30pub fn init(cx: &mut MutableAppContext) {
 31    //Global binding overrrides
 32    cx.add_action(ConnectedView::ctrl_c);
 33    cx.add_action(ConnectedView::up);
 34    cx.add_action(ConnectedView::down);
 35    cx.add_action(ConnectedView::escape);
 36    cx.add_action(ConnectedView::enter);
 37    //Useful terminal views
 38    cx.add_action(ConnectedView::deploy_context_menu);
 39    cx.add_action(ConnectedView::copy);
 40    cx.add_action(ConnectedView::paste);
 41    cx.add_action(ConnectedView::clear);
 42}
 43
 44///A terminal view, maintains the PTY's file handles and communicates with the terminal
 45pub struct ConnectedView {
 46    terminal: ModelHandle<Terminal>,
 47    has_new_content: bool,
 48    //Currently using iTerm bell, show bell emoji in tab until input is received
 49    has_bell: bool,
 50    // Only for styling purposes. Doesn't effect behavior
 51    modal: bool,
 52    context_menu: ViewHandle<ContextMenu>,
 53}
 54
 55impl ConnectedView {
 56    pub fn from_terminal(
 57        terminal: ModelHandle<Terminal>,
 58        modal: bool,
 59        cx: &mut ViewContext<Self>,
 60    ) -> Self {
 61        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
 62        cx.subscribe(&terminal, |this, _, event, cx| match event {
 63            Event::Wakeup => {
 64                if !cx.is_self_focused() {
 65                    this.has_new_content = true;
 66                    cx.notify();
 67                    cx.emit(Event::Wakeup);
 68                }
 69            }
 70            Event::Bell => {
 71                this.has_bell = true;
 72                cx.emit(Event::Wakeup);
 73            }
 74
 75            _ => cx.emit(*event),
 76        })
 77        .detach();
 78
 79        Self {
 80            terminal,
 81            has_new_content: true,
 82            has_bell: false,
 83            modal,
 84            context_menu: cx.add_view(|cx| ContextMenu::new(cx)),
 85        }
 86    }
 87
 88    pub fn handle(&self) -> ModelHandle<Terminal> {
 89        self.terminal.clone()
 90    }
 91
 92    pub fn has_new_content(&self) -> bool {
 93        self.has_new_content
 94    }
 95
 96    pub fn has_bell(&self) -> bool {
 97        self.has_bell
 98    }
 99
100    pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
101        self.has_bell = false;
102        cx.emit(Event::Wakeup);
103    }
104
105    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
106        let menu_entries = vec![
107            ContextMenuItem::item("Clear Buffer", Clear),
108            ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
109        ];
110
111        self.context_menu
112            .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
113
114        cx.notify();
115    }
116
117    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
118        self.terminal.update(cx, |term, _| term.clear());
119        cx.notify();
120    }
121
122    ///Attempt to paste the clipboard into the terminal
123    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
124        self.terminal.update(cx, |term, _| term.copy())
125    }
126
127    ///Attempt to paste the clipboard into the terminal
128    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
129        cx.read_from_clipboard().map(|item| {
130            self.terminal.read(cx).paste(item.text());
131        });
132    }
133
134    ///Synthesize the keyboard event corresponding to 'up'
135    fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
136        self.clear_bel(cx);
137        self.terminal
138            .read(cx)
139            .try_keystroke(&Keystroke::parse("up").unwrap());
140    }
141
142    ///Synthesize the keyboard event corresponding to 'down'
143    fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
144        self.clear_bel(cx);
145        self.terminal
146            .read(cx)
147            .try_keystroke(&Keystroke::parse("down").unwrap());
148    }
149
150    ///Synthesize the keyboard event corresponding to 'ctrl-c'
151    fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
152        self.clear_bel(cx);
153        self.terminal
154            .read(cx)
155            .try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
156    }
157
158    ///Synthesize the keyboard event corresponding to 'escape'
159    fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
160        self.clear_bel(cx);
161        self.terminal
162            .read(cx)
163            .try_keystroke(&Keystroke::parse("escape").unwrap());
164    }
165
166    ///Synthesize the keyboard event corresponding to 'enter'
167    fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
168        self.clear_bel(cx);
169        self.terminal
170            .read(cx)
171            .try_keystroke(&Keystroke::parse("enter").unwrap());
172    }
173}
174
175impl View for ConnectedView {
176    fn ui_name() -> &'static str {
177        "Terminal"
178    }
179
180    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
181        let terminal_handle = self.terminal.clone().downgrade();
182
183        Stack::new()
184            .with_child(
185                TerminalEl::new(cx.handle(), terminal_handle, self.modal)
186                    .contained()
187                    .boxed(),
188            )
189            .with_child(ChildView::new(&self.context_menu).boxed())
190            .boxed()
191    }
192
193    fn on_focus(&mut self, _cx: &mut ViewContext<Self>) {
194        self.has_new_content = false;
195    }
196
197    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
198        if self
199            .terminal
200            .read(cx)
201            .last_mode
202            .contains(TermMode::ALT_SCREEN)
203        {
204            None
205        } else {
206            Some(0..0)
207        }
208    }
209
210    fn replace_text_in_range(
211        &mut self,
212        _: Option<std::ops::Range<usize>>,
213        text: &str,
214        cx: &mut ViewContext<Self>,
215    ) {
216        self.terminal
217            .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
218    }
219
220    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
221        let mut context = Self::default_keymap_context();
222        if self.modal {
223            context.set.insert("ModalTerminal".into());
224        }
225        context
226    }
227}