Use ropey

This commit is contained in:
Austen Adler 2022-12-20 21:35:11 -05:00
parent 853770613e
commit d12e3bb43a
6 changed files with 316 additions and 54 deletions

129
Cargo.lock generated
View File

@ -298,7 +298,10 @@ dependencies = [
"crossbeam", "crossbeam",
"crossterm", "crossterm",
"parking_lot", "parking_lot",
"ropey",
"shellwords", "shellwords",
"tracing",
"tracing-subscriber",
"tui", "tui",
] ]
@ -364,6 +367,16 @@ dependencies = [
"version_check", "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]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.3.3" version = "0.3.3"
@ -384,6 +397,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.0" version = "0.3.0"
@ -399,6 +418,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -470,6 +495,12 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -582,6 +613,16 @@ version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -619,6 +660,15 @@ dependencies = [
"opaque-debug", "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]] [[package]]
name = "shellwords" name = "shellwords"
version = "1.1.0" version = "1.1.0"
@ -681,6 +731,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "str_indices"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.105" version = "1.0.105"
@ -766,6 +822,73 @@ dependencies = [
"syn", "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]] [[package]]
name = "tui" name = "tui"
version = "0.19.0" version = "0.19.0"
@ -815,6 +938,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@ -14,3 +14,6 @@ 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" parking_lot = "0.12.1"
tracing = { version = "0.1.37", features = ["release_max_level_off"] }
tracing-subscriber = "0.3.16"
ropey = "1.5.0"

View File

@ -1,16 +1,20 @@
use crate::event::EventMessage; use crate::event::EventMessage;
use crate::CommandOptions; use crate::CommandOptions;
use ansi4tui::bytes_to_text;
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 parking_lot::RwLock;
use ropey::Rope;
use std::collections::hash_map::DefaultHasher;
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
io::Write, io::Write,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use tui::widgets::Paragraph;
pub type CommandRequest = (Arc<RwLock<CommandOptions>>, Arc<String>); pub type CommandRequest = (Arc<RwLock<CommandOptions>>, Arc<String>);
use crate::App; use crate::App;
@ -18,22 +22,24 @@ use crate::App;
#[derive(Debug)] #[derive(Debug)]
pub enum CommandCompleted { pub enum CommandCompleted {
Success(CommandResult), Success(CommandResult),
Failure(Vec<u8>), Failure(Rope),
} }
#[derive(Debug)] #[derive(Debug)]
pub struct CommandResult { pub struct CommandResult {
pub status_success: bool, pub status_success: bool,
pub stdout: Vec<u8>, pub stdout: Rope,
pub stderr: Vec<u8>, pub stderr: Rope,
} }
// TODO: Result?
impl Default for CommandResult { impl Default for CommandResult {
fn default() -> Self { fn default() -> Self {
Self { Self {
status_success: true, status_success: true,
stdout: vec![], // TODO: Option<Rope>
stderr: vec![], stdout: Rope::from_str(""),
stderr: Rope::from_str(""),
} }
} }
} }
@ -49,13 +55,14 @@ pub fn command_event_loop(
match run_inner(command_request) { match run_inner(command_request) {
Ok(c) => { Ok(c) => {
// If there was no stdout and the command failed, don't touch stdout // 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) CommandCompleted::Failure(c.stderr)
} else { } else {
// CommandCompleted::Success((c, FormattedCommandResult::from(&c)))
CommandCompleted::Success(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<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;
// }
// }
// 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> { fn run_inner(command_request: CommandRequest) -> Result<CommandResult> {
// Spawn the child // Spawn the child
let mut child = { let mut child = {
@ -127,7 +116,7 @@ fn run_inner(command_request: CommandRequest) -> Result<CommandResult> {
Ok(CommandResult { Ok(CommandResult {
status_success: output.status.success(), status_success: output.status.success(),
stdout: output.stdout, stdout: Rope::from_reader(&output.stdout[..])?,
stderr: output.stderr, stderr: Rope::from_reader(&output.stderr[..])?,
}) })
} }

View File

@ -5,11 +5,13 @@
mod command; mod command;
mod event; mod event;
mod ui; mod ui;
mod util;
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::Context;
use anyhow::Result; use anyhow::Result;
use command::CommandCompleted;
use command::CommandRequest; use command::CommandRequest;
use command::CommandResult; use command::CommandResult;
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
@ -18,6 +20,8 @@ use crossterm::event::DisableBracketedPaste;
use crossterm::event::EnableBracketedPaste; use crossterm::event::EnableBracketedPaste;
use event::EventMessage; use event::EventMessage;
use parking_lot::RwLock; use parking_lot::RwLock;
use ropey::Rope;
use std::fs::File;
use std::str::FromStr; use std::str::FromStr;
use std::{ use std::{
io::{self, Write}, io::{self, Write},
@ -26,6 +30,10 @@ use std::{
thread, thread,
time::Duration, time::Duration,
}; };
use tracing::instrument;
use tracing::Level;
use tracing_subscriber::Layer;
use tracing_subscriber::{filter, prelude::*};
use tui::text::Text; use tui::text::Text;
use tui::{ use tui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
@ -33,6 +41,8 @@ use tui::{
widgets::{Block, Borders, Paragraph, Widget}, widgets::{Block, Borders, Paragraph, Widget},
Frame, Terminal, Frame, Terminal,
}; };
use ui::RenderState;
use ui::RenderStates;
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
@ -69,6 +79,9 @@ pub struct App {
/// Original text /// Original text
text_orig: Arc<String>, text_orig: Arc<String>,
/// Original text (for ui)
text_orig_rope: Rope,
// text_orig_formatted: 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 /// The list of options for a command, given in an RwLock so the command runner can update it
command_options: Arc<RwLock<CommandOptions>>, command_options: Arc<RwLock<CommandOptions>>,
@ -78,6 +91,9 @@ pub struct App {
/// The command request transmitter /// The command request transmitter
command_request_tx: Sender<CommandRequest>, command_request_tx: Sender<CommandRequest>,
/// The rendering state of the stdin/stdout/stderr
render_states: RwLock<RenderStates>,
} }
impl CommandOptions { impl CommandOptions {
@ -118,7 +134,7 @@ impl CommandOptions {
..defaults ..defaults
}, },
Template::Perl => Self { Template::Perl => Self {
cmdline_position: 10_u16, cmdline_position: 9_u16,
cmdline: String::from("-p -e 's///'"), cmdline: String::from("-p -e 's///'"),
..defaults ..defaults
}, },
@ -126,6 +142,7 @@ impl CommandOptions {
} }
} }
// impl App<'_> {
impl App { impl App {
/// Constructs a new instance of `App` /// Constructs a new instance of `App`
/// ///
@ -141,11 +158,14 @@ impl App {
input: String, input: String,
template: &Template, template: &Template,
) -> Self { ) -> Self {
let text_orig_rope = Rope::from_str(&input);
Self { Self {
text_orig: Arc::new(input), text_orig: Arc::new(input),
text_orig_rope,
command_options: Arc::new(RwLock::new(CommandOptions::from_template(template))), command_options: Arc::new(RwLock::new(CommandOptions::from_template(template))),
message_rx, message_rx,
command_request_tx, command_request_tx,
render_states: RwLock::new(RenderStates::default()),
} }
} }
@ -176,7 +196,7 @@ impl FromStr for Template {
"sed" => Ok(Self::Sed), "sed" => Ok(Self::Sed),
"awk" => Ok(Self::Awk), "awk" => Ok(Self::Awk),
"perl" => Ok(Self::Perl), "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")), e => Err(anyhow!("{e} is not a supported command")),
} }
} }
@ -198,6 +218,8 @@ impl Template {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
enable_tracing();
// Error if we aren't getting any stdin // Error if we aren't getting any stdin
if atty::is(atty::Stream::Stdin) { if atty::is(atty::Stream::Stdin) {
bail!("You must send stdin to this command"); bail!("You must send stdin to this command");
@ -249,9 +271,8 @@ fn main() -> Result<()> {
let res = res?; let res = res?;
if let Some(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::stderr().write_all(b"\n")?;
std::io::stdout().write_all(&res.1)?;
} }
Ok(()) Ok(())
@ -279,7 +300,9 @@ fn init_message_passing() -> (Receiver<EventMessage>, Sender<CommandRequest>) {
(message_rx, command_request_tx) (message_rx, command_request_tx)
} }
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(String, Vec<u8>)>> { #[allow(clippy::too_many_lines)]
#[instrument(skip(terminal, app))]
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<String>> {
// When starting the app, ensure the command runs at least once // When starting the app, ensure the command runs at least once
{ {
let mut command_options = app.command_options.write(); let mut command_options = app.command_options.write();
@ -294,16 +317,18 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(S
} }
loop { loop {
tracing::event!(Level::INFO, "Starting draw");
terminal.draw(|f| ui::draw(f, &app))?; terminal.draw(|f| ui::draw(f, &app))?;
tracing::event!(Level::INFO, "Waiting for event");
match app.message_rx.recv()? { match app.message_rx.recv()? {
EventMessage::CrosstermEvent(crossterm_event) => { EventMessage::CrosstermEvent(crossterm_event) => {
tracing::event!(Level::INFO, "Got crossterm event");
let mut command_options = app.command_options.write(); let mut command_options = app.command_options.write();
match crossterm_event { match crossterm_event {
Event::Key(key) => match key.code { Event::Key(key) => match key.code {
KeyCode::Esc => { KeyCode::Esc => {
// TODO: If there is any command line text, ask if the user is sure they want to quit
return Ok(None); return Ok(None);
} }
KeyCode::Char(c) => { KeyCode::Char(c) => {
@ -346,10 +371,9 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(S
command_options.cmdline_position = 0_u16; command_options.cmdline_position = 0_u16;
} }
KeyCode::Enter => { KeyCode::Enter => {
// TODO: Do not clone here return Ok(Some(format!(
return Ok(Some(( "{} {}",
format!("{} {}", command_options.command, command_options.cmdline), command_options.command, command_options.cmdline
command_options.command_result.stdout.clone(),
))); )));
} }
KeyCode::Left => { KeyCode::Left => {
@ -384,12 +408,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(S
} }
EventMessage::CommandCompleted(command_completed) => { EventMessage::CommandCompleted(command_completed) => {
tracing::event!(Level::INFO, "Got command completed event event");
let mut command_options = app.command_options.write(); let mut command_options = app.command_options.write();
match command_completed { match command_completed {
command::CommandCompleted::Success(c) => { CommandCompleted::Success(c) => {
command_options.command_result = c; command_options.command_result = c;
} }
command::CommandCompleted::Failure(stderr) => { CommandCompleted::Failure(stderr) => {
command_options.command_result.status_success = false; command_options.command_result.status_success = false;
command_options.command_result.stderr = stderr; command_options.command_result.stderr = stderr;
} }
@ -403,3 +428,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(S
} }
} }
} }
fn enable_tracing() {
let file = File::create("debug.log").unwrap();
let debug_log = tracing_subscriber::fmt::layer().with_writer(Arc::new(file));
tracing_subscriber::registry()
.with(debug_log.with_filter(filter::LevelFilter::INFO))
.init();
}

126
src/ui.rs
View File

@ -1,4 +1,5 @@
use crate::command; use crate::command;
use crate::util;
use crate::App; use crate::App;
use ansi4tui::bytes_to_text; use ansi4tui::bytes_to_text;
use anyhow::anyhow; use anyhow::anyhow;
@ -7,6 +8,9 @@ use anyhow::Result;
use command::CommandResult; use command::CommandResult;
use crossterm::event::DisableBracketedPaste; use crossterm::event::DisableBracketedPaste;
use crossterm::event::EnableBracketedPaste; use crossterm::event::EnableBracketedPaste;
use ropey::RopeSlice;
use std::cmp::min;
use std::io::Read;
use std::str::FromStr; use std::str::FromStr;
use std::{ use std::{
io::{self, Write}, io::{self, Write},
@ -15,6 +19,9 @@ use std::{
thread, thread,
time::Duration, time::Duration,
}; };
use tracing::event;
use tracing::instrument;
use tracing::Level;
use tui::text::Text; use tui::text::Text;
use tui::{ use tui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
@ -24,13 +31,16 @@ use tui::{
}; };
use crossterm::{ use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
#[instrument(skip(f, app))]
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) { pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) {
event!(Level::INFO, "Acquiring lock");
let command_options = app.command_options.read(); let command_options = app.command_options.read();
let mut render_states = app.render_states.write();
let vertical_chunks = Layout::default() let vertical_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@ -42,11 +52,16 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(vertical_chunks[0]); .split(vertical_chunks[0]);
f.render_widget( event!(Level::INFO, "Rendering orig");
Paragraph::new(app.text_orig.as_str()) // lazy_render_rope_slice(chunks[0], render_states.stdout)
.block(Block::default().title("Orig").borders(Borders::ALL)), render_states.stdout = lazy_render_rope_slice(
f,
chunks[0], chunks[0],
render_states.stdin.as_ref(),
app.text_orig_rope.slice(..),
"Output",
); );
event!(Level::INFO, "Rendering textbox");
f.render_widget( f.render_widget(
Paragraph::new(command_options.cmdline.as_ref()).block( Paragraph::new(command_options.cmdline.as_ref()).block(
Block::default() Block::default()
@ -66,7 +81,12 @@ 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], &command_options.command_result); ui_output(
f,
chunks[1],
&mut render_states,
&command_options.command_result,
);
f.set_cursor( f.set_cursor(
vertical_chunks[1].x + command_options.cmdline_position + 1, vertical_chunks[1].x + command_options.cmdline_position + 1,
@ -74,12 +94,21 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &App) {
); );
} }
fn ui_output<B: Backend>(f: &mut Frame<B>, output_region: Rect, command_result: &CommandResult) { #[instrument(skip(f, command_result))]
fn ui_output<B: Backend>(
f: &mut Frame<B>,
output_region: Rect,
render_states: &mut RenderStates,
command_result: &CommandResult,
) {
if command_result.status_success { if command_result.status_success {
f.render_widget( event!(Level::INFO, "Rendering output");
Paragraph::new(bytes_to_text(&command_result.stdout)) render_states.stdout = lazy_render_rope_slice(
.block(Block::default().title("Output").borders(Borders::ALL)), f,
output_region, output_region,
render_states.stdout.as_ref(),
command_result.stdout.slice(..),
"Output",
); );
} else { } else {
let chunks = Layout::default() let chunks = Layout::default()
@ -87,15 +116,82 @@ fn ui_output<B: Backend>(f: &mut Frame<B>, output_region: Rect, command_result:
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(output_region); .split(output_region);
f.render_widget( event!(Level::INFO, "Rendering output (split)");
Paragraph::new(bytes_to_text(&command_result.stdout)) render_states.stdout = lazy_render_rope_slice(
.block(Block::default().title("Output").borders(Borders::ALL)), f,
chunks[0], chunks[0],
render_states.stdout.as_ref(),
command_result.stdout.slice(..),
"Output",
); );
f.render_widget(
Paragraph::new(bytes_to_text(&command_result.stderr)) event!(Level::INFO, "Rendering err");
.block(Block::default().title("Error").borders(Borders::ALL)), render_states.stderr = lazy_render_rope_slice(
f,
chunks[1], 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<RenderState>,
pub stdout: Option<RenderState>,
pub stderr: Option<RenderState>,
}
#[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<B>,
output: Rect,
previous_state: Option<&RenderState>,
data: RopeSlice<'_>,
block_title: &'static str,
) -> Option<RenderState> {
let data = data
.lines()
.take(output.height as usize)
.flat_map(|l| l.bytes().collect::<Vec<u8>>())
.collect::<Vec<u8>>();
let current_state = RenderState {
size: output,
input_hash: util::hash_bytes(&data),
};
if let Some(ps) = previous_state {
if &current_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)
} }

11
src/util.rs Normal file
View File

@ -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()
}