diff --git a/Cargo.lock b/Cargo.lock index 621b2a5..38d7a82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,7 @@ dependencies = [ "atty", "crossbeam", "crossterm", + "parking_lot", "shellwords", "tui", ] diff --git a/Cargo.toml b/Cargo.toml index f7c76c8..ca7d19a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ ansi4tui = {path = "./ansi4tui/"} anyhow = "1.0.66" shellwords = "1.1.0" crossbeam = "0.8.2" +parking_lot = "0.12.1" diff --git a/src/command.rs b/src/command.rs index c10eab3..ba17219 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,18 +1,23 @@ use crate::event::EventMessage; +use crate::CommandOptions; use anyhow::bail; use anyhow::Context; use anyhow::Result; use crossbeam::channel::Receiver; use crossbeam::channel::Sender; +use parking_lot::RwLock; +use std::sync::Arc; use std::{ io::Write, process::{Command, Stdio}, }; +pub type CommandRequest = (Arc>, Arc); use crate::App; -pub struct CommandRequest { - cmdline: String, +pub enum CommandCompleted { + Success(CommandResult), + Failure(Vec), } pub struct CommandResult { @@ -31,46 +36,65 @@ impl Default for CommandResult { } } -// pub fn command_event_loop(command_request_receiver: Receiver,event_sender: Sender) -> Result<()> { -// loop { -// match command_request_receiver.recv() { -// Ok(command_request) => event_sender.send(EventMessage::CrosstermEvent(e))?, -// Err(e) => { -// event_sender.send(EventMessage::CrosstermError(e))?; -// bail!("Crossterm read error"); -// } -// } -// } -// } - -pub fn run(app: &mut App) { - 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; +pub fn command_event_loop( + command_request_receiver: Receiver, + event_sender: Sender, +) -> Result<()> { + loop { + match command_request_receiver.recv() { + 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() { + CommandCompleted::Failure(c.stderr) + } else { + CommandCompleted::Success(c) + } + } + Err(e) => CommandCompleted::Failure(e.to_string().as_bytes().to_vec()), + }, + ))?; + } + 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 { - let mut command = Command::new(&app.command); +// pub fn run(app: Arc) { +// 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 { + let request = command_request.0.read(); + + let mut command = Command::new(&request.command); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .args(&app.hidden_options); + .args(&request.hidden_options); - if app.wordsplit { - match shellwords::split(&app.cmdline) { + if request.wordsplit { + match shellwords::split(&request.cmdline) { Ok(a) => { command.args(a); } @@ -80,18 +104,18 @@ fn run_inner(app: &mut App) -> Result { } } else { // 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 text_orig_clone = app.text_orig.clone(); + let text_orig_clone = command_request.1.clone(); std::thread::spawn(move || { let _result = stdin.write_all(text_orig_clone.as_bytes()); }); // Collect the output - let output = child.wait_with_output().context("Failed to read stdout")?; + let output = child.wait_with_output()?; Ok(CommandResult { status_success: output.status.success(), diff --git a/src/event.rs b/src/event.rs index 51a50ae..31d71f3 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,12 +1,13 @@ -use crate::command::CommandRequest; +use crate::command::CommandCompleted; use anyhow::bail; use anyhow::Result; use crossbeam::channel::Sender; pub enum EventMessage { - CommandCompleted, + CommandCompleted(CommandCompleted), CrosstermEvent(crossterm::event::Event), CrosstermError(std::io::Error), + CommandLoopStopped, } pub fn crossterm_event_loop(event_sender: Sender) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 21b401e..2b4e14e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,16 @@ mod ui; use ansi4tui::bytes_to_text; use anyhow::anyhow; use anyhow::bail; +use anyhow::Context; use anyhow::Result; +use command::CommandRequest; use command::CommandResult; +use crossbeam::channel::Receiver; +use crossbeam::channel::Sender; use crossterm::event::DisableBracketedPaste; use crossterm::event::EnableBracketedPaste; use event::EventMessage; +use parking_lot::RwLock; use std::str::FromStr; use std::{ io::{self, Write}, @@ -35,8 +40,8 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -/// The state of the application -pub struct App { +/// The state of options for the command +pub struct CommandOptions { /// The actual command to be called command: String, @@ -46,94 +51,109 @@ pub struct App { /// The line the user inputs cmdline: String, - /// The position of the cursor - cmdline_position: u16, - - /// Original text - text_orig: Arc, - - /// The result of a command execution - command_result: CommandResult, - /// Should every keystroke transform the original text? autorun: bool, /// Should wordsplitting be enabled 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, + + // text_orig_formatted: Arc + /// The list of options for a command, given in an RwLock so the command runner can update it + command_options: Arc>, + + /// The receiver for events + message_rx: Receiver, + + /// The command request transmitter + command_request_tx: Sender, +} + +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 { + /// 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] - pub fn from_template(input: String, template: &Template) -> Self { - let text_orig = Arc::new(input); - let command_result = CommandResult::default(); - let command = template.command(); - let cmdline_position = 0; - let wordsplit = true; - - match template { - Template::Awk => Self { - cmdline: String::new(), - cmdline_position, - 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 from_template( + message_rx: Receiver, + command_request_tx: Sender, + input: String, + template: &Template, + ) -> Self { + Self { + text_orig: Arc::new(input), + command_options: Arc::new(RwLock::new(CommandOptions::from_template(template))), + message_rx, + command_request_tx, } } + + pub fn run_command(&self) -> Result<()> { + self.command_request_tx + .send((self.command_options.clone(), self.text_orig.clone()))?; + Ok(()) + } } pub enum Template { @@ -192,8 +212,16 @@ fn main() -> Result<()> { // Slurp all input 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 - 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()?; let mut stdout = io::stdout(); // execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -229,103 +257,148 @@ fn main() -> Result<()> { Ok(()) } -fn run_app( - terminal: &mut Terminal, - mut app: App, -) -> Result)>> { - 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); +fn init_message_passing() -> (Receiver, Sender) { + let (message_tx, message_rx) = crossbeam::channel::bounded(100); + let (command_request_tx, command_request_rx) = crossbeam::channel::bounded(100); // Start the event thread - let crossterm_tx = tx.clone(); + let crossterm_message_tx = message_tx.clone(); 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(terminal: &mut Terminal, app: App) -> Result)>> { + { + 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 { terminal.draw(|f| ui::draw(f, &app))?; - match rx.recv()? { - // match event::read()? { - EventMessage::CrosstermEvent(Event::Key(key)) => match key.code { - KeyCode::Esc => { - // TODO: If there is any command line text, ask if the user is sure they want to quit - return Ok(None); - } - KeyCode::Char(c) => { - app.cmdline.insert(app.cmdline_position as usize, c); - app.cmdline_position = app.cmdline_position.saturating_add(1); - if app.autorun { - command::run(&mut app); - } - } - KeyCode::Backspace => { - if app.cmdline_position > 0 { - app.cmdline_position = app.cmdline_position.saturating_sub(1); - app.cmdline.remove(app.cmdline_position as usize); + match app.message_rx.recv()? { + EventMessage::CrosstermEvent(crossterm_event) => { + let mut command_options = app.command_options.write(); + + match crossterm_event { + Event::Key(key) => match key.code { + KeyCode::Esc => { + // TODO: If there is any command line text, ask if the user is sure they want to quit + return Ok(None); + } + KeyCode::Char(c) => { + let cmdline_position = command_options.cmdline_position as usize; + command_options.cmdline.insert(cmdline_position, c); + command_options.cmdline_position = + command_options.cmdline_position.saturating_add(1); + + if command_options.autorun { + 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 { - command::run(&mut app); - } - } - 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); + Event::FocusGained + | Event::FocusLost + | Event::Mouse(_) + | Event::Resize(_, _) => {} } } - EventMessage::CrosstermEvent(Event::FocusGained) - | EventMessage::CrosstermEvent(Event::FocusLost) - | EventMessage::CrosstermEvent(Event::Mouse(_)) - | EventMessage::CrosstermEvent(Event::Resize(_, _)) => {} + EventMessage::CommandCompleted(command_completed) => { + let mut command_options = app.command_options.write(); + match command_completed { + 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 - _ => {} + _ => { + bail!("Unknown event type happened"); + } } } } diff --git a/src/ui.rs b/src/ui.rs index fc9d922..cda9b77 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -30,6 +30,8 @@ use crossterm::{ }; pub fn draw(f: &mut Frame, app: &App) { + let command_options = app.command_options.read(); + let vertical_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(3), Constraint::Length(3)].as_ref()) @@ -46,17 +48,17 @@ pub fn draw(f: &mut Frame, app: &App) { chunks[0], ); f.render_widget( - Paragraph::new(app.cmdline.as_ref()).block( + Paragraph::new(command_options.cmdline.as_ref()).block( Block::default() .title(format!( "Cmdline ({}{}{})", - app.command, - if app.hidden_options.is_empty() { + command_options.command, + if command_options.hidden_options.is_empty() { "" } else { " " }, - app.hidden_options.join(" ") + command_options.hidden_options.join(" ") )) .borders(Borders::ALL), ), @@ -64,18 +66,18 @@ pub fn draw(f: &mut Frame, app: &App) { ); // 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( - vertical_chunks[1].x + app.cmdline_position + 1, + vertical_chunks[1].x + command_options.cmdline_position + 1, vertical_chunks[1].y + 1, ); } -fn ui_output(f: &mut Frame, output_region: Rect, app: &App) { - if app.command_result.status_success { +fn ui_output(f: &mut Frame, output_region: Rect, command_result: &CommandResult) { + if command_result.status_success { 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)), output_region, ); @@ -86,12 +88,12 @@ fn ui_output(f: &mut Frame, output_region: Rect, app: &App) { .split(output_region); 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)), chunks[0], ); 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)), chunks[1], );