mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-12-23 20:09:44 +00:00
Added passthrough to the terminal child process
This commit is contained in:
parent
15e9c8ebe4
commit
7f2e59ad7a
11
src/list.rs
11
src/list.rs
|
@ -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)\""
|
||||
|
|
67
src/main.rs
67
src/main.rs
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user