Massive refactor using channels to improve typing speed

This commit is contained in:
Austen Adler 2022-12-18 03:12:35 -05:00
parent df9c53b20d
commit 8d26f6c55c
6 changed files with 312 additions and 210 deletions

1
Cargo.lock generated
View File

@ -297,6 +297,7 @@ dependencies = [
"atty", "atty",
"crossbeam", "crossbeam",
"crossterm", "crossterm",
"parking_lot",
"shellwords", "shellwords",
"tui", "tui",
] ]

View File

@ -13,3 +13,4 @@ ansi4tui = {path = "./ansi4tui/"}
anyhow = "1.0.66" anyhow = "1.0.66"
shellwords = "1.1.0" shellwords = "1.1.0"
crossbeam = "0.8.2" crossbeam = "0.8.2"
parking_lot = "0.12.1"

View File

@ -1,18 +1,23 @@
use crate::event::EventMessage; use crate::event::EventMessage;
use crate::CommandOptions;
use anyhow::bail; use anyhow::bail;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
use parking_lot::RwLock;
use std::sync::Arc;
use std::{ use std::{
io::Write, io::Write,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
pub type CommandRequest = (Arc<RwLock<CommandOptions>>, Arc<String>);
use crate::App; use crate::App;
pub struct CommandRequest { pub enum CommandCompleted {
cmdline: String, Success(CommandResult),
Failure(Vec<u8>),
} }
pub struct CommandResult { pub struct CommandResult {
@ -31,46 +36,65 @@ impl Default for CommandResult {
} }
} }
// pub fn command_event_loop(command_request_receiver: Receiver<CommandRequest>,event_sender: Sender<EventMessage>) -> Result<()> { pub fn command_event_loop(
// loop { command_request_receiver: Receiver<CommandRequest>,
// match command_request_receiver.recv() { event_sender: Sender<EventMessage>,
// Ok(command_request) => event_sender.send(EventMessage::CrosstermEvent(e))?, ) -> Result<()> {
// Err(e) => { loop {
// event_sender.send(EventMessage::CrosstermError(e))?; match command_request_receiver.recv() {
// bail!("Crossterm read error"); Ok(command_request) => {
// } event_sender.send(EventMessage::CommandCompleted(
// } match run_inner(command_request) {
// } Ok(c) => {
// } // If there was no stdout and the command failed, don't touch stdout
if !c.status_success && c.stdout.is_empty() {
pub fn run(app: &mut App) { CommandCompleted::Failure(c.stderr)
match run_inner(app) { } else {
Ok(c) => { CommandCompleted::Success(c)
// If there was no stdout and the command failed, don't touch stdout }
if !c.status_success && c.stdout.is_empty() { }
app.command_result.stderr = c.stderr; Err(e) => CommandCompleted::Failure(e.to_string().as_bytes().to_vec()),
app.command_result.status_success = c.status_success; },
} else { ))?;
app.command_result = c; }
Err(e) => {
event_sender.send(EventMessage::CommandLoopStopped)?;
bail!("Crossterm read error: {}", e);
} }
}
Err(e) => {
app.command_result.status_success = false;
app.command_result.stderr = e.to_string().as_bytes().to_vec();
} }
} }
} }
fn run_inner(app: &mut App) -> Result<CommandResult> { // pub fn run(app: Arc<App>) {
let mut command = Command::new(&app.command); // match run_inner(app) {
// Ok(c) => {
// // If there was no stdout and the command failed, don't touch stdout
// if !c.status_success && c.stdout.is_empty() {
// app.command_result.stderr = c.stderr;
// app.command_result.status_success = c.status_success;
// } else {
// app.command_result = c;
// }
// }
// Err(e) => {
// app.command_result.status_success = false;
// app.command_result.stderr = e.to_string().as_bytes().to_vec();
// }
// }
// }
fn run_inner(command_request: CommandRequest) -> Result<CommandResult> {
let request = command_request.0.read();
let mut command = Command::new(&request.command);
command command
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.args(&app.hidden_options); .args(&request.hidden_options);
if app.wordsplit { if request.wordsplit {
match shellwords::split(&app.cmdline) { match shellwords::split(&request.cmdline) {
Ok(a) => { Ok(a) => {
command.args(a); command.args(a);
} }
@ -80,18 +104,18 @@ fn run_inner(app: &mut App) -> Result<CommandResult> {
} }
} else { } else {
// TODO: Avoid cloning here // TODO: Avoid cloning here
command.arg(&app.cmdline); command.arg(&request.cmdline);
} }
let mut child = command.spawn().context("Could not spawn child process")?; let mut child = command.spawn()?;
let mut stdin = child.stdin.take().context("Could not take stdin")?; let mut stdin = child.stdin.take().context("Could not take stdin")?;
let text_orig_clone = app.text_orig.clone(); let text_orig_clone = command_request.1.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let _result = stdin.write_all(text_orig_clone.as_bytes()); let _result = stdin.write_all(text_orig_clone.as_bytes());
}); });
// Collect the output // Collect the output
let output = child.wait_with_output().context("Failed to read stdout")?; let output = child.wait_with_output()?;
Ok(CommandResult { Ok(CommandResult {
status_success: output.status.success(), status_success: output.status.success(),

View File

@ -1,12 +1,13 @@
use crate::command::CommandRequest; use crate::command::CommandCompleted;
use anyhow::bail; use anyhow::bail;
use anyhow::Result; use anyhow::Result;
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
pub enum EventMessage { pub enum EventMessage {
CommandCompleted, CommandCompleted(CommandCompleted),
CrosstermEvent(crossterm::event::Event), CrosstermEvent(crossterm::event::Event),
CrosstermError(std::io::Error), CrosstermError(std::io::Error),
CommandLoopStopped,
} }
pub fn crossterm_event_loop(event_sender: Sender<EventMessage>) -> Result<()> { pub fn crossterm_event_loop(event_sender: Sender<EventMessage>) -> Result<()> {

View File

@ -8,11 +8,16 @@ mod ui;
use ansi4tui::bytes_to_text; use ansi4tui::bytes_to_text;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::bail; use anyhow::bail;
use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use command::CommandRequest;
use command::CommandResult; use command::CommandResult;
use crossbeam::channel::Receiver;
use crossbeam::channel::Sender;
use crossterm::event::DisableBracketedPaste; use crossterm::event::DisableBracketedPaste;
use crossterm::event::EnableBracketedPaste; use crossterm::event::EnableBracketedPaste;
use event::EventMessage; use event::EventMessage;
use parking_lot::RwLock;
use std::str::FromStr; use std::str::FromStr;
use std::{ use std::{
io::{self, Write}, io::{self, Write},
@ -35,8 +40,8 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
/// The state of the application /// The state of options for the command
pub struct App { pub struct CommandOptions {
/// The actual command to be called /// The actual command to be called
command: String, command: String,
@ -46,94 +51,109 @@ pub struct App {
/// The line the user inputs /// The line the user inputs
cmdline: String, cmdline: String,
/// The position of the cursor
cmdline_position: u16,
/// Original text
text_orig: Arc<String>,
/// The result of a command execution
command_result: CommandResult,
/// Should every keystroke transform the original text? /// Should every keystroke transform the original text?
autorun: bool, autorun: bool,
/// Should wordsplitting be enabled /// Should wordsplitting be enabled
wordsplit: bool, wordsplit: bool,
/// The result of a command execution
command_result: CommandResult,
/// The position of the cursor
cmdline_position: u16,
}
/// The state of the application
pub struct App {
/// Original text
text_orig: Arc<String>,
// text_orig_formatted: Arc<String>
/// The list of options for a command, given in an RwLock so the command runner can update it
command_options: Arc<RwLock<CommandOptions>>,
/// The receiver for events
message_rx: Receiver<EventMessage>,
/// The command request transmitter
command_request_tx: Sender<CommandRequest>,
}
impl CommandOptions {
#[must_use]
pub fn from_template(template: &Template) -> Self {
let defaults = Self {
command: template.command(),
cmdline_position: 0_u16,
hidden_options: Vec::new(),
cmdline: String::new(),
command_result: CommandResult::default(),
autorun: true,
wordsplit: true,
};
match template {
Template::Awk => Self { ..defaults },
Template::Sh(_) => Self {
hidden_options: vec!["-c"],
wordsplit: false,
..defaults
},
Template::Jq => Self {
cmdline_position: 2,
cmdline: String::from("'.'"),
hidden_options: vec!["-C"],
..defaults
},
Template::Grep | Template::Rg => Self {
cmdline_position: 1,
cmdline: String::from("''"),
hidden_options: vec!["--color=always"],
..defaults
},
Template::Sed => Self {
cmdline_position: 3_u16,
cmdline: String::from("'s///g'"),
..defaults
},
Template::Perl => Self {
cmdline_position: 10_u16,
cmdline: String::from("-p -e 's///'"),
..defaults
},
}
}
} }
impl App { impl App {
/// Constructs a new instance of `App`
///
/// Arguments:
///
/// * `command_request_tx` - The sender for the command request worker
/// * `input` - The stdin to be passed in to each invocation
/// * `template` - The template to use
#[must_use] #[must_use]
pub fn from_template(input: String, template: &Template) -> Self { pub fn from_template(
let text_orig = Arc::new(input); message_rx: Receiver<EventMessage>,
let command_result = CommandResult::default(); command_request_tx: Sender<CommandRequest>,
let command = template.command(); input: String,
let cmdline_position = 0; template: &Template,
let wordsplit = true; ) -> Self {
Self {
match template { text_orig: Arc::new(input),
Template::Awk => Self { command_options: Arc::new(RwLock::new(CommandOptions::from_template(template))),
cmdline: String::new(), message_rx,
cmdline_position, command_request_tx,
text_orig,
command_result: CommandResult::default(),
autorun: true,
command,
hidden_options: vec![],
wordsplit,
},
Template::Sh(_) => Self {
cmdline: String::from(""),
cmdline_position: 0,
text_orig,
command_result,
autorun: true,
command,
hidden_options: vec!["-c"],
wordsplit: false,
},
Template::Jq => Self {
cmdline: String::from("'.'"),
cmdline_position: 2,
text_orig,
command_result,
autorun: true,
command,
hidden_options: vec!["-C"],
wordsplit,
},
Template::Grep | Template::Rg => Self {
cmdline: String::from("''"),
cmdline_position: 1,
text_orig,
command_result: CommandResult::default(),
autorun: true,
command,
hidden_options: vec!["--color=always"],
wordsplit,
},
Template::Sed => Self {
cmdline: String::from("'s///g'"),
cmdline_position: 3_u16,
text_orig,
command_result: CommandResult::default(),
autorun: true,
command,
hidden_options: vec![],
wordsplit,
},
Template::Perl => Self {
cmdline: String::from("-p -e 's///'"),
cmdline_position: 10_u16,
text_orig,
command_result: CommandResult::default(),
autorun: true,
command,
hidden_options: vec![],
wordsplit,
},
} }
} }
pub fn run_command(&self) -> Result<()> {
self.command_request_tx
.send((self.command_options.clone(), self.text_orig.clone()))?;
Ok(())
}
} }
pub enum Template { pub enum Template {
@ -192,8 +212,16 @@ fn main() -> Result<()> {
// Slurp all input // Slurp all input
let text_orig = io::read_to_string(io::stdin())?; let text_orig = io::read_to_string(io::stdin())?;
// Start the command worker thread
let (message_rx, command_request_tx) = init_message_passing();
// Run the actual application // Run the actual application
let app = App::from_template(text_orig, &Template::from_str(&arg)?); let app = App::from_template(
message_rx,
command_request_tx,
text_orig,
&Template::from_str(&arg)?,
);
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
// execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; // execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
@ -229,103 +257,148 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
fn run_app<B: Backend>( fn init_message_passing() -> (Receiver<EventMessage>, Sender<CommandRequest>) {
terminal: &mut Terminal<B>, let (message_tx, message_rx) = crossbeam::channel::bounded(100);
mut app: App, let (command_request_tx, command_request_rx) = crossbeam::channel::bounded(100);
) -> Result<Option<(String, Vec<u8>)>> {
if !app.cmdline.is_empty() {
command::run(&mut app);
}
if app.cmdline_position == 0 {
app.cmdline_position = app.cmdline.len() as u16;
}
let (tx, rx) = crossbeam::channel::bounded(100);
// Start the event thread // Start the event thread
let crossterm_tx = tx.clone(); let crossterm_message_tx = message_tx.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let _result = event::crossterm_event_loop(crossterm_tx); let _result = event::crossterm_event_loop(crossterm_message_tx);
}); });
std::mem::drop(tx); // Start the worker thread
let command_message_tx = message_tx.clone();
std::thread::spawn(move || {
let _result = command::command_event_loop(command_request_rx, command_message_tx);
});
std::mem::drop(message_tx);
(message_rx, command_request_tx)
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(String, Vec<u8>)>> {
{
let mut command_options = app.command_options.write();
if !command_options.cmdline.is_empty() {
app.run_command()?;
// command_request_tx.send((app.command_options.clone(), app.text_orig.clone()))?;
}
if command_options.cmdline_position == 0 {
command_options.cmdline_position = command_options.cmdline.len() as u16;
}
}
loop { loop {
terminal.draw(|f| ui::draw(f, &app))?; terminal.draw(|f| ui::draw(f, &app))?;
match rx.recv()? { match app.message_rx.recv()? {
// match event::read()? { EventMessage::CrosstermEvent(crossterm_event) => {
EventMessage::CrosstermEvent(Event::Key(key)) => match key.code { let mut command_options = app.command_options.write();
KeyCode::Esc => {
// TODO: If there is any command line text, ask if the user is sure they want to quit match crossterm_event {
return Ok(None); Event::Key(key) => match key.code {
} KeyCode::Esc => {
KeyCode::Char(c) => { // TODO: If there is any command line text, ask if the user is sure they want to quit
app.cmdline.insert(app.cmdline_position as usize, c); return Ok(None);
app.cmdline_position = app.cmdline_position.saturating_add(1); }
if app.autorun { KeyCode::Char(c) => {
command::run(&mut app); let cmdline_position = command_options.cmdline_position as usize;
} command_options.cmdline.insert(cmdline_position, c);
} command_options.cmdline_position =
KeyCode::Backspace => { command_options.cmdline_position.saturating_add(1);
if app.cmdline_position > 0 {
app.cmdline_position = app.cmdline_position.saturating_sub(1); if command_options.autorun {
app.cmdline.remove(app.cmdline_position as usize); app.run_command()?;
}
}
KeyCode::Backspace => {
if command_options.cmdline_position > 0 {
command_options.cmdline_position =
command_options.cmdline_position.saturating_sub(1);
let cmdline_position = command_options.cmdline_position as usize;
command_options.cmdline.remove(cmdline_position);
}
if command_options.autorun {
app.run_command()?;
}
}
KeyCode::Delete => {
let cmdline_position = command_options.cmdline_position as usize;
if (cmdline_position) < command_options.cmdline.len() {
command_options.cmdline.remove(cmdline_position);
}
if command_options.autorun {
app.run_command()?;
}
}
KeyCode::End => {
command_options.cmdline_position = command_options.cmdline.len() as u16;
}
KeyCode::Home => {
command_options.cmdline_position = 0_u16;
}
KeyCode::Enter => {
// TODO: Do not clone here
return Ok(Some((
format!("{} {}", command_options.command, command_options.cmdline),
command_options.command_result.stdout.clone(),
)));
}
KeyCode::Left => {
command_options.cmdline_position =
command_options.cmdline_position.saturating_sub(1);
}
KeyCode::Right => {
command_options.cmdline_position = std::cmp::min(
command_options.cmdline.len() as u16,
command_options.cmdline_position.saturating_add(1),
);
}
_ => {}
},
Event::Paste(data) => {
let cmdline_position = command_options.cmdline_position as usize;
command_options.cmdline.insert_str(cmdline_position, &data);
command_options.cmdline_position = command_options
.cmdline_position
.saturating_add(data.len() as u16);
if command_options.autorun {
app.run_command()?;
}
} }
if app.autorun { Event::FocusGained
command::run(&mut app); | Event::FocusLost
} | Event::Mouse(_)
} | Event::Resize(_, _) => {}
KeyCode::Delete => {
if (app.cmdline_position as usize) < app.cmdline.len() {
app.cmdline.remove(app.cmdline_position as usize);
}
if app.autorun {
command::run(&mut app);
}
}
KeyCode::End => {
app.cmdline_position = app.cmdline.len() as u16;
}
KeyCode::Home => {
app.cmdline_position = 0_u16;
}
KeyCode::Enter => {
return Ok(Some((
format!("{} {}", app.command, app.cmdline),
app.command_result.stdout,
)));
}
KeyCode::Left => {
app.cmdline_position = app.cmdline_position.saturating_sub(1);
}
KeyCode::Right => {
app.cmdline_position = std::cmp::min(
app.cmdline.len() as u16,
app.cmdline_position.saturating_add(1),
);
}
_ => {}
},
EventMessage::CrosstermEvent(Event::Paste(data)) => {
app.cmdline.insert_str(app.cmdline_position as usize, &data);
app.cmdline_position = app.cmdline_position.saturating_add(data.len() as u16);
if app.autorun {
command::run(&mut app);
} }
} }
EventMessage::CrosstermEvent(Event::FocusGained) EventMessage::CommandCompleted(command_completed) => {
| EventMessage::CrosstermEvent(Event::FocusLost) let mut command_options = app.command_options.write();
| EventMessage::CrosstermEvent(Event::Mouse(_)) match command_completed {
| EventMessage::CrosstermEvent(Event::Resize(_, _)) => {} command::CommandCompleted::Success(c) => {
command_options.command_result = c;
}
command::CommandCompleted::Failure(stderr) => {
command_options.command_result.status_success = false;
command_options.command_result.stderr = stderr;
}
}
}
// TODO // TODO
_ => {} _ => {
bail!("Unknown event type happened");
}
} }
} }
} }

View File

@ -30,6 +30,8 @@ use crossterm::{
}; };
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) { pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) {
let command_options = app.command_options.read();
let vertical_chunks = Layout::default() let vertical_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(3)].as_ref()) .constraints([Constraint::Min(3), Constraint::Length(3)].as_ref())
@ -46,17 +48,17 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) {
chunks[0], chunks[0],
); );
f.render_widget( f.render_widget(
Paragraph::new(app.cmdline.as_ref()).block( Paragraph::new(command_options.cmdline.as_ref()).block(
Block::default() Block::default()
.title(format!( .title(format!(
"Cmdline ({}{}{})", "Cmdline ({}{}{})",
app.command, command_options.command,
if app.hidden_options.is_empty() { if command_options.hidden_options.is_empty() {
"" ""
} else { } else {
" " " "
}, },
app.hidden_options.join(" ") command_options.hidden_options.join(" ")
)) ))
.borders(Borders::ALL), .borders(Borders::ALL),
), ),
@ -64,18 +66,18 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) {
); );
// Render the output in the outpout region // Render the output in the outpout region
ui_output(f, chunks[1], app); ui_output(f, chunks[1], &command_options.command_result);
f.set_cursor( f.set_cursor(
vertical_chunks[1].x + app.cmdline_position + 1, vertical_chunks[1].x + command_options.cmdline_position + 1,
vertical_chunks[1].y + 1, vertical_chunks[1].y + 1,
); );
} }
fn ui_output<B: Backend>(f: &mut Frame<B>, output_region: Rect, app: &App) { fn ui_output<B: Backend>(f: &mut Frame<B>, output_region: Rect, command_result: &CommandResult) {
if app.command_result.status_success { if command_result.status_success {
f.render_widget( f.render_widget(
Paragraph::new(bytes_to_text(&app.command_result.stdout)) Paragraph::new(bytes_to_text(&command_result.stdout))
.block(Block::default().title("New").borders(Borders::ALL)), .block(Block::default().title("New").borders(Borders::ALL)),
output_region, output_region,
); );
@ -86,12 +88,12 @@ fn ui_output<B: Backend>(f: &mut Frame<B>, output_region: Rect, app: &App) {
.split(output_region); .split(output_region);
f.render_widget( f.render_widget(
Paragraph::new(bytes_to_text(&app.command_result.stdout)) Paragraph::new(bytes_to_text(&command_result.stdout))
.block(Block::default().title("Output").borders(Borders::ALL)), .block(Block::default().title("Output").borders(Borders::ALL)),
chunks[0], chunks[0],
); );
f.render_widget( f.render_widget(
Paragraph::new(bytes_to_text(&app.command_result.stderr)) Paragraph::new(bytes_to_text(&command_result.stderr))
.block(Block::default().title("Error").borders(Borders::ALL)), .block(Block::default().title("Error").borders(Borders::ALL)),
chunks[1], chunks[1],
); );