2024-09-06 22:36:12 +01:00
|
|
|
use crate::{
|
|
|
|
float::FloatContent,
|
|
|
|
hint::{Shortcut, ShortcutList},
|
|
|
|
};
|
2024-07-11 23:58:27 +01:00
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
2024-09-06 00:39:10 +01:00
|
|
|
use linutil_core::Command;
|
2024-06-06 23:56:45 +01:00
|
|
|
use oneshot::{channel, Receiver};
|
|
|
|
use portable_pty::{
|
|
|
|
ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem,
|
|
|
|
};
|
|
|
|
use ratatui::{
|
2024-08-06 17:22:15 +01:00
|
|
|
layout::{Rect, Size},
|
|
|
|
style::{Color, Style, Stylize},
|
2024-06-06 23:56:45 +01:00
|
|
|
text::{Line, Span},
|
|
|
|
widgets::{Block, Borders},
|
|
|
|
Frame,
|
|
|
|
};
|
2024-08-06 17:22:15 +01:00
|
|
|
use std::{
|
|
|
|
io::Write,
|
|
|
|
sync::{Arc, Mutex},
|
|
|
|
thread::JoinHandle,
|
|
|
|
};
|
2024-06-06 23:56:45 +01:00
|
|
|
use tui_term::{
|
|
|
|
vt100::{self, Screen},
|
|
|
|
widget::PseudoTerminal,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub struct RunningCommand {
|
2024-08-06 17:22:15 +01:00
|
|
|
/// A buffer to save all the command output (accumulates, until the command exits)
|
2024-07-11 23:58:27 +01:00
|
|
|
buffer: Arc<Mutex<Vec<u8>>>,
|
2024-08-06 17:22:15 +01:00
|
|
|
/// A handle for the thread running the command
|
2024-07-19 14:56:09 +01:00
|
|
|
command_thread: Option<JoinHandle<ExitStatus>>,
|
2024-08-06 17:22:15 +01:00
|
|
|
/// A handle to kill the running process; it's an option because it can only be used once
|
2024-07-11 23:58:27 +01:00
|
|
|
child_killer: Option<Receiver<Box<dyn ChildKiller + Send + Sync>>>,
|
2024-08-06 17:22:15 +01:00
|
|
|
/// A join handle for the thread that reads command output and sends it to the main thread
|
2024-07-11 23:58:27 +01:00
|
|
|
_reader_thread: JoinHandle<()>,
|
|
|
|
/// Virtual terminal (pty) handle, used for resizing the pty
|
|
|
|
pty_master: Box<dyn MasterPty + Send>,
|
|
|
|
/// Used for sending keys to the emulated terminal
|
|
|
|
writer: Box<dyn Write + Send>,
|
|
|
|
/// Only set after the process has ended
|
|
|
|
status: Option<ExitStatus>,
|
2024-06-06 23:56:45 +01:00
|
|
|
}
|
|
|
|
|
2024-08-06 17:22:15 +01:00
|
|
|
impl FloatContent for RunningCommand {
|
|
|
|
fn draw(&mut self, frame: &mut Frame, area: Rect) {
|
|
|
|
// Calculate the inner size of the terminal area, considering borders
|
|
|
|
let inner_size = Size {
|
|
|
|
width: area.width - 2, // Adjust for border width
|
|
|
|
height: area.height - 2,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Define the block for the terminal display
|
|
|
|
let block = if !self.is_finished() {
|
|
|
|
// Display a block indicating the command is running
|
|
|
|
Block::default()
|
|
|
|
.borders(Borders::ALL)
|
|
|
|
.title_top(Line::from("Running the command....").centered())
|
|
|
|
.title_style(Style::default().reversed())
|
|
|
|
.title_bottom(Line::from("Press Ctrl-C to KILL the command"))
|
|
|
|
} else {
|
|
|
|
// Display a block with the command's exit status
|
|
|
|
let mut title_line = if self.get_exit_status().success() {
|
|
|
|
Line::from(
|
|
|
|
Span::default()
|
|
|
|
.content("SUCCESS!")
|
|
|
|
.style(Style::default().fg(Color::Green).reversed()),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
Line::from(
|
|
|
|
Span::default()
|
|
|
|
.content("FAILED!")
|
|
|
|
.style(Style::default().fg(Color::Red).reversed()),
|
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
title_line.push_span(
|
|
|
|
Span::default()
|
|
|
|
.content(" press <ENTER> to close this window ")
|
|
|
|
.style(Style::default()),
|
|
|
|
);
|
|
|
|
|
|
|
|
Block::default()
|
|
|
|
.borders(Borders::ALL)
|
|
|
|
.title_top(title_line.centered())
|
|
|
|
};
|
|
|
|
|
|
|
|
// Process the buffer and create the pseudo-terminal widget
|
|
|
|
let screen = self.screen(inner_size);
|
|
|
|
let pseudo_term = PseudoTerminal::new(&screen).block(block);
|
|
|
|
|
|
|
|
// Render the widget on the frame
|
|
|
|
frame.render_widget(pseudo_term, area);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handle key events of the running command "window". Returns true when the "window" should be
|
|
|
|
/// closed
|
|
|
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
|
|
|
match key.code {
|
|
|
|
// Handle Ctrl-C to kill the command
|
|
|
|
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
self.kill_child();
|
|
|
|
}
|
|
|
|
// Close the window when Enter is pressed and the command is finished
|
|
|
|
KeyCode::Enter if self.is_finished() => {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// Pass other key events to the terminal
|
|
|
|
_ => self.handle_passthrough_key_event(key),
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_finished(&self) -> bool {
|
|
|
|
// Check if the command thread has finished
|
|
|
|
if let Some(command_thread) = &self.command_thread {
|
|
|
|
command_thread.is_finished()
|
|
|
|
} else {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
}
|
2024-09-06 22:36:12 +01:00
|
|
|
|
|
|
|
fn get_shortcut_list(&self) -> ShortcutList {
|
|
|
|
if self.is_finished() {
|
|
|
|
ShortcutList {
|
|
|
|
scope_name: "Finished command",
|
|
|
|
hints: vec![Shortcut::new(vec!["Enter", "q"], "Close window")],
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ShortcutList {
|
|
|
|
scope_name: "Running command",
|
|
|
|
hints: vec![Shortcut::new(vec!["CTRL-c"], "Kill the command")],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-08-06 17:22:15 +01:00
|
|
|
}
|
|
|
|
|
2024-06-06 23:56:45 +01:00
|
|
|
impl RunningCommand {
|
2024-09-19 19:15:30 +01:00
|
|
|
pub fn new(commands: Vec<Command>) -> Self {
|
2024-06-06 23:56:45 +01:00
|
|
|
let pty_system = NativePtySystem::default();
|
2024-07-28 16:17:06 +01:00
|
|
|
|
2024-08-06 17:22:15 +01:00
|
|
|
// Build the command based on the provided Command enum variant
|
2024-09-19 19:15:30 +01:00
|
|
|
let mut cmd: CommandBuilder = CommandBuilder::new("sh");
|
|
|
|
cmd.arg("-c");
|
|
|
|
|
|
|
|
// All the merged commands are passed as a single argument to reduce the overhead of rebuilding the command arguments for each and every command
|
|
|
|
let mut script = String::new();
|
|
|
|
for command in commands {
|
|
|
|
match command {
|
|
|
|
Command::Raw(prompt) => script.push_str(&format!("{}\n", prompt)),
|
2024-09-22 17:50:43 +01:00
|
|
|
Command::LocalFile {
|
|
|
|
executable,
|
|
|
|
args,
|
|
|
|
file,
|
|
|
|
} => {
|
|
|
|
if let Some(parent_directory) = file.parent() {
|
|
|
|
script.push_str(&format!("cd {}\n", parent_directory.display()));
|
|
|
|
}
|
|
|
|
script.push_str(&executable);
|
|
|
|
for arg in args {
|
|
|
|
script.push(' ');
|
|
|
|
script.push_str(&arg);
|
2024-09-19 19:15:30 +01:00
|
|
|
}
|
2024-09-22 19:24:06 +01:00
|
|
|
script.push('\n'); // Ensures that each command is properly separated for execution preventing directory errors
|
2024-08-15 07:13:56 +01:00
|
|
|
}
|
2024-09-19 19:15:30 +01:00
|
|
|
Command::None => panic!("Command::None was treated as a command"),
|
2024-07-28 16:17:06 +01:00
|
|
|
}
|
|
|
|
}
|
2024-06-06 23:56:45 +01:00
|
|
|
|
2024-09-19 19:15:30 +01:00
|
|
|
cmd.arg(script);
|
|
|
|
|
2024-08-06 17:22:15 +01:00
|
|
|
// Open a pseudo-terminal with initial size
|
2024-06-06 23:56:45 +01:00
|
|
|
let pair = pty_system
|
|
|
|
.openpty(PtySize {
|
2024-08-06 17:22:15 +01:00
|
|
|
rows: 24, // Initial number of rows (will be updated dynamically)
|
|
|
|
cols: 80, // Initial number of columns (will be updated dynamically)
|
2024-06-06 23:56:45 +01:00
|
|
|
pixel_width: 0,
|
|
|
|
pixel_height: 0,
|
|
|
|
})
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
// Thread waiting for the child to complete
|
|
|
|
let command_handle = std::thread::spawn(move || {
|
|
|
|
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
|
|
|
let killer = child.clone_killer();
|
|
|
|
tx.send(killer).unwrap();
|
|
|
|
child.wait().unwrap()
|
|
|
|
});
|
|
|
|
|
|
|
|
let mut reader = pair.master.try_clone_reader().unwrap(); // This is a reader, this is where we
|
|
|
|
|
2024-07-11 23:58:27 +01:00
|
|
|
// A buffer, shared between the thread that reads the command output, and the main tread.
|
|
|
|
// The main thread only reads the contents
|
2024-06-06 23:56:45 +01:00
|
|
|
let command_buffer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
|
|
|
|
let reader_handle = {
|
|
|
|
// Arc is just a reference, so we can create an owned copy without any problem
|
|
|
|
let command_buffer = command_buffer.clone();
|
|
|
|
// The closure below moves all variables used into it, so we can no longer use them,
|
2024-07-11 23:58:27 +01:00
|
|
|
// that's why command_buffer.clone(), because we need to use command_buffer later
|
2024-06-06 23:56:45 +01:00
|
|
|
std::thread::spawn(move || {
|
|
|
|
let mut buf = [0u8; 8192];
|
|
|
|
loop {
|
|
|
|
let size = reader.read(&mut buf).unwrap(); // Can block here
|
|
|
|
if size == 0 {
|
|
|
|
break; // EOF
|
|
|
|
}
|
|
|
|
let mut mutex = command_buffer.lock(); // Only lock the mutex after the read is
|
|
|
|
// done, to minimise the time it is opened
|
|
|
|
let command_buffer = mutex.as_mut().unwrap();
|
|
|
|
command_buffer.extend_from_slice(&buf[0..size]);
|
|
|
|
// The mutex is closed here automatically
|
|
|
|
}
|
|
|
|
})
|
|
|
|
};
|
2024-07-11 23:58:27 +01:00
|
|
|
|
|
|
|
let writer = pair.master.take_writer().unwrap();
|
2024-06-06 23:56:45 +01:00
|
|
|
Self {
|
|
|
|
buffer: command_buffer,
|
|
|
|
command_thread: Some(command_handle),
|
|
|
|
child_killer: Some(rx),
|
|
|
|
_reader_thread: reader_handle,
|
|
|
|
pty_master: pair.master,
|
2024-07-11 23:58:27 +01:00
|
|
|
writer,
|
2024-06-06 23:56:45 +01:00
|
|
|
status: None,
|
|
|
|
}
|
|
|
|
}
|
2024-08-06 17:22:15 +01:00
|
|
|
|
2024-06-06 23:56:45 +01:00
|
|
|
fn screen(&mut self, size: Size) -> Screen {
|
|
|
|
// Resize the emulated pty
|
|
|
|
self.pty_master
|
|
|
|
.resize(PtySize {
|
|
|
|
rows: size.height,
|
|
|
|
cols: size.width,
|
|
|
|
pixel_width: 0,
|
|
|
|
pixel_height: 0,
|
|
|
|
})
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
// Process the buffer with a parser with the current screen size
|
2024-07-11 23:58:27 +01:00
|
|
|
// We don't actually need to create a new parser every time, but it is so much easier this
|
2024-06-06 23:56:45 +01:00
|
|
|
// way, and doesn't cost that much
|
|
|
|
let mut parser = vt100::Parser::new(size.height, size.width, 0);
|
|
|
|
let mutex = self.buffer.lock();
|
|
|
|
let buffer = mutex.as_ref().unwrap();
|
|
|
|
parser.process(buffer);
|
|
|
|
parser.screen().clone()
|
|
|
|
}
|
2024-08-06 17:22:15 +01:00
|
|
|
|
2024-06-06 23:56:45 +01:00
|
|
|
/// This function will block if the command is not finished
|
|
|
|
fn get_exit_status(&mut self) -> ExitStatus {
|
|
|
|
if self.command_thread.is_some() {
|
|
|
|
let handle = self.command_thread.take().unwrap();
|
|
|
|
let exit_status = handle.join().unwrap();
|
|
|
|
self.status = Some(exit_status.clone());
|
|
|
|
exit_status
|
|
|
|
} else {
|
|
|
|
self.status.as_ref().unwrap().clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-11 23:58:27 +01:00
|
|
|
/// Send SIGHUB signal, *not* SIGKILL or SIGTERM, to the child process
|
2024-06-06 23:56:45 +01:00
|
|
|
pub fn kill_child(&mut self) {
|
|
|
|
if !self.is_finished() {
|
|
|
|
let mut killer = self.child_killer.take().unwrap().recv().unwrap();
|
|
|
|
killer.kill().unwrap();
|
|
|
|
}
|
|
|
|
}
|
2024-07-11 23:58:27 +01:00
|
|
|
|
|
|
|
/// Convert the KeyEvent to pty key codes, and send them to the virtual terminal
|
|
|
|
fn handle_passthrough_key_event(&mut self, key: &KeyEvent) {
|
|
|
|
let input_bytes = match key.code {
|
|
|
|
KeyCode::Char(ch) => {
|
2024-09-22 17:50:43 +01:00
|
|
|
let raw_utf8 = || ch.to_string().into_bytes();
|
|
|
|
|
|
|
|
match ch.to_ascii_uppercase() {
|
|
|
|
_ if key.modifiers != KeyModifiers::CONTROL => raw_utf8(),
|
|
|
|
// https://github.com/fyne-io/terminal/blob/master/input.go
|
|
|
|
// https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b
|
|
|
|
'2' | '@' | ' ' => vec![0],
|
|
|
|
'3' | '[' => vec![27],
|
|
|
|
'4' | '\\' => vec![28],
|
|
|
|
'5' | ']' => vec![29],
|
|
|
|
'6' | '^' => vec![30],
|
|
|
|
'7' | '-' | '_' => vec![31],
|
|
|
|
c if ('A'..='_').contains(&c) => {
|
|
|
|
let ascii_val = c as u8;
|
|
|
|
let ascii_to_send = ascii_val - 64;
|
|
|
|
vec![ascii_to_send]
|
2024-07-11 23:58:27 +01:00
|
|
|
}
|
2024-09-22 17:50:43 +01:00
|
|
|
_ => raw_utf8(),
|
2024-07-11 23:58:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
KeyCode::Enter => vec![b'\n'],
|
2024-09-05 03:09:02 +01:00
|
|
|
KeyCode::Backspace => vec![0x7f],
|
2024-07-11 23:58:27 +01:00
|
|
|
KeyCode::Left => vec![27, 91, 68],
|
|
|
|
KeyCode::Right => vec![27, 91, 67],
|
|
|
|
KeyCode::Up => vec![27, 91, 65],
|
|
|
|
KeyCode::Down => vec![27, 91, 66],
|
|
|
|
KeyCode::Tab => vec![9],
|
|
|
|
KeyCode::Home => vec![27, 91, 72],
|
|
|
|
KeyCode::End => vec![27, 91, 70],
|
|
|
|
KeyCode::PageUp => vec![27, 91, 53, 126],
|
|
|
|
KeyCode::PageDown => vec![27, 91, 54, 126],
|
|
|
|
KeyCode::BackTab => vec![27, 91, 90],
|
|
|
|
KeyCode::Delete => vec![27, 91, 51, 126],
|
|
|
|
KeyCode::Insert => vec![27, 91, 50, 126],
|
|
|
|
KeyCode::Esc => vec![27],
|
|
|
|
_ => return,
|
|
|
|
};
|
|
|
|
// Send the keycodes to the virtual terminal
|
|
|
|
let _ = self.writer.write_all(&input_bytes);
|
|
|
|
}
|
2024-07-19 14:56:09 +01:00
|
|
|
}
|