1use std::cell::RefCell;
4use std::rc::Rc;
5
6use rich::Style;
7use rich_intuituive::interact::{Key, KeyCode};
8use rich_intuituive::widget::{Canvas, DrawCx, EventCx, Used, Widget, WidgetEvent, widget};
9use rich_intuituive::{Ctx, Node, Signal, signal};
10
11use crate::host::{Command, ExitStatus, LocalPty, Notify, PtyHost};
12use crate::term::TermCore;
13use crate::wake::{Fed, connect};
14
15pub const DEFAULT_SCROLLBACK: usize = 1000;
17
18type ExitHandler = Box<dyn FnMut(ExitStatus, &mut Ctx)>;
19
20struct PaneState {
21 core: TermCore,
22 wake: Signal<u64>,
24 status: Signal<Option<ExitStatus>>,
25 on_exit: Option<ExitHandler>,
26}
27
28impl Fed for PaneState {
29 fn install(&mut self, notify: Notify) {
30 self.core.set_notify(notify);
31 }
32
33 fn pump(this: &Rc<RefCell<Self>>, cx: &mut Ctx) {
34 let (pumped, wake, status) = {
35 let mut state = this.borrow_mut();
36 (state.core.pump(), state.wake, state.status)
37 };
38 if pumped.output || pumped.exit.is_some() {
39 wake.update(|n| *n = n.wrapping_add(1));
40 }
41 if let Some(exit) = pumped.exit {
42 status.set(Some(exit.clone()));
43 let handler = this.borrow_mut().on_exit.take();
44 if let Some(mut handler) = handler {
45 handler(exit, cx);
46 }
47 }
48 }
49}
50
51pub struct TerminalPane {
66 state: Rc<RefCell<PaneState>>,
67 release: Vec<Key>,
68}
69
70pub fn terminal(command: impl Into<Command>) -> TerminalPane {
91 terminal_with(LocalPty::new(command))
92}
93
94pub fn terminal_with(host: impl PtyHost + 'static) -> TerminalPane {
100 let state = Rc::new(RefCell::new(PaneState {
101 core: TermCore::new(Box::new(host), DEFAULT_SCROLLBACK),
102 wake: signal(0),
103 status: signal(None),
104 on_exit: None,
105 }));
106 connect(&state);
107 TerminalPane {
108 state,
109 release: Vec::new(),
111 }
112}
113
114impl TerminalPane {
115 #[must_use]
118 pub fn on_exit(self, handler: impl FnMut(ExitStatus, &mut Ctx) + 'static) -> TerminalPane {
119 self.state.borrow_mut().on_exit = Some(Box::new(handler));
120 self
121 }
122
123 pub fn status(&self) -> Signal<Option<ExitStatus>> {
126 self.state.borrow().status
127 }
128
129 #[must_use]
132 pub fn scrollback(self, rows: usize) -> TerminalPane {
133 self.state.borrow_mut().core.set_scrollback(rows);
134 self
135 }
136
137 #[must_use]
141 pub fn release_keys(mut self, keys: &str) -> TerminalPane {
142 self.release
143 .extend(keys.split_whitespace().filter_map(Key::parse));
144 self
145 }
146
147 pub fn node(self) -> Node {
150 widget(PaneWidget {
151 state: self.state,
152 release: self.release,
153 caret: None,
154 })
155 .on_key("ctrl+c", |cx| cx.quit())
159 }
160}
161
162impl From<TerminalPane> for Node {
163 fn from(pane: TerminalPane) -> Node {
164 pane.node()
165 }
166}
167
168struct PaneWidget {
169 state: Rc<RefCell<PaneState>>,
170 release: Vec<Key>,
171 caret: Option<(u16, u16)>,
172}
173
174impl Widget for PaneWidget {
175 fn name(&self) -> &'static str {
176 "terminal"
177 }
178
179 fn describe(&self) -> Option<String> {
180 let state = self.state.borrow();
181 Some(match &state.core.exit {
182 Some(status) => status.to_string(),
183 None if state.core.started() => "running".into(),
184 None => "not started".into(),
185 })
186 }
187
188 fn draw(&mut self, cx: &mut DrawCx, canvas: &mut Canvas) {
189 let mut state = self.state.borrow_mut();
190 state.wake.get();
191 let (width, height) = (canvas.width(), canvas.height());
192 state.core.fit(width, height);
193 if let Some(error) = state.core.error.clone() {
194 let style = cx.style("error", "red");
195 canvas.markup(cx.console(), 0, 0, width, &escape(&error), Some(&style));
196 self.caret = None;
197 return;
198 }
199 let lines = state.core.lines();
200 canvas.lines(&lines);
201 let back = state.core.scrolled();
202 if back > 0 {
203 let note = format!(" ↑{back} ");
204 let w = rich::cells::cell_len(¬e) as u16;
205 let style = Style::parse("reverse").unwrap_or_default();
206 canvas.print(width.saturating_sub(w), 0, ¬e, Some(&style));
207 }
208 self.caret = state.core.cursor();
209 }
210
211 fn event(&mut self, cx: &mut EventCx, event: &WidgetEvent) -> Used {
212 let mut state = self.state.borrow_mut();
213 if state.core.exit.is_some() || state.core.error.is_some() || !state.core.started() {
216 return Used::No;
217 }
218 match event {
219 WidgetEvent::Key(key) => {
220 if self.release.contains(key) {
221 return Used::No;
222 }
223 let page = cx.size().1.max(1) as isize;
224 match (key.code, key.modifiers.shift) {
225 (KeyCode::PageUp, true) => state.core.scroll_by(page),
226 (KeyCode::PageDown, true) => state.core.scroll_by(-page),
227 _ => state.core.key(*key),
228 }
229 cx.redraw();
230 Used::Yes
231 }
232 WidgetEvent::Paste(text) => {
233 state.core.paste(text);
234 cx.redraw();
235 Used::Yes
236 }
237 WidgetEvent::Mouse(mouse) => {
238 if !state.core.mouse(*mouse) {
239 return Used::No;
240 }
241 if state.core.wants_mouse() {
242 if mouse.kind
243 == rich_intuituive::interact::MouseKind::Down(
244 rich_intuituive::interact::Button::Left,
245 )
246 {
247 cx.capture_mouse();
248 }
249 } else {
250 cx.redraw();
251 }
252 Used::Yes
253 }
254 _ => Used::No,
255 }
256 }
257
258 fn focusable(&self) -> bool {
259 true
260 }
261
262 fn caret(&self) -> Option<(u16, u16)> {
263 self.caret
264 }
265}
266
267fn escape(text: &str) -> String {
269 text.replace('[', "\\[")
270}