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