1use std::io::Write;
2use std::path::PathBuf;
3use std::sync::{Arc, Mutex, PoisonError};
4use std::thread::JoinHandle;
5
6use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
7use log::info;
8use portable_pty::{
9 ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem,
10};
11use ratatui::prelude::*;
12use ratatui::symbols::border;
13use ratatui::widgets::{Block, Clear, Widget};
14use tui_term::widget::PseudoTerminal;
15use vt100_ctt::Parser;
16
17use crate::error::{CarchError, Result};
18use crate::ui::theme::Theme;
19
20const SCROLLBACK_LEN: usize = 1000;
21const PAGE_STEP: usize = 10;
22
23type ChildKillerSender = oneshot::Sender<Option<Box<dyn ChildKiller + Send + Sync>>>;
24type ChildKillerReceiver = oneshot::Receiver<Option<Box<dyn ChildKiller + Send + Sync>>>;
25
26pub enum PopupEvent {
27 Close,
28 None,
29}
30
31pub struct RunScriptPopup {
32 buffer: Arc<Mutex<Vec<u8>>>,
33 command_thread: Option<JoinHandle<ExitStatus>>,
34 child_killer: Option<ChildKillerReceiver>,
35 pty_master: Box<dyn MasterPty + Send>,
36 writer: Box<dyn Write + Send>,
37 status: Option<ExitStatus>,
38 parser: Option<Parser>,
39 parser_size: (u16, u16),
40 pty_size: (u16, u16),
41 processed_len: usize,
42 last_max_check: usize,
43 scroll_offset: usize,
44 actual_max: usize,
45 auto_scroll: bool,
46 theme: Theme,
47 was_finished: bool,
48}
49
50impl RunScriptPopup {
51 pub fn new(script_path: PathBuf, log_mode: bool, theme: Theme) -> Result<Self> {
52 let pty_system = NativePtySystem::default();
53
54 let mut cmd = CommandBuilder::new("bash");
55 cmd.arg(script_path);
56
57 let pair = pty_system
58 .openpty(PtySize {
59 rows: 24,
60 cols: 80,
61 pixel_width: 0,
62 pixel_height: 0,
63 })
64 .map_err(|e| CarchError::Pty(format!("openpty: {e}")))?;
65
66 let (tx, rx): (ChildKillerSender, ChildKillerReceiver) = oneshot::channel();
67 let pair_slave = pair.slave;
68 let command_handle = std::thread::spawn(move || -> ExitStatus {
69 match pair_slave.spawn_command(cmd) {
70 Ok(mut child) => {
71 let killer = child.clone_killer();
72 if tx.send(Some(killer)).is_err() {
73 log::warn!("child_killer receiver dropped before killer was sent");
74 }
75 child.wait().unwrap_or_else(|e| {
76 log::error!("child.wait failed: {e}");
77 ExitStatus::with_exit_code(1)
78 })
79 }
80 Err(e) => {
81 log::error!("spawn_command failed: {e}");
82 if tx.send(None).is_err() {
83 log::warn!("child_killer receiver dropped before None was sent");
84 }
85 ExitStatus::with_exit_code(1)
86 }
87 }
88 });
89
90 let mut reader = pair
91 .master
92 .try_clone_reader()
93 .map_err(|e| CarchError::Pty(format!("try_clone_reader: {e}")))?;
94
95 let command_buffer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
96 {
97 let command_buffer = command_buffer.clone();
98 std::thread::spawn(move || {
99 let mut buf = [0u8; 16384];
100 while let Ok(size) = reader.read(&mut buf) {
101 if size == 0 {
102 break;
103 }
104 let mut mutex = match command_buffer.lock() {
105 Ok(g) => g,
106 Err(p) => p.into_inner(),
107 };
108 let data = &buf[0..size];
109 if log_mode {
110 info!("{}", String::from_utf8_lossy(data));
111 }
112 mutex.extend_from_slice(data);
113 }
114 });
115 }
116
117 let writer =
118 pair.master.take_writer().map_err(|e| CarchError::Pty(format!("take_writer: {e}")))?;
119
120 Ok(Self {
121 buffer: command_buffer,
122 command_thread: Some(command_handle),
123 child_killer: Some(rx),
124 pty_master: pair.master,
125 writer,
126 status: None,
127 parser: None,
128 parser_size: (0, 0),
129 pty_size: (0, 0),
130 processed_len: 0,
131 last_max_check: 0,
132 scroll_offset: 0,
133 actual_max: 0,
134 auto_scroll: true,
135 theme,
136 was_finished: false,
137 })
138 }
139
140 pub fn has_new_data(&mut self) -> bool {
141 let now_finished = self.is_finished();
142 if now_finished != self.was_finished {
143 return true;
144 }
145 if self.auto_scroll
146 && let Ok(buf) = self.buffer.try_lock()
147 && buf.len() > self.processed_len
148 {
149 return true;
150 }
151 false
152 }
153
154 pub fn acknowledge_data(&mut self) {
155 self.was_finished = self.is_finished();
156 }
157
158 pub fn handle_key_event(&mut self, key: KeyEvent) -> PopupEvent {
159 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
160 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
161
162 match key.code {
163 KeyCode::Char('c') if ctrl => {
164 let _ = self.writer.write_all(&[3]);
165 PopupEvent::None
166 }
167 KeyCode::Char('q') | KeyCode::Enter | KeyCode::Esc if self.is_finished() => {
168 PopupEvent::Close
169 }
170 KeyCode::Up if shift => {
171 self.scroll_up(1);
172 PopupEvent::None
173 }
174 KeyCode::Down if shift => {
175 self.scroll_down(1);
176 PopupEvent::None
177 }
178 KeyCode::PageUp => {
179 self.scroll_up(PAGE_STEP);
180 PopupEvent::None
181 }
182 KeyCode::PageDown => {
183 self.scroll_down(PAGE_STEP);
184 PopupEvent::None
185 }
186 KeyCode::Char('g') if !ctrl => {
187 self.scroll_to_top();
188 PopupEvent::None
189 }
190 KeyCode::Char('G') => {
191 self.scroll_to_bottom();
192 PopupEvent::None
193 }
194 _ => {
195 self.handle_passthrough_key_event(key);
196 PopupEvent::None
197 }
198 }
199 }
200
201 fn scroll_up(&mut self, n: usize) {
202 if n == 0 {
203 return;
204 }
205 let max = self.max_scrollback();
206 let next = self.scroll_offset.saturating_add(n).min(max);
207 self.scroll_offset = next;
208 self.auto_scroll = next == 0;
209 }
210
211 fn scroll_down(&mut self, n: usize) {
212 if n == 0 {
213 return;
214 }
215 if self.scroll_offset > n {
216 self.scroll_offset -= n;
217 } else {
218 self.scroll_offset = 0;
219 }
220 self.auto_scroll = self.scroll_offset == 0;
221 }
222
223 fn scroll_to_top(&mut self) {
224 let max = self.max_scrollback();
225 self.scroll_offset = max;
226 self.auto_scroll = max == 0;
227 }
228
229 fn scroll_to_bottom(&mut self) {
230 self.scroll_offset = 0;
231 self.auto_scroll = true;
232 }
233
234 fn max_scrollback(&self) -> usize {
235 self.actual_max
236 }
237
238 fn is_finished(&self) -> bool {
239 self.command_thread.as_ref().is_none_or(JoinHandle::is_finished)
240 }
241
242 fn sync_parser(&mut self, size: Size) {
243 let (cols, rows) = (size.width, size.height);
244
245 if self.pty_size != (cols, rows) {
246 let _ = self.pty_master.resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 });
247 self.pty_size = (cols, rows);
248 }
249
250 let parser = self.parser.get_or_insert_with(|| Parser::new(rows, cols, SCROLLBACK_LEN));
251
252 if self.parser_size != (cols, rows) {
253 parser.screen_mut().set_size(rows, cols);
254 self.parser_size = (cols, rows);
255 }
256
257 let new_bytes: Vec<u8> = {
258 let buf = self.buffer.lock().unwrap_or_else(PoisonError::into_inner);
259 if self.processed_len <= buf.len() {
260 buf[self.processed_len..].to_vec()
261 } else {
262 self.processed_len = 0;
263 buf[..].to_vec()
264 }
265 };
266 let got_new = !new_bytes.is_empty();
267 if got_new {
268 parser.process(&new_bytes);
269 self.processed_len += new_bytes.len();
270 }
271
272 if got_new || self.last_max_check == 0 {
273 parser.screen_mut().set_scrollback(usize::MAX);
274 self.actual_max = parser.screen().scrollback();
275 self.last_max_check = self.processed_len;
276 }
277
278 if self.scroll_offset > self.actual_max {
279 self.scroll_offset = self.actual_max;
280 self.auto_scroll = self.actual_max == 0;
281 }
282
283 parser.screen_mut().set_scrollback(self.scroll_offset);
284 }
285
286 fn get_exit_status(&mut self) -> ExitStatus {
287 if self.command_thread.is_some() {
288 let handle = self.command_thread.take().unwrap();
289 let exit_status = handle.join().unwrap_or_else(|_| ExitStatus::with_exit_code(1));
290 self.status = Some(exit_status.clone());
291 exit_status
292 } else {
293 self.status.clone().unwrap_or_else(|| ExitStatus::with_exit_code(1))
294 }
295 }
296
297 pub fn kill_child(&mut self) {
298 if !self.is_finished()
299 && let Some(killer_rx) = self.child_killer.take()
300 && let Ok(Some(mut killer)) = killer_rx.recv()
301 {
302 let _ = killer.kill();
303 }
304 }
305
306 fn handle_passthrough_key_event(&mut self, key: KeyEvent) {
307 let input_bytes = match key.code {
308 KeyCode::Char(ch) => ch.to_string().into_bytes(),
309 KeyCode::Enter => vec![b'\r'],
310 KeyCode::Backspace => vec![0x7f],
311 KeyCode::Left => vec![27, 91, 68],
312 KeyCode::Right => vec![27, 91, 67],
313 KeyCode::Up => vec![27, 91, 65],
314 KeyCode::Down => vec![27, 91, 66],
315 KeyCode::Tab => vec![9],
316 KeyCode::Home => vec![27, 91, 72],
317 KeyCode::End => vec![27, 91, 70],
318 KeyCode::BackTab => vec![27, 91, 90],
319 KeyCode::Delete => vec![27, 91, 51, 126],
320 KeyCode::Insert => vec![27, 91, 50, 126],
321 KeyCode::Esc => vec![27],
322 _ => return,
323 };
324 let _ = self.writer.write_all(&input_bytes);
325 }
326}
327
328impl Drop for RunScriptPopup {
329 fn drop(&mut self) {
330 if !self.is_finished()
331 && let Some(rx) = self.child_killer.take()
332 && let Ok(Some(mut killer)) = rx.recv()
333 {
334 let _ = killer.kill();
335 }
336 }
337}
338
339impl Widget for &mut RunScriptPopup {
340 fn render(self, area: Rect, buf: &mut Buffer) {
341 let running = !self.is_finished();
342
343 let block = if running {
344 Block::bordered()
345 .border_set(border::ROUNDED)
346 .border_style(Style::default().fg(self.theme.primary))
347 .title_style(Style::default().fg(self.theme.primary).reversed())
348 } else {
349 let (title_text, style_color) = if self.get_exit_status().success() {
350 (
351 Line::styled(
352 "Success! Press <Enter> to close",
353 Style::default().fg(self.theme.success).reversed(),
354 ),
355 self.theme.success,
356 )
357 } else {
358 (
359 Line::styled(
360 "Failed! Press <Enter> to close",
361 Style::default().fg(self.theme.error).reversed(),
362 ),
363 self.theme.error,
364 )
365 };
366
367 Block::bordered()
368 .border_set(border::ROUNDED)
369 .border_style(Style::default().fg(style_color))
370 .title_top(title_text.centered())
371 };
372
373 let inner_area = block.inner(area);
374 self.sync_parser(inner_area.as_size());
375
376 Clear.render(area, buf);
377 block.render(area, buf);
378
379 if let Some(parser) = self.parser.as_ref() {
380 let pseudo_term = PseudoTerminal::new(parser.screen());
381 pseudo_term.render(inner_area, buf);
382 }
383 }
384}