carch_core/ui/popups/
run_script.rs

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}