diff --git a/Cargo.lock b/Cargo.lock index 38d7a82..880805d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,7 +298,10 @@ dependencies = [ "crossbeam", "crossterm", "parking_lot", + "ropey", "shellwords", + "tracing", + "tracing-subscriber", "tui", ] @@ -364,6 +367,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -384,6 +397,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -399,6 +418,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -470,6 +495,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -582,6 +613,16 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "ropey" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd22239fafefc42138ca5da064f3c17726a80d2379d817a3521240e78dd0064" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -619,6 +660,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shellwords" version = "1.1.0" @@ -681,6 +731,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "str_indices" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" + [[package]] name = "syn" version = "1.0.105" @@ -766,6 +822,73 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + [[package]] name = "tui" version = "0.19.0" @@ -815,6 +938,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index ca7d19a..e0c30f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,6 @@ anyhow = "1.0.66" shellwords = "1.1.0" crossbeam = "0.8.2" parking_lot = "0.12.1" +tracing = { version = "0.1.37", features = ["release_max_level_off"] } +tracing-subscriber = "0.3.16" +ropey = "1.5.0" diff --git a/src/command.rs b/src/command.rs index d9440fd..142d627 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,16 +1,20 @@ use crate::event::EventMessage; use crate::CommandOptions; +use ansi4tui::bytes_to_text; use anyhow::bail; use anyhow::Context; use anyhow::Result; use crossbeam::channel::Receiver; use crossbeam::channel::Sender; use parking_lot::RwLock; +use ropey::Rope; +use std::collections::hash_map::DefaultHasher; use std::sync::Arc; use std::{ io::Write, process::{Command, Stdio}, }; +use tui::widgets::Paragraph; pub type CommandRequest = (Arc>, Arc); use crate::App; @@ -18,22 +22,24 @@ use crate::App; #[derive(Debug)] pub enum CommandCompleted { Success(CommandResult), - Failure(Vec), + Failure(Rope), } #[derive(Debug)] pub struct CommandResult { pub status_success: bool, - pub stdout: Vec, - pub stderr: Vec, + pub stdout: Rope, + pub stderr: Rope, } +// TODO: Result? impl Default for CommandResult { fn default() -> Self { Self { status_success: true, - stdout: vec![], - stderr: vec![], + // TODO: Option + stdout: Rope::from_str(""), + stderr: Rope::from_str(""), } } } @@ -49,13 +55,14 @@ pub fn command_event_loop( 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() { + if !c.status_success && c.stdout.len_bytes() == 0 { CommandCompleted::Failure(c.stderr) } else { + // CommandCompleted::Success((c, FormattedCommandResult::from(&c))) CommandCompleted::Success(c) } } - Err(e) => CommandCompleted::Failure(e.to_string().as_bytes().to_vec()), + Err(e) => CommandCompleted::Failure(Rope::from_str(&e.to_string())), }, ))?; } @@ -67,24 +74,6 @@ pub fn command_event_loop( } } -// 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 { // Spawn the child let mut child = { @@ -127,7 +116,7 @@ fn run_inner(command_request: CommandRequest) -> Result { Ok(CommandResult { status_success: output.status.success(), - stdout: output.stdout, - stderr: output.stderr, + stdout: Rope::from_reader(&output.stdout[..])?, + stderr: Rope::from_reader(&output.stderr[..])?, }) } diff --git a/src/main.rs b/src/main.rs index 082e928..27c7158 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,11 +5,13 @@ mod command; mod event; mod ui; +mod util; use ansi4tui::bytes_to_text; use anyhow::anyhow; use anyhow::bail; use anyhow::Context; use anyhow::Result; +use command::CommandCompleted; use command::CommandRequest; use command::CommandResult; use crossbeam::channel::Receiver; @@ -18,6 +20,8 @@ use crossterm::event::DisableBracketedPaste; use crossterm::event::EnableBracketedPaste; use event::EventMessage; use parking_lot::RwLock; +use ropey::Rope; +use std::fs::File; use std::str::FromStr; use std::{ io::{self, Write}, @@ -26,6 +30,10 @@ use std::{ thread, time::Duration, }; +use tracing::instrument; +use tracing::Level; +use tracing_subscriber::Layer; +use tracing_subscriber::{filter, prelude::*}; use tui::text::Text; use tui::{ backend::{Backend, CrosstermBackend}, @@ -33,6 +41,8 @@ use tui::{ widgets::{Block, Borders, Paragraph, Widget}, Frame, Terminal, }; +use ui::RenderState; +use ui::RenderStates; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, @@ -69,6 +79,9 @@ pub struct App { /// Original text text_orig: Arc, + /// Original text (for ui) + text_orig_rope: Rope, + // 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>, @@ -78,6 +91,9 @@ pub struct App { /// The command request transmitter command_request_tx: Sender, + + /// The rendering state of the stdin/stdout/stderr + render_states: RwLock, } impl CommandOptions { @@ -118,7 +134,7 @@ impl CommandOptions { ..defaults }, Template::Perl => Self { - cmdline_position: 10_u16, + cmdline_position: 9_u16, cmdline: String::from("-p -e 's///'"), ..defaults }, @@ -126,6 +142,7 @@ impl CommandOptions { } } +// impl App<'_> { impl App { /// Constructs a new instance of `App` /// @@ -141,11 +158,14 @@ impl App { input: String, template: &Template, ) -> Self { + let text_orig_rope = Rope::from_str(&input); Self { text_orig: Arc::new(input), + text_orig_rope, command_options: Arc::new(RwLock::new(CommandOptions::from_template(template))), message_rx, command_request_tx, + render_states: RwLock::new(RenderStates::default()), } } @@ -176,7 +196,7 @@ impl FromStr for Template { "sed" => Ok(Self::Sed), "awk" => Ok(Self::Awk), "perl" => Ok(Self::Perl), - s @ "sh" | s @ "bash" | s @ "zsh" | s @ "dash" => Ok(Self::Sh(s.to_string())), + s @ ("sh" | "bash" | "zsh" | "dash") => Ok(Self::Sh(s.to_string())), e => Err(anyhow!("{e} is not a supported command")), } } @@ -198,6 +218,8 @@ impl Template { } fn main() -> Result<()> { + enable_tracing(); + // Error if we aren't getting any stdin if atty::is(atty::Stream::Stdin) { bail!("You must send stdin to this command"); @@ -249,9 +271,8 @@ fn main() -> Result<()> { let res = res?; if let Some(res) = res { - std::io::stderr().write_all(res.0.as_bytes())?; + std::io::stderr().write_all(res.as_bytes())?; std::io::stderr().write_all(b"\n")?; - std::io::stdout().write_all(&res.1)?; } Ok(()) @@ -279,7 +300,9 @@ fn init_message_passing() -> (Receiver, Sender) { (message_rx, command_request_tx) } -fn run_app(terminal: &mut Terminal, app: App) -> Result)>> { +#[allow(clippy::too_many_lines)] +#[instrument(skip(terminal, app))] +fn run_app(terminal: &mut Terminal, app: App) -> Result> { // When starting the app, ensure the command runs at least once { let mut command_options = app.command_options.write(); @@ -294,16 +317,18 @@ fn run_app(terminal: &mut Terminal, app: App) -> Result { + tracing::event!(Level::INFO, "Got 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) => { @@ -346,10 +371,9 @@ fn run_app(terminal: &mut Terminal, app: App) -> Result { - // TODO: Do not clone here - return Ok(Some(( - format!("{} {}", command_options.command, command_options.cmdline), - command_options.command_result.stdout.clone(), + return Ok(Some(format!( + "{} {}", + command_options.command, command_options.cmdline ))); } KeyCode::Left => { @@ -384,12 +408,13 @@ fn run_app(terminal: &mut Terminal, app: App) -> Result { + tracing::event!(Level::INFO, "Got command completed event event"); let mut command_options = app.command_options.write(); match command_completed { - command::CommandCompleted::Success(c) => { + CommandCompleted::Success(c) => { command_options.command_result = c; } - command::CommandCompleted::Failure(stderr) => { + CommandCompleted::Failure(stderr) => { command_options.command_result.status_success = false; command_options.command_result.stderr = stderr; } @@ -403,3 +428,12 @@ fn run_app(terminal: &mut Terminal, app: App) -> Result(f: &mut Frame, app: &App) { + event!(Level::INFO, "Acquiring lock"); let command_options = app.command_options.read(); + let mut render_states = app.render_states.write(); let vertical_chunks = Layout::default() .direction(Direction::Vertical) @@ -42,11 +52,16 @@ pub fn draw(f: &mut Frame, app: &App) { .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(vertical_chunks[0]); - f.render_widget( - Paragraph::new(app.text_orig.as_str()) - .block(Block::default().title("Orig").borders(Borders::ALL)), + event!(Level::INFO, "Rendering orig"); + // lazy_render_rope_slice(chunks[0], render_states.stdout) + render_states.stdout = lazy_render_rope_slice( + f, chunks[0], + render_states.stdin.as_ref(), + app.text_orig_rope.slice(..), + "Output", ); + event!(Level::INFO, "Rendering textbox"); f.render_widget( Paragraph::new(command_options.cmdline.as_ref()).block( Block::default() @@ -66,7 +81,12 @@ pub fn draw(f: &mut Frame, app: &App) { ); // Render the output in the outpout region - ui_output(f, chunks[1], &command_options.command_result); + ui_output( + f, + chunks[1], + &mut render_states, + &command_options.command_result, + ); f.set_cursor( vertical_chunks[1].x + command_options.cmdline_position + 1, @@ -74,12 +94,21 @@ pub fn draw(f: &mut Frame, app: &App) { ); } -fn ui_output(f: &mut Frame, output_region: Rect, command_result: &CommandResult) { +#[instrument(skip(f, command_result))] +fn ui_output( + f: &mut Frame, + output_region: Rect, + render_states: &mut RenderStates, + command_result: &CommandResult, +) { if command_result.status_success { - f.render_widget( - Paragraph::new(bytes_to_text(&command_result.stdout)) - .block(Block::default().title("Output").borders(Borders::ALL)), + event!(Level::INFO, "Rendering output"); + render_states.stdout = lazy_render_rope_slice( + f, output_region, + render_states.stdout.as_ref(), + command_result.stdout.slice(..), + "Output", ); } else { let chunks = Layout::default() @@ -87,15 +116,82 @@ fn ui_output(f: &mut Frame, output_region: Rect, command_result: .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(output_region); - f.render_widget( - Paragraph::new(bytes_to_text(&command_result.stdout)) - .block(Block::default().title("Output").borders(Borders::ALL)), + event!(Level::INFO, "Rendering output (split)"); + render_states.stdout = lazy_render_rope_slice( + f, chunks[0], + render_states.stdout.as_ref(), + command_result.stdout.slice(..), + "Output", ); - f.render_widget( - Paragraph::new(bytes_to_text(&command_result.stderr)) - .block(Block::default().title("Error").borders(Borders::ALL)), + + event!(Level::INFO, "Rendering err"); + render_states.stderr = lazy_render_rope_slice( + f, chunks[1], + render_states.stderr.as_ref(), + command_result.stderr.slice(..), + "Stderr", ); + + // f.render_widget( + // Paragraph::new(bytes_to_text(&command_result.stdout.bytes().collect())) + // .block(Block::default().title("Output").borders(Borders::ALL)), + // chunks[0], + // ); + // f.render_widget( + // Paragraph::new(bytes_to_text(&command_result.stderr.bytes().collect())) + // .block(Block::default().title("Error").borders(Borders::ALL)), + // chunks[1], + // ); } + event!(Level::INFO, "Done rendering out"); +} + +#[derive(Default, Debug)] +pub struct RenderStates { + pub stdin: Option, + pub stdout: Option, + pub stderr: Option, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct RenderState { + pub size: Rect, + pub input_hash: u64, +} + +#[instrument(skip(f, data))] +fn lazy_render_rope_slice<'a, B: Backend>( + f: &mut Frame, + output: Rect, + previous_state: Option<&RenderState>, + data: RopeSlice<'_>, + block_title: &'static str, +) -> Option { + let data = data + .lines() + .take(output.height as usize) + .flat_map(|l| l.bytes().collect::>()) + .collect::>(); + + let current_state = RenderState { + size: output, + input_hash: util::hash_bytes(&data), + }; + + if let Some(ps) = previous_state { + if ¤t_state == ps { + event!(Level::INFO, "Not rendering: {current_state:?}"); + // return Some(current_state); + } + } + + f.render_widget( + Paragraph::new(bytes_to_text(&data)) + .block(Block::default().title(block_title).borders(Borders::ALL)), + output, + ); + + Some(current_state) } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..bdfb0ae --- /dev/null +++ b/src/util.rs @@ -0,0 +1,11 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; + +use ropey::RopeSlice; + +pub fn hash_bytes(r: &[u8]) -> u64 { + let mut s = DefaultHasher::new(); + r.hash(&mut s); + s.finish() +}