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",
"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"

View File

@ -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"

View File

@ -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<RwLock<CommandOptions>>, Arc<String>);
use crate::App;
@ -18,22 +22,24 @@ use crate::App;
#[derive(Debug)]
pub enum CommandCompleted {
Success(CommandResult),
Failure(Vec<u8>),
Failure(Rope),
}
#[derive(Debug)]
pub struct CommandResult {
pub status_success: bool,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
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<Rope>
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<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> {
// Spawn the child
let mut child = {
@ -127,7 +116,7 @@ fn run_inner(command_request: CommandRequest) -> Result<CommandResult> {
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[..])?,
})
}

View File

@ -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<String>,
/// Original text (for ui)
text_orig_rope: Rope,
// 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>>,
@ -78,6 +91,9 @@ pub struct App {
/// The command request transmitter
command_request_tx: Sender<CommandRequest>,
/// The rendering state of the stdin/stdout/stderr
render_states: RwLock<RenderStates>,
}
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<EventMessage>, Sender<CommandRequest>) {
(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
{
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 {
tracing::event!(Level::INFO, "Starting draw");
terminal.draw(|f| ui::draw(f, &app))?;
tracing::event!(Level::INFO, "Waiting for event");
match app.message_rx.recv()? {
EventMessage::CrosstermEvent(crossterm_event) => {
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<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(S
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(),
return Ok(Some(format!(
"{} {}",
command_options.command, command_options.cmdline
)));
}
KeyCode::Left => {
@ -384,12 +408,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: App) -> Result<Option<(S
}
EventMessage::CommandCompleted(command_completed) => {
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<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::util;
use crate::App;
use ansi4tui::bytes_to_text;
use anyhow::anyhow;
@ -7,6 +8,9 @@ use anyhow::Result;
use command::CommandResult;
use crossterm::event::DisableBracketedPaste;
use crossterm::event::EnableBracketedPaste;
use ropey::RopeSlice;
use std::cmp::min;
use std::io::Read;
use std::str::FromStr;
use std::{
io::{self, Write},
@ -15,6 +19,9 @@ use std::{
thread,
time::Duration,
};
use tracing::event;
use tracing::instrument;
use tracing::Level;
use tui::text::Text;
use tui::{
backend::{Backend, CrosstermBackend},
@ -24,13 +31,16 @@ use tui::{
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
#[instrument(skip(f, 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 mut render_states = app.render_states.write();
let vertical_chunks = Layout::default()
.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())
.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<B: Backend>(f: &mut Frame<B>, 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<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 {
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<B: Backend>(f: &mut Frame<B>, 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<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()
}