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