1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use log::info;
3use oneshot::Receiver;
4use portable_pty::{
5 ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem,
6};
7use ratatui::prelude::*;
8use ratatui::symbols::border;
9use ratatui::widgets::{Block, Clear, Widget};
10use std::io::Write;
11use std::path::PathBuf;
12use std::sync::{Arc, Mutex};
13use std::thread::JoinHandle;
14use tui_term::widget::PseudoTerminal;
15use vt100_ctt::{Parser, Screen};
16
17use crate::ui::theme::Theme;
18
19pub enum PopupEvent {
20 Close,
21 None,
22}
23
24pub struct RunScriptPopup {
25 buffer: Arc<Mutex<Vec<u8>>>,
26 command_thread: Option<JoinHandle<ExitStatus>>,
27 child_killer: Option<Receiver<Box<dyn ChildKiller + Send + Sync>>>,
28 _reader_thread: JoinHandle<()>,
29 pty_master: Box<dyn MasterPty + Send>,
30 writer: Box<dyn Write + Send>,
31 status: Option<ExitStatus>,
32 scroll_offset: usize,
33 theme: Theme,
34}
35
36impl RunScriptPopup {
37 pub fn new(script_path: PathBuf, log_mode: bool, theme: Theme) -> Self {
38 let pty_system = NativePtySystem::default();
39
40 let mut cmd = CommandBuilder::new("bash");
41 cmd.arg(script_path);
42
43 let pair = pty_system
44 .openpty(PtySize {
45 rows: 24,
46 cols: 80,
47 pixel_width: 0,
48 pixel_height: 0,
49 })
50 .unwrap();
51
52 let (tx, rx) = oneshot::channel();
53 let command_handle = std::thread::spawn(move || {
54 let mut child = pair.slave.spawn_command(cmd).unwrap();
55 let killer = child.clone_killer();
56 tx.send(killer).unwrap();
57 child.wait().unwrap()
58 });
59
60 let mut reader = pair.master.try_clone_reader().unwrap();
61
62 let command_buffer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
63 let reader_handle = {
64 let command_buffer = command_buffer.clone();
65 std::thread::spawn(move || {
66 let mut buf = [0u8; 16384];
67 while let Ok(size) = reader.read(&mut buf) {
68 if size == 0 {
69 break;
70 }
71 let mut mutex = command_buffer.lock().unwrap();
72 let data = &buf[0..size];
73 if log_mode {
74 info!("{}", &String::from_utf8_lossy(data));
75 }
76 mutex.extend_from_slice(data);
77 }
78 })
79 };
80
81 let writer = pair.master.take_writer().unwrap();
82 Self {
83 buffer: command_buffer,
84 command_thread: Some(command_handle),
85 child_killer: Some(rx),
86 _reader_thread: reader_handle,
87 pty_master: pair.master,
88 writer,
89 status: None,
90 scroll_offset: 0,
91 theme,
92 }
93 }
94
95 pub fn handle_key_event(&mut self, key: KeyEvent) -> PopupEvent {
96 match key.code {
97 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
98 let _ = self.writer.write_all(&[3]);
99 PopupEvent::None
100 }
101 KeyCode::Enter if self.is_finished() => PopupEvent::Close,
102 KeyCode::Esc if self.is_finished() => PopupEvent::Close,
103 KeyCode::PageUp => {
104 self.scroll_offset = self.scroll_offset.saturating_add(10);
105 PopupEvent::None
106 }
107 KeyCode::PageDown => {
108 self.scroll_offset = self.scroll_offset.saturating_sub(10);
109 PopupEvent::None
110 }
111 _ => {
112 self.handle_passthrough_key_event(key);
113 PopupEvent::None
114 }
115 }
116 }
117
118 fn is_finished(&self) -> bool {
119 if let Some(command_thread) = &self.command_thread {
120 command_thread.is_finished()
121 } else {
122 true
123 }
124 }
125
126 fn screen(&mut self, size: Size) -> Screen {
127 self.pty_master
128 .resize(PtySize {
129 rows: size.height,
130 cols: size.width,
131 pixel_width: 0,
132 pixel_height: 0,
133 })
134 .unwrap();
135
136 let mut parser = Parser::new(size.height, size.width, 1000);
137 let mutex = self.buffer.lock().unwrap();
138 parser.process(&mutex);
139 parser.screen_mut().set_scrollback(self.scroll_offset);
140 parser.screen().clone()
141 }
142
143 fn get_exit_status(&mut self) -> ExitStatus {
144 if self.command_thread.is_some() {
145 let handle = self.command_thread.take().unwrap();
146 let exit_status = handle.join().unwrap();
147 self.status = Some(exit_status.clone());
148 exit_status
149 } else {
150 self.status.as_ref().unwrap().clone()
151 }
152 }
153
154 pub fn kill_child(&mut self) {
155 if !self.is_finished()
156 && let Some(killer_rx) = self.child_killer.take()
157 && let Ok(mut killer) = killer_rx.recv()
158 {
159 let _ = killer.kill();
160 }
161 }
162
163 fn handle_passthrough_key_event(&mut self, key: KeyEvent) {
164 let input_bytes = match key.code {
165 KeyCode::Char(ch) => ch.to_string().into_bytes(),
166 KeyCode::Enter => vec![b'\r'],
167 KeyCode::Backspace => vec![0x7f],
168 KeyCode::Left => vec![27, 91, 68],
169 KeyCode::Right => vec![27, 91, 67],
170 KeyCode::Up => vec![27, 91, 65],
171 KeyCode::Down => vec![27, 91, 66],
172 KeyCode::Tab => vec![9],
173 KeyCode::Home => vec![27, 91, 72],
174 KeyCode::End => vec![27, 91, 70],
175 KeyCode::BackTab => vec![27, 91, 90],
176 KeyCode::Delete => vec![27, 91, 51, 126],
177 KeyCode::Insert => vec![27, 91, 50, 126],
178 KeyCode::Esc => vec![27],
179 _ => return,
180 };
181 let _ = self.writer.write_all(&input_bytes);
182 }
183}
184
185impl Widget for &mut RunScriptPopup {
186 fn render(self, area: Rect, buf: &mut Buffer) {
187 let block = if !self.is_finished() {
188 Block::bordered()
189 .border_set(border::ROUNDED)
190 .border_style(Style::default().fg(self.theme.primary))
191 .title_style(Style::default().fg(self.theme.primary).reversed())
192 .title_bottom(Line::from("Press Ctrl-C to kill"))
193 } else {
194 let (title_text, style_color) = if self.get_exit_status().success() {
195 (
196 Line::styled(
197 "Success! Press <Enter> to close",
198 Style::default().fg(self.theme.success).reversed(),
199 ),
200 self.theme.primary,
201 )
202 } else {
203 (
204 Line::styled(
205 "Failed! Press <Enter> to close",
206 Style::default().fg(self.theme.error).reversed(),
207 ),
208 self.theme.primary,
209 )
210 };
211
212 Block::bordered()
213 .border_set(border::ROUNDED)
214 .border_style(Style::default().fg(style_color))
215 .title_top(title_text.centered())
216 };
217
218 let inner_area = block.inner(area);
219 let screen = self.screen(inner_area.as_size());
220 let pseudo_term = PseudoTerminal::new(&screen);
221
222 Clear.render(area, buf);
223 block.render(area, buf);
224 pseudo_term.render(inner_area, buf);
225 }
226}