From 8667f0737ba21032eee42b47163e09a0c716715b Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 15:25:08 +0100 Subject: [PATCH 01/19] added interactive flag and prepared for mode change --- Cargo.lock | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/main.rs | 34 ++++-- 3 files changed, 347 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e467b03..164f5b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "anstream" version = "0.6.11" @@ -50,6 +68,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + [[package]] name = "block-buffer" version = "0.10.4" @@ -59,6 +95,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cfg-if" version = "1.0.0" @@ -96,7 +138,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -130,6 +172,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -150,6 +217,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "generic-array" version = "0.14.7" @@ -160,12 +233,37 @@ dependencies = [ "version_check", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -178,18 +276,84 @@ version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "lru" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2994eeba8ed550fd9b47a0b38f0242bc3344e496483c6180b69139cc2fa5d1d7" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pest" version = "2.7.6" @@ -221,7 +385,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -253,6 +417,40 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb" +dependencies = [ + "bitflags 2.4.2", + "cassowary", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "same-file" version = "1.0.6" @@ -262,6 +460,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sha2" version = "0.10.8" @@ -273,18 +477,97 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + [[package]] name = "smawk" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "stability" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -302,9 +585,11 @@ version = "0.2.0" dependencies = [ "clap", "colored", + "crossterm", "lazy_static", "pest", "pest_derive", + "ratatui", "textwrap", "walkdir", ] @@ -337,7 +622,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -364,6 +649,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.11" @@ -392,6 +683,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -554,3 +851,23 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] diff --git a/Cargo.toml b/Cargo.toml index d777220..777dcb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,11 @@ categories = ["command-line-utilities", "filesystem"] [dependencies] clap = { version = "4.4.18", features = ["derive"] } colored = "2.1.0" +crossterm = "0.27.0" lazy_static = "1.4.0" pest = "2.7.6" pest_derive = "2.7.6" +ratatui = "0.25.0" textwrap = "0.16.0" walkdir = "2.4.0" diff --git a/src/main.rs b/src/main.rs index 0a2128b..3038b3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::{path::Path, process::Command}; use colored::Colorize; use pest::Parser; +use tag::search::TaggedFile; use tag::{ parsers::searchquery::{construct_query_ast, evaluate_ast, QueryParser, Rule}, search::get_tags_from_files, @@ -13,6 +14,7 @@ mod cli { #[derive(Parser)] #[command(author, version, about, long_about = None)] + #[allow(clippy::struct_excessive_bools)] pub struct Cli { #[clap(value_name = "PATH")] /// The path that will be searched. @@ -22,7 +24,7 @@ mod cli { /// Search query for the tags. pub query: Option, - #[arg(short, long)] + #[arg(short, long, group = "output")] /// Only print the paths of matched files. pub silent: bool, @@ -41,6 +43,9 @@ mod cli { #[arg(short, long, group = "q-input")] /// Receive a query from the standard input. pub query_stdin: bool, + + #[arg(short, long, group = "output")] + pub interactive: bool, } impl Cli { @@ -104,6 +109,17 @@ fn execute_filter_command_on_file(path: &Path, command: &str) -> bool { output.unwrap().status.success() } +fn non_interactive_output(file: &TaggedFile, command_output: &str) { + println!("\t{}", format!("tags: {:?}", file.tags).blue()); + + if !command_output.is_empty() { + println!( + "\tOutput of command:\n{}", + textwrap::indent(command_output, "\t\t") + ); + } +} + fn main() -> Result<(), Box> { let mut args = cli::Cli::new_and_parse(); @@ -176,15 +192,15 @@ fn main() -> Result<(), Box> { String::new() }; - if !args.silent { - println!("\t{}", format!("tags: {:?}", file.tags).blue()); + // don't print any more information in silent mode + if args.silent { + continue; + } - if !output.is_empty() { - println!( - "\tOutput of command:\n{}", - textwrap::indent(output.as_str(), "\t\t") - ); - } + if args.interactive { + todo!() + } else { + non_interactive_output(&file, output.as_str()); } } From 5cb69be3da8e8da672f87efb0665c350f8b9a8b7 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 15:39:04 +0100 Subject: [PATCH 02/19] prepared interactive mode --- src/main.rs | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3038b3e..ef56a3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,14 @@ -use std::io::{BufRead, IsTerminal}; +use std::io::{self, stdout, BufRead, IsTerminal}; use std::{path::Path, process::Command}; use colored::Colorize; +use crossterm::terminal::{ + self, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use crossterm::ExecutableCommand; use pest::Parser; +use ratatui::backend::CrosstermBackend; +use ratatui::{Frame, Terminal}; use tag::search::TaggedFile; use tag::{ parsers::searchquery::{construct_query_ast, evaluate_ast, QueryParser, Rule}, @@ -120,11 +126,31 @@ fn non_interactive_output(file: &TaggedFile, command_output: &str) { } } +fn interactive_output(file: &TaggedFile, command_output: &str) -> io::Result<()> { + let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + + let mut show_next = false; + while !show_next { + terminal.draw(|frame| interactive_output_ui(file, command_output, frame))?; + show_next = handle_events()?; + } + + Ok(()) +} + +fn interactive_output_ui(file: &TaggedFile, command_output: &str, frame: &mut Frame) { + todo!() +} + +fn handle_events() -> io::Result { + todo!() +} + fn main() -> Result<(), Box> { let mut args = cli::Cli::new_and_parse(); // detect if output is in a terminal or not - if !std::io::stdout().is_terminal() { + if !stdout().is_terminal() { args.silent = true; args.no_color = true; } @@ -166,6 +192,11 @@ fn main() -> Result<(), Box> { let query = query.unwrap(); + if args.interactive { + enable_raw_mode()?; + stdout().execute(EnterAlternateScreen)?; + } + for file in file_index { let ast = construct_query_ast( query.clone().next().unwrap().into_inner(), @@ -184,7 +215,9 @@ fn main() -> Result<(), Box> { continue; } - println!("{}", file.path.display().to_string().green()); + if !args.interactive { + println!("{}", file.path.display().to_string().green()); + } let output = if args.command.is_some() { execute_command_on_file(&file.path, &args.command.clone().unwrap()) @@ -204,5 +237,10 @@ fn main() -> Result<(), Box> { } } + if args.interactive { + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + } + Ok(()) } From 7724ba48ff302b59f4262d43c2b7be97165c5e1c Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 15:58:14 +0100 Subject: [PATCH 03/19] wrote handle events --- src/main.rs | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index ef56a3d..1a99cc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ +use std::default; use std::io::{self, stdout, BufRead, IsTerminal}; use std::{path::Path, process::Command}; use colored::Colorize; +use crossterm::event::{Event, KeyCode}; use crossterm::terminal::{ self, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; -use crossterm::ExecutableCommand; +use crossterm::{event, ExecutableCommand}; use pest::Parser; use ratatui::backend::CrosstermBackend; use ratatui::{Frame, Terminal}; @@ -61,6 +63,14 @@ mod cli { } } +/// `InteractiveInputs` contains possible inputs for interactive mode. +#[derive(Default)] +struct InteractiveInputs { + pub next_file: bool, + pub next_tab: bool, + pub previous_tab: bool, +} + fn execute_command_on_file(path: &Path, command: &str) -> String { let command = command.replace("#FILE#", path.to_str().unwrap()); @@ -129,21 +139,45 @@ fn non_interactive_output(file: &TaggedFile, command_output: &str) { fn interactive_output(file: &TaggedFile, command_output: &str) -> io::Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; - let mut show_next = false; - while !show_next { - terminal.draw(|frame| interactive_output_ui(file, command_output, frame))?; - show_next = handle_events()?; + let mut interactive_inputs = InteractiveInputs::default(); + while !interactive_inputs.next_file { + terminal.draw(|frame| { + interactive_output_ui(file, command_output, &interactive_inputs, frame); + })?; + interactive_inputs = handle_events()?; } Ok(()) } -fn interactive_output_ui(file: &TaggedFile, command_output: &str, frame: &mut Frame) { +fn interactive_output_ui( + file: &TaggedFile, + command_output: &str, + interactive_inputs: &InteractiveInputs, + frame: &mut Frame, +) { todo!() } -fn handle_events() -> io::Result { - todo!() +fn handle_events() -> io::Result { + let mut interactive_inputs = InteractiveInputs::default(); + + if event::poll(std::time::Duration::from_millis(50))? { + if let Event::Key(key) = event::read()? { + if key.kind != event::KeyEventKind::Press { + return Ok(interactive_inputs); + } + + match key.code { + KeyCode::Char('n') => interactive_inputs.next_file = true, + KeyCode::Char('l') | KeyCode::Right => interactive_inputs.next_tab = true, + KeyCode::Char('h') | KeyCode::Left => interactive_inputs.previous_tab = true, + _ => return Ok(interactive_inputs), + } + } + } + + Ok(interactive_inputs) } fn main() -> Result<(), Box> { From 8717582c294c9ebfbcecec3e5cb499f55953aa9d Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 16:14:55 +0100 Subject: [PATCH 04/19] render tabs --- src/main.rs | 120 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 48 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1a99cc7..f6b3fff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,13 @@ -use std::default; -use std::io::{self, stdout, BufRead, IsTerminal}; +use std::io::{stdout, BufRead, IsTerminal}; use std::{path::Path, process::Command}; use colored::Colorize; -use crossterm::event::{Event, KeyCode}; use crossterm::terminal::{ - self, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; -use crossterm::{event, ExecutableCommand}; +use crossterm::ExecutableCommand; use pest::Parser; -use ratatui::backend::CrosstermBackend; -use ratatui::{Frame, Terminal}; + use tag::search::TaggedFile; use tag::{ parsers::searchquery::{construct_query_ast, evaluate_ast, QueryParser, Rule}, @@ -63,14 +60,6 @@ mod cli { } } -/// `InteractiveInputs` contains possible inputs for interactive mode. -#[derive(Default)] -struct InteractiveInputs { - pub next_file: bool, - pub next_tab: bool, - pub previous_tab: bool, -} - fn execute_command_on_file(path: &Path, command: &str) -> String { let command = command.replace("#FILE#", path.to_str().unwrap()); @@ -136,48 +125,83 @@ fn non_interactive_output(file: &TaggedFile, command_output: &str) { } } -fn interactive_output(file: &TaggedFile, command_output: &str) -> io::Result<()> { - let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; +mod interactive_output { + use crossterm::event::{Event, KeyCode}; + + use crossterm::event; + use ratatui::backend::CrosstermBackend; + use ratatui::style::{Style, Stylize}; + use ratatui::widgets::{Block, Borders, Tabs}; + use ratatui::{symbols, Frame, Terminal}; + use std::io::{self, stdout}; - let mut interactive_inputs = InteractiveInputs::default(); - while !interactive_inputs.next_file { - terminal.draw(|frame| { - interactive_output_ui(file, command_output, &interactive_inputs, frame); - })?; - interactive_inputs = handle_events()?; + use tag::search::TaggedFile; + + /// `InteractiveInputs` contains possible inputs for interactive mode. + #[derive(Default)] + struct InteractiveInputs { + pub next_file: bool, + pub tab_index: usize, } - Ok(()) -} + pub fn interactive_output(file: &TaggedFile, command_output: &str) -> io::Result<()> { + let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; -fn interactive_output_ui( - file: &TaggedFile, - command_output: &str, - interactive_inputs: &InteractiveInputs, - frame: &mut Frame, -) { - todo!() -} + let mut interactive_inputs = InteractiveInputs::default(); + while !interactive_inputs.next_file { + terminal.draw(|frame| { + interactive_output_ui(file, command_output, &interactive_inputs, frame); + })?; + interactive_inputs = handle_events()?; -fn handle_events() -> io::Result { - let mut interactive_inputs = InteractiveInputs::default(); + // prevent an overflow of the tab index + // and also handle wrapping + interactive_inputs.tab_index %= 3; + } - if event::poll(std::time::Duration::from_millis(50))? { - if let Event::Key(key) = event::read()? { - if key.kind != event::KeyEventKind::Press { - return Ok(interactive_inputs); - } + Ok(()) + } - match key.code { - KeyCode::Char('n') => interactive_inputs.next_file = true, - KeyCode::Char('l') | KeyCode::Right => interactive_inputs.next_tab = true, - KeyCode::Char('h') | KeyCode::Left => interactive_inputs.previous_tab = true, - _ => return Ok(interactive_inputs), + fn interactive_output_ui( + file: &TaggedFile, + command_output: &str, + interactive_inputs: &InteractiveInputs, + frame: &mut Frame, + ) { + let tabs = Tabs::new(vec!["File Content", "Command Output", "Tags"]) + .block( + Block::default() + .title(file.path.to_str().unwrap()) + .borders(Borders::all()), + ) + .style(Style::default().white()) + .highlight_style(Style::default().blue()) + .select(interactive_inputs.tab_index) + .divider(symbols::DOT); + + frame.render_widget(tabs, frame.size()); + } + + fn handle_events() -> io::Result { + let mut interactive_inputs = InteractiveInputs::default(); + + if event::poll(std::time::Duration::from_millis(50))? { + if let Event::Key(key) = event::read()? { + if key.kind != event::KeyEventKind::Press { + return Ok(interactive_inputs); + } + + match key.code { + KeyCode::Char('n') => interactive_inputs.next_file = true, + KeyCode::Char('l') | KeyCode::Right => interactive_inputs.tab_index += 1, + KeyCode::Char('h') | KeyCode::Left => interactive_inputs.tab_index -= 1, + _ => return Ok(interactive_inputs), + } } } - } - Ok(interactive_inputs) + Ok(interactive_inputs) + } } fn main() -> Result<(), Box> { @@ -265,7 +289,7 @@ fn main() -> Result<(), Box> { } if args.interactive { - todo!() + interactive_output::interactive_output(&file, &output)?; } else { non_interactive_output(&file, output.as_str()); } From b8de5fea57f7eaf21f34ba0e124f0f7f3d400db6 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 16:16:42 +0100 Subject: [PATCH 05/19] correct handling of tab index --- src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index f6b3fff..fe4220a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,7 +152,7 @@ mod interactive_output { terminal.draw(|frame| { interactive_output_ui(file, command_output, &interactive_inputs, frame); })?; - interactive_inputs = handle_events()?; + interactive_inputs = handle_events(&interactive_inputs)?; // prevent an overflow of the tab index // and also handle wrapping @@ -182,8 +182,11 @@ mod interactive_output { frame.render_widget(tabs, frame.size()); } - fn handle_events() -> io::Result { - let mut interactive_inputs = InteractiveInputs::default(); + fn handle_events(previous_inputs: &InteractiveInputs) -> io::Result { + let mut interactive_inputs = InteractiveInputs { + tab_index: previous_inputs.tab_index, + ..Default::default() + }; if event::poll(std::time::Duration::from_millis(50))? { if let Event::Key(key) = event::read()? { From fcf560a67a45b518225e89f8ac6d8246539ed767 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 16:56:29 +0100 Subject: [PATCH 06/19] basic display --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 164f5b1..90d7ab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,6 +586,7 @@ dependencies = [ "clap", "colored", "crossterm", + "itertools", "lazy_static", "pest", "pest_derive", diff --git a/Cargo.toml b/Cargo.toml index 777dcb0..ea41bf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ categories = ["command-line-utilities", "filesystem"] clap = { version = "4.4.18", features = ["derive"] } colored = "2.1.0" crossterm = "0.27.0" +itertools = "0.12.0" lazy_static = "1.4.0" pest = "2.7.6" pest_derive = "2.7.6" diff --git a/src/main.rs b/src/main.rs index fe4220a..1fdadb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -126,14 +126,16 @@ fn non_interactive_output(file: &TaggedFile, command_output: &str) { } mod interactive_output { - use crossterm::event::{Event, KeyCode}; - use crossterm::event; + use crossterm::event::{Event, KeyCode}; + use itertools::Itertools; use ratatui::backend::CrosstermBackend; + use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; - use ratatui::widgets::{Block, Borders, Tabs}; - use ratatui::{symbols, Frame, Terminal}; + use ratatui::widgets::{Block, Borders, Paragraph, Tabs, Widget, Wrap}; + use ratatui::{layout, symbols, Frame, Terminal}; use std::io::{self, stdout}; + use std::rc::Rc; use tag::search::TaggedFile; @@ -168,18 +170,76 @@ mod interactive_output { interactive_inputs: &InteractiveInputs, frame: &mut Frame, ) { + let area = layout(frame.size(), Direction::Vertical, &[1, 0]); let tabs = Tabs::new(vec!["File Content", "Command Output", "Tags"]) - .block( - Block::default() - .title(file.path.to_str().unwrap()) - .borders(Borders::all()), - ) .style(Style::default().white()) .highlight_style(Style::default().blue()) .select(interactive_inputs.tab_index) .divider(symbols::DOT); - frame.render_widget(tabs, frame.size()); + frame.render_widget(tabs, area[0]); + + render_tab_content( + file, + command_output, + interactive_inputs.tab_index, + area[1], + frame, + ); + } + + fn render_tab_content( + file: &TaggedFile, + command_output: &str, + tab_index: usize, + area: Rect, + frame: &mut Frame, + ) { + let content = match tab_index { + 0 => std::fs::read_to_string(&file.path).unwrap(), + 1 => command_output.to_string(), + 2 => { + let mut out_string = String::new(); + for tag in &file.tags { + out_string += tag.as_str(); + out_string.push('\n'); + } + out_string + } + _ => unreachable!(), + }; + + let paragraph = Paragraph::new(content) + .block( + Block::new() + .title(file.path.to_str().unwrap()) + .borders(Borders::all()), + ) + .style(Style::new()) + .alignment(Alignment::Left) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, area); + } + + /// simple helper method to split an area into multiple sub-areas + /// copied and slightly modified from + /// [here](https://docs.rs/ratatui/latest/src/demo2/root.rs.html#34) + pub fn layout(area: Rect, direction: Direction, heights: &[u16]) -> Rc<[Rect]> { + let constraints = heights + .iter() + .map(|&h| { + if h > 0 { + Constraint::Length(h) + } else { + Constraint::Min(0) + } + }) + .collect_vec(); + Layout::default() + .direction(direction) + .constraints(constraints) + .split(area) } fn handle_events(previous_inputs: &InteractiveInputs) -> io::Result { @@ -196,8 +256,12 @@ mod interactive_output { match key.code { KeyCode::Char('n') => interactive_inputs.next_file = true, - KeyCode::Char('l') | KeyCode::Right => interactive_inputs.tab_index += 1, - KeyCode::Char('h') | KeyCode::Left => interactive_inputs.tab_index -= 1, + KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { + interactive_inputs.tab_index += 1; + } + KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => { + interactive_inputs.tab_index -= 1; + } _ => return Ok(interactive_inputs), } } From 88f9319c3870474ca8d2675500bb34ae0eee7613 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 17:21:32 +0100 Subject: [PATCH 07/19] works (no command output yet) --- src/main.rs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1fdadb0..1444f88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,8 +132,8 @@ mod interactive_output { use ratatui::backend::CrosstermBackend; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; - use ratatui::widgets::{Block, Borders, Paragraph, Tabs, Widget, Wrap}; - use ratatui::{layout, symbols, Frame, Terminal}; + use ratatui::widgets::{Block, Borders, Paragraph, Tabs, Wrap}; + use ratatui::{symbols, Frame, Terminal}; use std::io::{self, stdout}; use std::rc::Rc; @@ -142,23 +142,28 @@ mod interactive_output { /// `InteractiveInputs` contains possible inputs for interactive mode. #[derive(Default)] struct InteractiveInputs { - pub next_file: bool, pub tab_index: usize, + pub file_index: usize, + pub quit: bool, } - pub fn interactive_output(file: &TaggedFile, command_output: &str) -> io::Result<()> { + pub fn interactive_output(files: &[TaggedFile]) -> io::Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; let mut interactive_inputs = InteractiveInputs::default(); - while !interactive_inputs.next_file { + while !interactive_inputs.quit { + let file = &files[interactive_inputs.file_index]; + let command_output = ""; + terminal.draw(|frame| { interactive_output_ui(file, command_output, &interactive_inputs, frame); })?; interactive_inputs = handle_events(&interactive_inputs)?; - // prevent an overflow of the tab index + // prevent an overflow of the index // and also handle wrapping interactive_inputs.tab_index %= 3; + interactive_inputs.file_index %= files.len(); } Ok(()) @@ -206,7 +211,7 @@ mod interactive_output { } out_string } - _ => unreachable!(), + _ => unreachable!(), // tabs are constrained to be between 0 and 2 }; let paragraph = Paragraph::new(content) @@ -245,6 +250,7 @@ mod interactive_output { fn handle_events(previous_inputs: &InteractiveInputs) -> io::Result { let mut interactive_inputs = InteractiveInputs { tab_index: previous_inputs.tab_index, + file_index: previous_inputs.file_index, ..Default::default() }; @@ -255,13 +261,15 @@ mod interactive_output { } match key.code { - KeyCode::Char('n') => interactive_inputs.next_file = true, + KeyCode::Char('n') => interactive_inputs.file_index += 1, + KeyCode::Char('p') => interactive_inputs.file_index -= 1, KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { interactive_inputs.tab_index += 1; } KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => { interactive_inputs.tab_index -= 1; } + KeyCode::Char('q') => interactive_inputs.quit = true, _ => return Ok(interactive_inputs), } } @@ -322,6 +330,8 @@ fn main() -> Result<(), Box> { stdout().execute(EnterAlternateScreen)?; } + let mut file_matched_index = Vec::new(); + for file in file_index { let ast = construct_query_ast( query.clone().next().unwrap().into_inner(), @@ -355,14 +365,16 @@ fn main() -> Result<(), Box> { continue; } - if args.interactive { - interactive_output::interactive_output(&file, &output)?; - } else { + if !args.interactive { non_interactive_output(&file, output.as_str()); } + + file_matched_index.push(file); } if args.interactive { + interactive_output::interactive_output(&file_matched_index)?; + disable_raw_mode()?; stdout().execute(LeaveAlternateScreen)?; } From 3d27aa261d7a138851e09ee2197cc1a46484ae25 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 17:25:02 +0100 Subject: [PATCH 08/19] command output works --- src/main.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1444f88..d170565 100644 --- a/src/main.rs +++ b/src/main.rs @@ -147,16 +147,16 @@ mod interactive_output { pub quit: bool, } - pub fn interactive_output(files: &[TaggedFile]) -> io::Result<()> { + pub fn interactive_output(files: &[TaggedFile], command_outputs: &[String]) -> io::Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; let mut interactive_inputs = InteractiveInputs::default(); while !interactive_inputs.quit { let file = &files[interactive_inputs.file_index]; - let command_output = ""; + let command_output = command_outputs[interactive_inputs.file_index].clone(); terminal.draw(|frame| { - interactive_output_ui(file, command_output, &interactive_inputs, frame); + interactive_output_ui(file, command_output.as_str(), &interactive_inputs, frame); })?; interactive_inputs = handle_events(&interactive_inputs)?; @@ -331,6 +331,7 @@ fn main() -> Result<(), Box> { } let mut file_matched_index = Vec::new(); + let mut command_outputs = Vec::new(); for file in file_index { let ast = construct_query_ast( @@ -370,10 +371,11 @@ fn main() -> Result<(), Box> { } file_matched_index.push(file); + command_outputs.push(output); } if args.interactive { - interactive_output::interactive_output(&file_matched_index)?; + interactive_output::interactive_output(&file_matched_index, &command_outputs)?; disable_raw_mode()?; stdout().execute(LeaveAlternateScreen)?; From 632aa43500e7c21029b223270dde350ee24ad76f Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 18:06:08 +0100 Subject: [PATCH 09/19] implemented scrolling --- src/main.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index d170565..29b279a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,6 +132,7 @@ mod interactive_output { use ratatui::backend::CrosstermBackend; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; + use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Tabs, Wrap}; use ratatui::{symbols, Frame, Terminal}; use std::io::{self, stdout}; @@ -144,6 +145,7 @@ mod interactive_output { struct InteractiveInputs { pub tab_index: usize, pub file_index: usize, + pub scroll_index: u16, pub quit: bool, } @@ -175,28 +177,37 @@ mod interactive_output { interactive_inputs: &InteractiveInputs, frame: &mut Frame, ) { - let area = layout(frame.size(), Direction::Vertical, &[1, 0]); - let tabs = Tabs::new(vec!["File Content", "Command Output", "Tags"]) - .style(Style::default().white()) - .highlight_style(Style::default().blue()) - .select(interactive_inputs.tab_index) - .divider(symbols::DOT); + let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); - frame.render_widget(tabs, area[0]); + render_tabs(area[0], frame, interactive_inputs); render_tab_content( file, command_output, interactive_inputs.tab_index, + interactive_inputs.scroll_index, area[1], frame, ); + + render_help_menu(area[2], frame); + } + + fn render_tabs(area: Rect, frame: &mut Frame, interactive_inputs: &InteractiveInputs) { + let tabs = Tabs::new(vec!["File Content", "Command Output", "Tags"]) + .style(Style::default().white()) + .highlight_style(Style::default().blue()) + .select(interactive_inputs.tab_index) + .divider(symbols::DOT); + + frame.render_widget(tabs, area); } fn render_tab_content( file: &TaggedFile, command_output: &str, tab_index: usize, + scroll_index: u16, area: Rect, frame: &mut Frame, ) { @@ -214,6 +225,9 @@ mod interactive_output { _ => unreachable!(), // tabs are constrained to be between 0 and 2 }; + #[allow(clippy::cast_possible_truncation)] + let scroll_index = scroll_index % content.lines().collect_vec().len() as u16; + let paragraph = Paragraph::new(content) .block( Block::new() @@ -222,7 +236,33 @@ mod interactive_output { ) .style(Style::new()) .alignment(Alignment::Left) - .wrap(Wrap { trim: false }); + .wrap(Wrap { trim: false }) + .scroll((scroll_index, 0)); + + frame.render_widget(paragraph, area); + } + + fn render_help_menu(area: Rect, frame: &mut Frame) { + let keys = [ + ("q", "Quit"), + ("Up-Arrow/k", "Scroll Up"), + ("Down-Arrow/j", "Scroll Down"), + ("n", "Next File"), + ("p", "Previous File"), + ("Tab/Right-Arrow/l", "Next Tab"), + ("Shift+Tab/Left-Arrow/h", "Previous Tab"), + ]; + + let spans = keys + .iter() + .flat_map(|(key, desc)| { + let key = Span::styled(format!("| {key} "), Style::new().green()); + let desc = Span::styled(format!(" {desc} | "), Style::new().green()); + [key, desc] + }) + .collect_vec(); + + let paragraph = Paragraph::new(Line::from(spans)).alignment(Alignment::Center); frame.render_widget(paragraph, area); } @@ -251,6 +291,7 @@ mod interactive_output { let mut interactive_inputs = InteractiveInputs { tab_index: previous_inputs.tab_index, file_index: previous_inputs.file_index, + scroll_index: previous_inputs.scroll_index, ..Default::default() }; @@ -262,13 +303,33 @@ mod interactive_output { match key.code { KeyCode::Char('n') => interactive_inputs.file_index += 1, - KeyCode::Char('p') => interactive_inputs.file_index -= 1, + KeyCode::Char('p') => { + if interactive_inputs.file_index == 0 { + interactive_inputs.file_index = usize::MAX; + } else { + interactive_inputs.file_index -= 1; + } + } KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { interactive_inputs.tab_index += 1; } KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => { interactive_inputs.tab_index -= 1; } + KeyCode::Char('k') | KeyCode::Up => { + if interactive_inputs.scroll_index == 0 { + interactive_inputs.scroll_index = u16::MAX; + } else { + interactive_inputs.scroll_index -= 1; + } + } + KeyCode::Char('j') | KeyCode::Down => { + if interactive_inputs.scroll_index == u16::MAX { + interactive_inputs.scroll_index = u16::MIN; + } else { + interactive_inputs.scroll_index += 1; + } + } KeyCode::Char('q') => interactive_inputs.quit = true, _ => return Ok(interactive_inputs), } From 879002a71bdb87c453839b92fde8061308ea85e3 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 19:31:48 +0100 Subject: [PATCH 10/19] commands now actually execute --- Cargo.lock | 12 ++++++++ Cargo.toml | 1 + src/main.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90d7ab7..e7a26e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -592,6 +592,7 @@ dependencies = [ "pest_derive", "ratatui", "textwrap", + "tui-textarea", "walkdir", ] @@ -626,6 +627,17 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tui-textarea" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index ea41bf8..38156ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ pest = "2.7.6" pest_derive = "2.7.6" ratatui = "0.25.0" textwrap = "0.16.0" +tui-textarea = "0.4.0" walkdir = "2.4.0" [lints.clippy] diff --git a/src/main.rs b/src/main.rs index 29b279a..ffbbabb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,28 +137,44 @@ mod interactive_output { use ratatui::{symbols, Frame, Terminal}; use std::io::{self, stdout}; use std::rc::Rc; + use tui_textarea::{Input, Key, TextArea}; use tag::search::TaggedFile; + use crate::execute_command_on_file; + /// `InteractiveInputs` contains possible inputs for interactive mode. #[derive(Default)] struct InteractiveInputs { pub tab_index: usize, pub file_index: usize, pub scroll_index: u16, + pub command_mode: bool, pub quit: bool, } pub fn interactive_output(files: &[TaggedFile], command_outputs: &[String]) -> io::Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + // the command textarea + let mut textarea = TextArea::default(); + textarea.set_cursor_line_style(Style::default()); + textarea.set_placeholder_text("Enter a command"); + textarea.set_block(Block::new().title("command").borders(Borders::all())); + let mut interactive_inputs = InteractiveInputs::default(); while !interactive_inputs.quit { let file = &files[interactive_inputs.file_index]; let command_output = command_outputs[interactive_inputs.file_index].clone(); terminal.draw(|frame| { - interactive_output_ui(file, command_output.as_str(), &interactive_inputs, frame); + interactive_output_ui( + file, + command_output.as_str(), + &mut interactive_inputs, + &mut textarea, + frame, + ); })?; interactive_inputs = handle_events(&interactive_inputs)?; @@ -174,23 +190,28 @@ mod interactive_output { fn interactive_output_ui( file: &TaggedFile, command_output: &str, - interactive_inputs: &InteractiveInputs, + interactive_inputs: &mut InteractiveInputs, + text_area: &mut TextArea, frame: &mut Frame, ) { - let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); + if interactive_inputs.command_mode { + interactive_inputs.command_mode = command_mode(file, text_area, frame).unwrap(); + } else { + let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); - render_tabs(area[0], frame, interactive_inputs); + render_tabs(area[0], frame, interactive_inputs); - render_tab_content( - file, - command_output, - interactive_inputs.tab_index, - interactive_inputs.scroll_index, - area[1], - frame, - ); + render_tab_content( + file, + command_output, + interactive_inputs.tab_index, + interactive_inputs.scroll_index, + area[1], + frame, + ); - render_help_menu(area[2], frame); + render_help_menu(area[2], frame); + } } fn render_tabs(area: Rect, frame: &mut Frame, interactive_inputs: &InteractiveInputs) { @@ -203,6 +224,39 @@ mod interactive_output { frame.render_widget(tabs, area); } + fn command_mode( + file: &TaggedFile, + text_area: &mut TextArea, + frame: &mut Frame, + ) -> Result> { + let layout = + Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice()); + + match crossterm::event::read()?.into() { + Input { key: Key::Esc, .. } => { + return Ok(true); + } + Input { + key: Key::Enter, .. + } => { + execute_command_on_file(&file.path, &text_area.lines()[0]); + return Ok(false); + } + Input { + key: Key::Char('m'), + ctrl: true, + .. + } => {} + input => { + text_area.input(input); + } + } + + frame.render_widget(text_area.widget(), layout.split(frame.size())[0]); + + Ok(true) + } + fn render_tab_content( file: &TaggedFile, command_output: &str, @@ -251,6 +305,7 @@ mod interactive_output { ("p", "Previous File"), ("Tab/Right-Arrow/l", "Next Tab"), ("Shift+Tab/Left-Arrow/h", "Previous Tab"), + ("c", "Execute a command"), ]; let spans = keys @@ -292,6 +347,7 @@ mod interactive_output { tab_index: previous_inputs.tab_index, file_index: previous_inputs.file_index, scroll_index: previous_inputs.scroll_index, + command_mode: previous_inputs.command_mode, ..Default::default() }; @@ -330,6 +386,7 @@ mod interactive_output { interactive_inputs.scroll_index += 1; } } + KeyCode::Char('c') => interactive_inputs.command_mode = true, KeyCode::Char('q') => interactive_inputs.quit = true, _ => return Ok(interactive_inputs), } From f8f97f87f838abef8d870c453496babbf9cc4107 Mon Sep 17 00:00:00 2001 From: miampf Date: Tue, 23 Jan 2024 19:43:14 +0100 Subject: [PATCH 11/19] WIP: need to fix rendering --- src/main.rs | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index ffbbabb..dc10983 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,7 +133,7 @@ mod interactive_output { use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Span}; - use ratatui::widgets::{Block, Borders, Paragraph, Tabs, Wrap}; + use ratatui::widgets::{Block, Borders, Clear, Paragraph, Tabs, Wrap}; use ratatui::{symbols, Frame, Terminal}; use std::io::{self, stdout}; use std::rc::Rc; @@ -196,22 +196,21 @@ mod interactive_output { ) { if interactive_inputs.command_mode { interactive_inputs.command_mode = command_mode(file, text_area, frame).unwrap(); - } else { - let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); + } + let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); - render_tabs(area[0], frame, interactive_inputs); + render_tabs(area[0], frame, interactive_inputs); - render_tab_content( - file, - command_output, - interactive_inputs.tab_index, - interactive_inputs.scroll_index, - area[1], - frame, - ); + render_tab_content( + file, + command_output, + interactive_inputs.tab_index, + interactive_inputs.scroll_index, + area[1], + frame, + ); - render_help_menu(area[2], frame); - } + render_help_menu(area[2], frame); } fn render_tabs(area: Rect, frame: &mut Frame, interactive_inputs: &InteractiveInputs) { @@ -224,6 +223,7 @@ mod interactive_output { frame.render_widget(tabs, area); } + // TODO: Fix rendering. fn command_mode( file: &TaggedFile, text_area: &mut TextArea, @@ -252,7 +252,16 @@ mod interactive_output { } } - frame.render_widget(text_area.widget(), layout.split(frame.size())[0]); + let area = Rect::new( + 0, + frame.size().height / 2, + frame.size().width, + frame.size().height, + ); + + frame.render_widget(Clear, layout.split(area)[0]); + + frame.render_widget(text_area.widget(), layout.split(area)[0]); Ok(true) } From 18b12cc25e85978827d077b7bb04928cbd5b427c Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 15:37:31 +0100 Subject: [PATCH 12/19] fixed rendering of command input --- src/main.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index dc10983..767ebe8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -194,9 +194,6 @@ mod interactive_output { text_area: &mut TextArea, frame: &mut Frame, ) { - if interactive_inputs.command_mode { - interactive_inputs.command_mode = command_mode(file, text_area, frame).unwrap(); - } let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); render_tabs(area[0], frame, interactive_inputs); @@ -211,6 +208,10 @@ mod interactive_output { ); render_help_menu(area[2], frame); + + if interactive_inputs.command_mode { + interactive_inputs.command_mode = command_mode(file, text_area, frame).unwrap(); + } } fn render_tabs(area: Rect, frame: &mut Frame, interactive_inputs: &InteractiveInputs) { @@ -234,7 +235,7 @@ mod interactive_output { match crossterm::event::read()?.into() { Input { key: Key::Esc, .. } => { - return Ok(true); + return Ok(false); } Input { key: Key::Enter, .. @@ -252,15 +253,9 @@ mod interactive_output { } } - let area = Rect::new( - 0, - frame.size().height / 2, - frame.size().width, - frame.size().height, - ); + let area = Rect::new(0, frame.size().height / 2, frame.size().width, 10); frame.render_widget(Clear, layout.split(area)[0]); - frame.render_widget(text_area.widget(), layout.split(area)[0]); Ok(true) From b91eae17383cf292bd9f26c509827b3659e7cb84 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 15:39:28 +0100 Subject: [PATCH 13/19] removed todo comment --- src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 767ebe8..ea57959 100644 --- a/src/main.rs +++ b/src/main.rs @@ -224,7 +224,6 @@ mod interactive_output { frame.render_widget(tabs, area); } - // TODO: Fix rendering. fn command_mode( file: &TaggedFile, text_area: &mut TextArea, From 5a063501bbbc3c3930b96e18fdca35afa1190593 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 15:44:43 +0100 Subject: [PATCH 14/19] added better styling for command input --- src/main.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index ea57959..c2af7e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,7 +160,13 @@ mod interactive_output { let mut textarea = TextArea::default(); textarea.set_cursor_line_style(Style::default()); textarea.set_placeholder_text("Enter a command"); - textarea.set_block(Block::new().title("command").borders(Borders::all())); + textarea.set_block( + Block::new() + .title("command") + .borders(Borders::all()) + .border_style(Style::default().red().on_black()) + .style(Style::default().black().on_white()), + ); let mut interactive_inputs = InteractiveInputs::default(); while !interactive_inputs.quit { From 088075eaada856ee58f90f657c422416ab92b959 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 15:51:07 +0100 Subject: [PATCH 15/19] split functions for command mode --- src/main.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index c2af7e8..75a73d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -216,7 +216,8 @@ mod interactive_output { render_help_menu(area[2], frame); if interactive_inputs.command_mode { - interactive_inputs.command_mode = command_mode(file, text_area, frame).unwrap(); + interactive_inputs.command_mode = command_mode_input(file, text_area).unwrap(); + command_mode_render(text_area, frame); } } @@ -230,14 +231,20 @@ mod interactive_output { frame.render_widget(tabs, area); } - fn command_mode( - file: &TaggedFile, - text_area: &mut TextArea, - frame: &mut Frame, - ) -> Result> { + fn command_mode_render(text_area: &mut TextArea, frame: &mut Frame) { let layout = Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice()); + let area = Rect::new(0, frame.size().height / 2, frame.size().width, 10); + + frame.render_widget(Clear, layout.split(area)[0]); + frame.render_widget(text_area.widget(), layout.split(area)[0]); + } + + fn command_mode_input( + file: &TaggedFile, + text_area: &mut TextArea, + ) -> Result { match crossterm::event::read()?.into() { Input { key: Key::Esc, .. } => { return Ok(false); @@ -258,11 +265,6 @@ mod interactive_output { } } - let area = Rect::new(0, frame.size().height / 2, frame.size().width, 10); - - frame.render_widget(Clear, layout.split(area)[0]); - frame.render_widget(text_area.widget(), layout.split(area)[0]); - Ok(true) } From d6be0c03e78f0a4655a578e8120068ecae38a001 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 15:52:21 +0100 Subject: [PATCH 16/19] renamed mode to inspect --- src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 75a73d9..27cdaa6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,7 +50,8 @@ mod cli { pub query_stdin: bool, #[arg(short, long, group = "output")] - pub interactive: bool, + /// Enter an interactive inspection mode to view each file individually. + pub inspect: bool, } impl Cli { @@ -454,7 +455,7 @@ fn main() -> Result<(), Box> { let query = query.unwrap(); - if args.interactive { + if args.inspect { enable_raw_mode()?; stdout().execute(EnterAlternateScreen)?; } @@ -480,7 +481,7 @@ fn main() -> Result<(), Box> { continue; } - if !args.interactive { + if !args.inspect { println!("{}", file.path.display().to_string().green()); } @@ -495,7 +496,7 @@ fn main() -> Result<(), Box> { continue; } - if !args.interactive { + if !args.inspect { non_interactive_output(&file, output.as_str()); } @@ -503,7 +504,7 @@ fn main() -> Result<(), Box> { command_outputs.push(output); } - if args.interactive { + if args.inspect { interactive_output::interactive_output(&file_matched_index, &command_outputs)?; disable_raw_mode()?; From 1b9085d674c4c49732e9583338e1a7147a19c583 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 16:05:03 +0100 Subject: [PATCH 17/19] reorganization --- src/commands.rs | 60 +++++++++ src/inspect.rs | 289 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 6 + src/main.rs | 342 +----------------------------------------------- 4 files changed, 358 insertions(+), 339 deletions(-) create mode 100644 src/commands.rs create mode 100644 src/inspect.rs diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..9ab8f9e --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,60 @@ +use std::{path::Path, process::Command}; + +use colored::Colorize; + +/// `execute_command_on_file` executes a command on a given #FILE#. +pub fn execute_command_on_file(path: &Path, command: &str) -> String { + let command = command.replace("#FILE#", path.to_str().unwrap()); + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").arg("/C").arg(command.clone()).output() + } else { + Command::new("bash").arg("-c").arg(command.clone()).output() + }; + + if let Err(e) = &output { + eprintln!( + "{} Wasn't able to execute command {}: {}", + "[ERROR]".red().bold(), + command.blue().underline(), + e.to_string().red() + ); + } + + let output = output.unwrap(); + let output_string = std::str::from_utf8(output.stdout.as_slice()); + + if let Err(e) = &output_string { + eprintln!( + "{} Failed to get output from command {}: {}", + "[ERROR]".red().bold(), + command.blue().underline(), + e.to_string().red() + ); + } + + output_string.unwrap().to_string() +} + +/// `execute_filter_command_on_file` executes a command on a given #FILE# and returns +/// true if the command ran successfully. +pub fn execute_filter_command_on_file(path: &Path, command: &str) -> bool { + let command = command.replace("#FILE#", path.to_str().unwrap()); + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").arg("/C").arg(command.clone()).output() + } else { + Command::new("bash").arg("-c").arg(command.clone()).output() + }; + + if let Err(e) = &output { + eprintln!( + "{} Wasn't able to execute command {}: {}", + "[ERROR]".red().bold(), + command.blue().underline(), + e.to_string().red() + ); + } + + output.unwrap().status.success() +} diff --git a/src/inspect.rs b/src/inspect.rs new file mode 100644 index 0000000..c81e4c1 --- /dev/null +++ b/src/inspect.rs @@ -0,0 +1,289 @@ +use crossterm::event; +use crossterm::event::{Event, KeyCode}; +use itertools::Itertools; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, Paragraph, Tabs, Wrap}; +use ratatui::{symbols, Frame, Terminal}; +use std::io::{self, stdout}; +use std::rc::Rc; +use tui_textarea::{Input, Key, TextArea}; + +use crate::search::TaggedFile; + +use crate::commands::execute_command_on_file; + +/// `InteractiveInputs` contains possible inputs for interactive mode. +#[derive(Default)] +struct InteractiveInputs { + pub tab_index: usize, + pub file_index: usize, + pub scroll_index: u16, + pub command_mode: bool, + pub quit: bool, +} + +/// `interactive_output` handles the interactive UI of the inspect mode. +/// +/// # Errors +/// +/// This function returns an error if rendering or handling inputs fails. +pub fn interactive_output(files: &[TaggedFile], command_outputs: &[String]) -> io::Result<()> { + let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + + // the command textarea + let mut textarea = TextArea::default(); + textarea.set_cursor_line_style(Style::default()); + textarea.set_placeholder_text("Enter a command"); + textarea.set_block( + Block::new() + .title("command") + .borders(Borders::all()) + .border_style(Style::default().red().on_black()) + .style(Style::default().black().on_white()), + ); + + let mut interactive_inputs = InteractiveInputs::default(); + while !interactive_inputs.quit { + let file = &files[interactive_inputs.file_index]; + let command_output = command_outputs[interactive_inputs.file_index].clone(); + + terminal.draw(|frame| { + interactive_output_ui( + file, + command_output.as_str(), + &mut interactive_inputs, + &mut textarea, + frame, + ); + })?; + interactive_inputs = handle_events(&interactive_inputs)?; + + // prevent an overflow of the index + // and also handle wrapping + interactive_inputs.tab_index %= 3; + interactive_inputs.file_index %= files.len(); + } + + Ok(()) +} + +/// `interactive_output_ui` renders the UI. +fn interactive_output_ui( + file: &TaggedFile, + command_output: &str, + interactive_inputs: &mut InteractiveInputs, + text_area: &mut TextArea, + frame: &mut Frame, +) { + let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); + + render_tabs(area[0], frame, interactive_inputs); + + render_tab_content( + file, + command_output, + interactive_inputs.tab_index, + interactive_inputs.scroll_index, + area[1], + frame, + ); + + render_help_menu(area[2], frame); + + if interactive_inputs.command_mode { + interactive_inputs.command_mode = command_mode_input(file, text_area).unwrap(); + command_mode_render(text_area, frame); + } +} + +/// `render_tabs` renders the tabs at the top of the screen. +fn render_tabs(area: Rect, frame: &mut Frame, interactive_inputs: &InteractiveInputs) { + let tabs = Tabs::new(vec!["File Content", "Command Output", "Tags"]) + .style(Style::default().white()) + .highlight_style(Style::default().blue()) + .select(interactive_inputs.tab_index) + .divider(symbols::DOT); + + frame.render_widget(tabs, area); +} + +/// `render_tab_content` renders the main content of the current tab. +fn render_tab_content( + file: &TaggedFile, + command_output: &str, + tab_index: usize, + scroll_index: u16, + area: Rect, + frame: &mut Frame, +) { + let content = match tab_index { + 0 => std::fs::read_to_string(&file.path).unwrap(), + 1 => command_output.to_string(), + 2 => { + let mut out_string = String::new(); + for tag in &file.tags { + out_string += tag.as_str(); + out_string.push('\n'); + } + out_string + } + _ => unreachable!(), // tabs are constrained to be between 0 and 2 + }; + + #[allow(clippy::cast_possible_truncation)] + let scroll_index = scroll_index % content.lines().collect_vec().len() as u16; + + let paragraph = Paragraph::new(content) + .block( + Block::new() + .title(file.path.to_str().unwrap()) + .borders(Borders::all()), + ) + .style(Style::new()) + .alignment(Alignment::Left) + .wrap(Wrap { trim: false }) + .scroll((scroll_index, 0)); + + frame.render_widget(paragraph, area); +} + +/// `render_help_menu` renders the help menu at the bottom of the screen. +fn render_help_menu(area: Rect, frame: &mut Frame) { + let keys = [ + ("q", "Quit"), + ("Up-Arrow/k", "Scroll Up"), + ("Down-Arrow/j", "Scroll Down"), + ("n", "Next File"), + ("p", "Previous File"), + ("Tab/Right-Arrow/l", "Next Tab"), + ("Shift+Tab/Left-Arrow/h", "Previous Tab"), + ("c", "Execute a command"), + ]; + + let spans = keys + .iter() + .flat_map(|(key, desc)| { + let key = Span::styled(format!("| {key} "), Style::new().green()); + let desc = Span::styled(format!(" {desc} | "), Style::new().green()); + [key, desc] + }) + .collect_vec(); + + let paragraph = Paragraph::new(Line::from(spans)).alignment(Alignment::Center); + + frame.render_widget(paragraph, area); +} + +/// simple helper method to split an area into multiple sub-areas +/// copied and slightly modified from +/// [here](https://docs.rs/ratatui/latest/src/demo2/root.rs.html#34) +fn layout(area: Rect, direction: Direction, heights: &[u16]) -> Rc<[Rect]> { + let constraints = heights + .iter() + .map(|&h| { + if h > 0 { + Constraint::Length(h) + } else { + Constraint::Min(0) + } + }) + .collect_vec(); + Layout::default() + .direction(direction) + .constraints(constraints) + .split(area) +} + +/// `handle_events` handles inputs in normal mode. +fn handle_events(previous_inputs: &InteractiveInputs) -> io::Result { + let mut interactive_inputs = InteractiveInputs { + tab_index: previous_inputs.tab_index, + file_index: previous_inputs.file_index, + scroll_index: previous_inputs.scroll_index, + command_mode: previous_inputs.command_mode, + ..Default::default() + }; + + if event::poll(std::time::Duration::from_millis(50))? { + if let Event::Key(key) = event::read()? { + if key.kind != event::KeyEventKind::Press { + return Ok(interactive_inputs); + } + + match key.code { + KeyCode::Char('n') => interactive_inputs.file_index += 1, + KeyCode::Char('p') => { + if interactive_inputs.file_index == 0 { + interactive_inputs.file_index = usize::MAX; + } else { + interactive_inputs.file_index -= 1; + } + } + KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { + interactive_inputs.tab_index += 1; + } + KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => { + interactive_inputs.tab_index -= 1; + } + KeyCode::Char('k') | KeyCode::Up => { + if interactive_inputs.scroll_index == 0 { + interactive_inputs.scroll_index = u16::MAX; + } else { + interactive_inputs.scroll_index -= 1; + } + } + KeyCode::Char('j') | KeyCode::Down => { + if interactive_inputs.scroll_index == u16::MAX { + interactive_inputs.scroll_index = u16::MIN; + } else { + interactive_inputs.scroll_index += 1; + } + } + KeyCode::Char('c') => interactive_inputs.command_mode = true, + KeyCode::Char('q') => interactive_inputs.quit = true, + _ => return Ok(interactive_inputs), + } + } + } + + Ok(interactive_inputs) +} + +/// `command_mode_render` renders the popup for a command in the middle of the screen. +fn command_mode_render(text_area: &mut TextArea, frame: &mut Frame) { + let layout = + Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice()); + + let area = Rect::new(0, frame.size().height / 2, frame.size().width, 10); + + frame.render_widget(Clear, layout.split(area)[0]); + frame.render_widget(text_area.widget(), layout.split(area)[0]); +} + +/// `command_mode_input` handles inputs in command mode. +fn command_mode_input(file: &TaggedFile, text_area: &mut TextArea) -> Result { + match crossterm::event::read()?.into() { + Input { key: Key::Esc, .. } => { + return Ok(false); + } + Input { + key: Key::Enter, .. + } => { + execute_command_on_file(&file.path, &text_area.lines()[0]); + return Ok(false); + } + Input { + key: Key::Char('m'), + ctrl: true, + .. + } => {} + input => { + text_area.input(input); + } + } + + Ok(true) +} diff --git a/src/lib.rs b/src/lib.rs index ed14c8a..f247f4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,3 +3,9 @@ pub mod parsers; /// search contains functions for searching files and tags. pub mod search; + +/// interactive inspection ui +pub mod inspect; + +/// system commands on files +pub mod commands; diff --git a/src/main.rs b/src/main.rs index 27cdaa6..c5fe8c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::io::{stdout, BufRead, IsTerminal}; -use std::{path::Path, process::Command}; use colored::Colorize; use crossterm::terminal::{ @@ -8,6 +7,8 @@ use crossterm::terminal::{ use crossterm::ExecutableCommand; use pest::Parser; +use tag::commands::{execute_command_on_file, execute_filter_command_on_file}; +use tag::inspect; use tag::search::TaggedFile; use tag::{ parsers::searchquery::{construct_query_ast, evaluate_ast, QueryParser, Rule}, @@ -61,60 +62,6 @@ mod cli { } } -fn execute_command_on_file(path: &Path, command: &str) -> String { - let command = command.replace("#FILE#", path.to_str().unwrap()); - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").arg("/C").arg(command.clone()).output() - } else { - Command::new("bash").arg("-c").arg(command.clone()).output() - }; - - if let Err(e) = &output { - eprintln!( - "{} Wasn't able to execute command {}: {}", - "[ERROR]".red().bold(), - command.blue().underline(), - e.to_string().red() - ); - } - - let output = output.unwrap(); - let output_string = std::str::from_utf8(output.stdout.as_slice()); - - if let Err(e) = &output_string { - eprintln!( - "{} Failed to get output from command {}: {}", - "[ERROR]".red().bold(), - command.blue().underline(), - e.to_string().red() - ); - } - - output_string.unwrap().to_string() -} - -fn execute_filter_command_on_file(path: &Path, command: &str) -> bool { - let command = command.replace("#FILE#", path.to_str().unwrap()); - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").arg("/C").arg(command.clone()).output() - } else { - Command::new("bash").arg("-c").arg(command.clone()).output() - }; - - if let Err(e) = &output { - eprintln!( - "{} Wasn't able to execute command {}: {}", - "[ERROR]".red().bold(), - command.blue().underline(), - e.to_string().red() - ); - } - - output.unwrap().status.success() -} - fn non_interactive_output(file: &TaggedFile, command_output: &str) { println!("\t{}", format!("tags: {:?}", file.tags).blue()); @@ -126,289 +73,6 @@ fn non_interactive_output(file: &TaggedFile, command_output: &str) { } } -mod interactive_output { - use crossterm::event; - use crossterm::event::{Event, KeyCode}; - use itertools::Itertools; - use ratatui::backend::CrosstermBackend; - use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; - use ratatui::style::{Style, Stylize}; - use ratatui::text::{Line, Span}; - use ratatui::widgets::{Block, Borders, Clear, Paragraph, Tabs, Wrap}; - use ratatui::{symbols, Frame, Terminal}; - use std::io::{self, stdout}; - use std::rc::Rc; - use tui_textarea::{Input, Key, TextArea}; - - use tag::search::TaggedFile; - - use crate::execute_command_on_file; - - /// `InteractiveInputs` contains possible inputs for interactive mode. - #[derive(Default)] - struct InteractiveInputs { - pub tab_index: usize, - pub file_index: usize, - pub scroll_index: u16, - pub command_mode: bool, - pub quit: bool, - } - - pub fn interactive_output(files: &[TaggedFile], command_outputs: &[String]) -> io::Result<()> { - let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; - - // the command textarea - let mut textarea = TextArea::default(); - textarea.set_cursor_line_style(Style::default()); - textarea.set_placeholder_text("Enter a command"); - textarea.set_block( - Block::new() - .title("command") - .borders(Borders::all()) - .border_style(Style::default().red().on_black()) - .style(Style::default().black().on_white()), - ); - - let mut interactive_inputs = InteractiveInputs::default(); - while !interactive_inputs.quit { - let file = &files[interactive_inputs.file_index]; - let command_output = command_outputs[interactive_inputs.file_index].clone(); - - terminal.draw(|frame| { - interactive_output_ui( - file, - command_output.as_str(), - &mut interactive_inputs, - &mut textarea, - frame, - ); - })?; - interactive_inputs = handle_events(&interactive_inputs)?; - - // prevent an overflow of the index - // and also handle wrapping - interactive_inputs.tab_index %= 3; - interactive_inputs.file_index %= files.len(); - } - - Ok(()) - } - - fn interactive_output_ui( - file: &TaggedFile, - command_output: &str, - interactive_inputs: &mut InteractiveInputs, - text_area: &mut TextArea, - frame: &mut Frame, - ) { - let area = layout(frame.size(), Direction::Vertical, &[1, 0, 1]); - - render_tabs(area[0], frame, interactive_inputs); - - render_tab_content( - file, - command_output, - interactive_inputs.tab_index, - interactive_inputs.scroll_index, - area[1], - frame, - ); - - render_help_menu(area[2], frame); - - if interactive_inputs.command_mode { - interactive_inputs.command_mode = command_mode_input(file, text_area).unwrap(); - command_mode_render(text_area, frame); - } - } - - fn render_tabs(area: Rect, frame: &mut Frame, interactive_inputs: &InteractiveInputs) { - let tabs = Tabs::new(vec!["File Content", "Command Output", "Tags"]) - .style(Style::default().white()) - .highlight_style(Style::default().blue()) - .select(interactive_inputs.tab_index) - .divider(symbols::DOT); - - frame.render_widget(tabs, area); - } - - fn command_mode_render(text_area: &mut TextArea, frame: &mut Frame) { - let layout = - Layout::default().constraints([Constraint::Length(3), Constraint::Min(1)].as_slice()); - - let area = Rect::new(0, frame.size().height / 2, frame.size().width, 10); - - frame.render_widget(Clear, layout.split(area)[0]); - frame.render_widget(text_area.widget(), layout.split(area)[0]); - } - - fn command_mode_input( - file: &TaggedFile, - text_area: &mut TextArea, - ) -> Result { - match crossterm::event::read()?.into() { - Input { key: Key::Esc, .. } => { - return Ok(false); - } - Input { - key: Key::Enter, .. - } => { - execute_command_on_file(&file.path, &text_area.lines()[0]); - return Ok(false); - } - Input { - key: Key::Char('m'), - ctrl: true, - .. - } => {} - input => { - text_area.input(input); - } - } - - Ok(true) - } - - fn render_tab_content( - file: &TaggedFile, - command_output: &str, - tab_index: usize, - scroll_index: u16, - area: Rect, - frame: &mut Frame, - ) { - let content = match tab_index { - 0 => std::fs::read_to_string(&file.path).unwrap(), - 1 => command_output.to_string(), - 2 => { - let mut out_string = String::new(); - for tag in &file.tags { - out_string += tag.as_str(); - out_string.push('\n'); - } - out_string - } - _ => unreachable!(), // tabs are constrained to be between 0 and 2 - }; - - #[allow(clippy::cast_possible_truncation)] - let scroll_index = scroll_index % content.lines().collect_vec().len() as u16; - - let paragraph = Paragraph::new(content) - .block( - Block::new() - .title(file.path.to_str().unwrap()) - .borders(Borders::all()), - ) - .style(Style::new()) - .alignment(Alignment::Left) - .wrap(Wrap { trim: false }) - .scroll((scroll_index, 0)); - - frame.render_widget(paragraph, area); - } - - fn render_help_menu(area: Rect, frame: &mut Frame) { - let keys = [ - ("q", "Quit"), - ("Up-Arrow/k", "Scroll Up"), - ("Down-Arrow/j", "Scroll Down"), - ("n", "Next File"), - ("p", "Previous File"), - ("Tab/Right-Arrow/l", "Next Tab"), - ("Shift+Tab/Left-Arrow/h", "Previous Tab"), - ("c", "Execute a command"), - ]; - - let spans = keys - .iter() - .flat_map(|(key, desc)| { - let key = Span::styled(format!("| {key} "), Style::new().green()); - let desc = Span::styled(format!(" {desc} | "), Style::new().green()); - [key, desc] - }) - .collect_vec(); - - let paragraph = Paragraph::new(Line::from(spans)).alignment(Alignment::Center); - - frame.render_widget(paragraph, area); - } - - /// simple helper method to split an area into multiple sub-areas - /// copied and slightly modified from - /// [here](https://docs.rs/ratatui/latest/src/demo2/root.rs.html#34) - pub fn layout(area: Rect, direction: Direction, heights: &[u16]) -> Rc<[Rect]> { - let constraints = heights - .iter() - .map(|&h| { - if h > 0 { - Constraint::Length(h) - } else { - Constraint::Min(0) - } - }) - .collect_vec(); - Layout::default() - .direction(direction) - .constraints(constraints) - .split(area) - } - - fn handle_events(previous_inputs: &InteractiveInputs) -> io::Result { - let mut interactive_inputs = InteractiveInputs { - tab_index: previous_inputs.tab_index, - file_index: previous_inputs.file_index, - scroll_index: previous_inputs.scroll_index, - command_mode: previous_inputs.command_mode, - ..Default::default() - }; - - if event::poll(std::time::Duration::from_millis(50))? { - if let Event::Key(key) = event::read()? { - if key.kind != event::KeyEventKind::Press { - return Ok(interactive_inputs); - } - - match key.code { - KeyCode::Char('n') => interactive_inputs.file_index += 1, - KeyCode::Char('p') => { - if interactive_inputs.file_index == 0 { - interactive_inputs.file_index = usize::MAX; - } else { - interactive_inputs.file_index -= 1; - } - } - KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { - interactive_inputs.tab_index += 1; - } - KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => { - interactive_inputs.tab_index -= 1; - } - KeyCode::Char('k') | KeyCode::Up => { - if interactive_inputs.scroll_index == 0 { - interactive_inputs.scroll_index = u16::MAX; - } else { - interactive_inputs.scroll_index -= 1; - } - } - KeyCode::Char('j') | KeyCode::Down => { - if interactive_inputs.scroll_index == u16::MAX { - interactive_inputs.scroll_index = u16::MIN; - } else { - interactive_inputs.scroll_index += 1; - } - } - KeyCode::Char('c') => interactive_inputs.command_mode = true, - KeyCode::Char('q') => interactive_inputs.quit = true, - _ => return Ok(interactive_inputs), - } - } - } - - Ok(interactive_inputs) - } -} - fn main() -> Result<(), Box> { let mut args = cli::Cli::new_and_parse(); @@ -505,7 +169,7 @@ fn main() -> Result<(), Box> { } if args.inspect { - interactive_output::interactive_output(&file_matched_index, &command_outputs)?; + inspect::interactive_output(&file_matched_index, &command_outputs)?; disable_raw_mode()?; stdout().execute(LeaveAlternateScreen)?; From a24a3bcf4f08e94827aeb9e1121318f4bef98384 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 16:14:35 +0100 Subject: [PATCH 18/19] fixed last bugs --- src/inspect.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/inspect.rs b/src/inspect.rs index c81e4c1..6e5ae6b 100644 --- a/src/inspect.rs +++ b/src/inspect.rs @@ -134,7 +134,11 @@ fn render_tab_content( }; #[allow(clippy::cast_possible_truncation)] - let scroll_index = scroll_index % content.lines().collect_vec().len() as u16; + let scroll_index = if content.is_empty() { + 0 + } else { + scroll_index % content.lines().collect_vec().len() as u16 + }; let paragraph = Paragraph::new(content) .block( @@ -226,7 +230,11 @@ fn handle_events(previous_inputs: &InteractiveInputs) -> io::Result { - interactive_inputs.tab_index -= 1; + if interactive_inputs.tab_index != 0 { + interactive_inputs.tab_index -= 1; + } else { + interactive_inputs.tab_index = 2; // 2 = last tab + } } KeyCode::Char('k') | KeyCode::Up => { if interactive_inputs.scroll_index == 0 { From 9bab493b352f78b5f826e63e4df03d1cdef129f6 Mon Sep 17 00:00:00 2001 From: miampf Date: Sat, 27 Jan 2024 16:18:48 +0100 Subject: [PATCH 19/19] fixed clippy warnings --- src/commands.rs | 11 ++++++++++- src/inspect.rs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 9ab8f9e..3bccb85 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,6 +3,11 @@ use std::{path::Path, process::Command}; use colored::Colorize; /// `execute_command_on_file` executes a command on a given #FILE#. +/// +/// # Panics +/// +/// This function panics if it fails to convert the given path to a `&str`. +#[must_use] pub fn execute_command_on_file(path: &Path, command: &str) -> String { let command = command.replace("#FILE#", path.to_str().unwrap()); @@ -37,7 +42,11 @@ pub fn execute_command_on_file(path: &Path, command: &str) -> String { } /// `execute_filter_command_on_file` executes a command on a given #FILE# and returns -/// true if the command ran successfully. +/// +/// # Panics +/// +/// This function panics if it fails to convert the given path to a `&str`. +#[must_use] pub fn execute_filter_command_on_file(path: &Path, command: &str) -> bool { let command = command.replace("#FILE#", path.to_str().unwrap()); diff --git a/src/inspect.rs b/src/inspect.rs index 6e5ae6b..3774d6a 100644 --- a/src/inspect.rs +++ b/src/inspect.rs @@ -280,7 +280,7 @@ fn command_mode_input(file: &TaggedFile, text_area: &mut TextArea) -> Result { - execute_command_on_file(&file.path, &text_area.lines()[0]); + let _ = execute_command_on_file(&file.path, &text_area.lines()[0]); return Ok(false); } Input {