Merge pull request #13 from JustLinuxUser/main

Added passthrough to the terminal child process
This commit is contained in:
Chris Titus 2024-07-12 18:41:18 -05:00 committed by GitHub
commit 5cd59c5d06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 135 additions and 70 deletions

BIN
linutil

Binary file not shown.

View File

@ -1,4 +1,3 @@
use crate::theme::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
use ego_tree::{tree, NodeId};
@ -29,8 +28,6 @@ pub struct CustomList {
}
impl CustomList {
/// It's really easy to make this accept a tree, and make it reusable, but rn its only called
/// once, so I didn't bother, and it gets initialized here
pub fn new() -> Self {
// When a function call ends with an exclamation mark, it means it's a macro, like in this
// case the tree! macro expands to `ego-tree::tree` data structure
@ -38,6 +35,14 @@ impl CustomList {
name: "root",
command: ""
} => {
ListNode {
name: "Full bash",
command: "bash"
},
ListNode {
name: "Full zsh",
command: "zsh"
},
ListNode {
name: "Setup Bash Prompt",
command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""

View File

@ -11,7 +11,7 @@ use std::{
use clap::Parser;
use crossterm::{
cursor::RestorePosition,
event::{self, DisableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers},
event::{self, DisableMouseCapture, Event, KeyCode, KeyEventKind},
style::ResetColor,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
@ -60,43 +60,7 @@ fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut custom_list = CustomList::new();
loop {
// If currently running a command, display the command window, else display only the list
// Event read is blocking
if event::poll(Duration::from_millis(10))? {
// It's guaranteed that the `read()` won't block when the `poll()`
// function returns `true`
if let Event::Key(key) = event::read()? {
// We are only interested in Press and Repeat events
if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat {
// Only process list inputs when a command is not running
if let None = command_opt {
if let Some(cmd) = custom_list.handle_key(key) {
command_opt = Some(RunningCommand::new(cmd));
}
}
// In the future we might want to add key handling for the running command, and
// we would put it here
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Some(ref mut command) = command_opt {
command.kill_child();
}
}
KeyCode::Enter => {
if let Some(ref mut command) = command_opt {
if command.is_finished() {
command_opt = None;
}
}
}
_ => (),
};
}
}
}
// Always redraw
terminal
.draw(|frame| {
custom_list.draw(frame, frame.size());
@ -105,5 +69,32 @@ fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
})
.unwrap();
// Wait for an event
if !event::poll(Duration::from_millis(10))? {
continue;
}
// It's guaranteed that the `read()` won't block when the `poll()`
// function returns `true`
if let Event::Key(key) = event::read()? {
// We are only interested in Press and Repeat events
if key.kind != KeyEventKind::Press && key.kind != KeyEventKind::Repeat {
continue;
}
if let Some(ref mut command) = command_opt {
if command.handle_key_event(&key) {
command_opt = None;
}
} else {
if key.code == KeyCode::Char('q') {
return Ok(());
}
if let Some(cmd) = custom_list.handle_key(key) {
command_opt = Some(RunningCommand::new(cmd));
}
}
}
}
}

View File

@ -1,15 +1,17 @@
use std::{
io::Write,
sync::{Arc, Mutex},
thread::JoinHandle,
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use oneshot::{channel, Receiver};
use portable_pty::{
ChildKiller, CommandBuilder, ExitStatus, MasterPty, NativePtySystem, PtySize, PtySystem,
};
use ratatui::{
layout::Size,
style::{Color, Style, Styled, Stylize},
style::{Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders},
Frame,
@ -21,26 +23,30 @@ use tui_term::{
use crate::{float::floating_window, theme::get_theme};
// This is a struct for stoaring everything connected to a running command
/// This is a struct for storing everything connected to a running command
// Create a new instance on every new command you want to run
pub struct RunningCommand {
buffer: Arc<Mutex<Vec<u8>>>, // A buffer to save all the command output (accumulates, untill the command
// exits)
command_thread: Option<JoinHandle<ExitStatus>>, // the tread where the command is being executed
child_killer: Option<Receiver<Box<dyn ChildKiller + Send + Sync>>>, // This is a thing that
// will allow us to kill the running command on Ctrl-C
// Also, don't mind the name :)
/// A buffer to save all the command output (accumulates, untill the command exits)
buffer: Arc<Mutex<Vec<u8>>>,
//It is an option, because we want to be able to .join it, without
// moving the whole RunningCommand struct, (we want to have the exit code, and still have acess
// to the buffer, to render the terminal output)
_reader_thread: JoinHandle<()>, // The thread that reads the command output, and sends it to us
// by writing to the buffer. We need another thread, because the reader may block, and we want
// our UI to stay responsive.
pty_master: Box<dyn MasterPty + Send>, // This is a master handle of the emulated terminal, we
// will use it to resize the emulated terminal
status: Option<ExitStatus>, // We want to be able to get the exit status more then once, and
// this is a nice place to store it. We will put it here, after joining the reader_tread
/// A handle of the tread where the command is being executed
command_thread: Option<JoinHandle<ExitStatus>>,
/// A handle to kill the running process, it's an option because it can only be used once
child_killer: Option<Receiver<Box<dyn ChildKiller + Send + Sync>>>,
/// A join handle for the thread that is reading all the command output and sending it to the
/// main thread
_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>,
}
impl RunningCommand {
@ -72,19 +78,15 @@ impl RunningCommand {
});
let mut reader = pair.master.try_clone_reader().unwrap(); // This is a reader, this is where we
// are reading the command output from
// This is a bit complicated, but I will try my best to explain :)
// Arc<Mutex<>> Means that this object is an Async Reference Count (Arc) Mutex lock. We
// need the ark part, because when all references holding that ark go out of scope, we want
// the memory to get freed. Mutex is to allow us to write and read to the memory from
// different threads, without fear that some thread will be reading when other is writing
// A buffer, shared between the thread that reads the command output, and the main tread.
// The main thread only reads the contents
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,
// thats why command_buffer.clone(), because we need to use command_buffer later
// that's why command_buffer.clone(), because we need to use command_buffer later
std::thread::spawn(move || {
let mut buf = [0u8; 8192];
loop {
@ -100,12 +102,15 @@ impl RunningCommand {
}
})
};
let writer = pair.master.take_writer().unwrap();
Self {
buffer: command_buffer,
command_thread: Some(command_handle),
child_killer: Some(rx),
_reader_thread: reader_handle,
pty_master: pair.master,
writer,
status: None,
}
}
@ -121,7 +126,7 @@ impl RunningCommand {
.unwrap();
// Process the buffer with a parser with the current screen size
// We don't actually need to create a new parser every time, but it is so much easyer this
// We don't actually need to create a new parser every time, but it is so much easier this
// way, and doesn't cost that much
let mut parser = vt100::Parser::new(size.height, size.width, 0);
let mutex = self.buffer.lock();
@ -150,7 +155,6 @@ impl RunningCommand {
pub fn draw(&mut self, frame: &mut Frame) {
{
let theme = get_theme();
// Funny name
let floater = floating_window(frame.size());
@ -202,12 +206,77 @@ impl RunningCommand {
frame.render_widget(pseudo_term, floater);
}
}
/// From what I observed this sends SIGHUB signal, *not* SIGKILL or SIGTERM, so the process
/// doesn't get a chance to clean up. If neccesary, I can look into sending SIGTERM directly
/// Send SIGHUB signal, *not* SIGKILL or SIGTERM, to the child process
pub fn kill_child(&mut self) {
if !self.is_finished() {
let mut killer = self.child_killer.take().unwrap().recv().unwrap();
killer.kill().unwrap();
}
}
/// Handle key events of the running command "window". Returns true when the "window" should be
/// closed
pub fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.kill_child()
}
KeyCode::Enter if self.is_finished() => {
return true;
}
_ => self.handle_passthrough_key_event(key),
};
false
}
/// 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) => {
let mut send = vec![ch as u8];
let upper = ch.to_ascii_uppercase();
if key.modifiers == KeyModifiers::CONTROL {
match upper {
// https://github.com/fyne-io/terminal/blob/master/input.go
// https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b
'2' | '@' | ' ' => send = vec![0],
'3' | '[' => send = vec![27],
'4' | '\\' => send = vec![28],
'5' | ']' => send = vec![29],
'6' | '^' => send = vec![30],
'7' | '-' | '_' => send = vec![31],
char if ('A'..='_').contains(&char) => {
// Since A == 65,
// we can safely subtract 64 to get
// the corresponding control character
let ascii_val = char as u8;
let ascii_to_send = ascii_val - 64;
send = vec![ascii_to_send];
}
_ => {}
}
}
send
}
KeyCode::Enter => vec![b'\n'],
KeyCode::Backspace => vec![8],
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);
}
}