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, ModelHandle, MutableAppContext, View,
12 ViewContext, ViewHandle,
13};
14use smol::Timer;
15use workspace::pane;
16
17use crate::{connected_el::TerminalEl, Event, Terminal};
18
19const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
20
21///Event to transmit the scroll from the element to the view
22#[derive(Clone, Debug, PartialEq)]
23pub struct ScrollTerminal(pub i32);
24
25#[derive(Clone, PartialEq)]
26pub struct DeployContextMenu {
27 pub position: Vector2F,
28}
29
30actions!(
31 terminal,
32 [Up, Down, CtrlC, Escape, Enter, Clear, Copy, Paste,]
33);
34impl_internal_actions!(project_panel, [DeployContextMenu]);
35
36pub fn init(cx: &mut MutableAppContext) {
37 //Global binding overrrides
38 cx.add_action(ConnectedView::ctrl_c);
39 cx.add_action(ConnectedView::up);
40 cx.add_action(ConnectedView::down);
41 cx.add_action(ConnectedView::escape);
42 cx.add_action(ConnectedView::enter);
43 //Useful terminal views
44 cx.add_action(ConnectedView::deploy_context_menu);
45 cx.add_action(ConnectedView::copy);
46 cx.add_action(ConnectedView::paste);
47 cx.add_action(ConnectedView::clear);
48}
49
50///A terminal view, maintains the PTY's file handles and communicates with the terminal
51pub struct ConnectedView {
52 terminal: ModelHandle<Terminal>,
53 has_new_content: bool,
54 //Currently using iTerm bell, show bell emoji in tab until input is received
55 has_bell: bool,
56 // Only for styling purposes. Doesn't effect behavior
57 modal: bool,
58 context_menu: ViewHandle<ContextMenu>,
59 show_cursor: bool,
60 blinking_paused: bool,
61 blink_epoch: usize,
62}
63
64impl ConnectedView {
65 pub fn from_terminal(
66 terminal: ModelHandle<Terminal>,
67 modal: bool,
68 cx: &mut ViewContext<Self>,
69 ) -> Self {
70 cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
71 cx.subscribe(&terminal, |this, _, event, cx| match event {
72 Event::Wakeup => {
73 if !cx.is_self_focused() {
74 this.has_new_content = true;
75 cx.notify();
76 cx.emit(Event::Wakeup);
77 }
78 }
79 Event::Bell => {
80 this.has_bell = true;
81 cx.emit(Event::Wakeup);
82 }
83
84 _ => cx.emit(*event),
85 })
86 .detach();
87
88 Self {
89 terminal,
90 has_new_content: true,
91 has_bell: false,
92 modal,
93 context_menu: cx.add_view(ContextMenu::new),
94 show_cursor: true,
95 blinking_paused: false,
96 blink_epoch: 0,
97 }
98 }
99
100 pub fn handle(&self) -> ModelHandle<Terminal> {
101 self.terminal.clone()
102 }
103
104 pub fn has_new_content(&self) -> bool {
105 self.has_new_content
106 }
107
108 pub fn has_bell(&self) -> bool {
109 self.has_bell
110 }
111
112 pub fn clear_bel(&mut self, cx: &mut ViewContext<ConnectedView>) {
113 self.has_bell = false;
114 cx.emit(Event::Wakeup);
115 }
116
117 pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
118 let menu_entries = vec![
119 ContextMenuItem::item("Clear Buffer", Clear),
120 ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
121 ];
122
123 self.context_menu
124 .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
125
126 cx.notify();
127 }
128
129 fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
130 self.terminal.update(cx, |term, _| term.clear());
131 cx.notify();
132 }
133
134 //Following code copied from editor cursor
135 pub fn blink_show(&self) -> bool {
136 self.blinking_paused || self.show_cursor
137 }
138
139 fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
140 if epoch == self.blink_epoch && !self.blinking_paused {
141 self.show_cursor = !self.show_cursor;
142 cx.notify();
143
144 let epoch = self.next_blink_epoch();
145 cx.spawn(|this, mut cx| {
146 let this = this.downgrade();
147 async move {
148 Timer::after(CURSOR_BLINK_INTERVAL).await;
149 if let Some(this) = this.upgrade(&cx) {
150 this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
151 }
152 }
153 })
154 .detach();
155 }
156 }
157
158 pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
159 self.show_cursor = true;
160 cx.notify();
161
162 let epoch = self.next_blink_epoch();
163 cx.spawn(|this, mut cx| {
164 let this = this.downgrade();
165 async move {
166 Timer::after(CURSOR_BLINK_INTERVAL).await;
167 if let Some(this) = this.upgrade(&cx) {
168 this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
169 }
170 }
171 })
172 .detach();
173 }
174
175 fn next_blink_epoch(&mut self) -> usize {
176 self.blink_epoch += 1;
177 self.blink_epoch
178 }
179
180 fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
181 if epoch == self.blink_epoch {
182 self.blinking_paused = false;
183 self.blink_cursors(epoch, cx);
184 }
185 }
186
187 ///Attempt to paste the clipboard into the terminal
188 fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
189 self.terminal.update(cx, |term, _| term.copy())
190 }
191
192 ///Attempt to paste the clipboard into the terminal
193 fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
194 if let Some(item) = cx.read_from_clipboard() {
195 self.terminal.read(cx).paste(item.text());
196 }
197 }
198
199 ///Synthesize the keyboard event corresponding to 'up'
200 fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
201 self.clear_bel(cx);
202 self.terminal
203 .read(cx)
204 .try_keystroke(&Keystroke::parse("up").unwrap());
205 }
206
207 ///Synthesize the keyboard event corresponding to 'down'
208 fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
209 self.clear_bel(cx);
210 self.terminal
211 .read(cx)
212 .try_keystroke(&Keystroke::parse("down").unwrap());
213 }
214
215 ///Synthesize the keyboard event corresponding to 'ctrl-c'
216 fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
217 self.clear_bel(cx);
218 self.terminal
219 .read(cx)
220 .try_keystroke(&Keystroke::parse("ctrl-c").unwrap());
221 }
222
223 ///Synthesize the keyboard event corresponding to 'escape'
224 fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
225 self.clear_bel(cx);
226 self.terminal
227 .read(cx)
228 .try_keystroke(&Keystroke::parse("escape").unwrap());
229 }
230
231 ///Synthesize the keyboard event corresponding to 'enter'
232 fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
233 self.clear_bel(cx);
234 self.terminal
235 .read(cx)
236 .try_keystroke(&Keystroke::parse("enter").unwrap());
237 }
238}
239
240impl View for ConnectedView {
241 fn ui_name() -> &'static str {
242 "Terminal"
243 }
244
245 fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
246 let terminal_handle = self.terminal.clone().downgrade();
247
248 let self_id = cx.view_id();
249 let focused = cx
250 .focused_view_id(cx.window_id())
251 .filter(|view_id| *view_id == self_id)
252 .is_some();
253
254 Stack::new()
255 .with_child(
256 TerminalEl::new(
257 cx.handle(),
258 terminal_handle,
259 self.modal,
260 focused,
261 self.blink_show(),
262 )
263 .contained()
264 .boxed(),
265 )
266 .with_child(ChildView::new(&self.context_menu).boxed())
267 .boxed()
268 }
269
270 fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
271 self.has_new_content = false;
272 self.terminal.read(cx).focus_in();
273 self.blink_cursors(self.blink_epoch, cx);
274 cx.notify();
275 }
276
277 fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
278 self.terminal.read(cx).focus_out();
279 cx.notify();
280 }
281
282 //IME stuff
283 fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
284 if self
285 .terminal
286 .read(cx)
287 .last_mode
288 .contains(TermMode::ALT_SCREEN)
289 {
290 None
291 } else {
292 Some(0..0)
293 }
294 }
295
296 fn replace_text_in_range(
297 &mut self,
298 _: Option<std::ops::Range<usize>>,
299 text: &str,
300 cx: &mut ViewContext<Self>,
301 ) {
302 self.terminal
303 .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
304 }
305
306 fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
307 let mut context = Self::default_keymap_context();
308 if self.modal {
309 context.set.insert("ModalTerminal".into());
310 }
311 context
312 }
313}