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