diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a088f4..2f95a7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ Types of changes - Security in case of vulnerabilities. --> +### Added + +- A text user interface with progress-bars and modern output (requires a TTY). + ## [0.3.1] - 2022-02-20 ### Added diff --git a/Cargo.lock b/Cargo.lock index af73a4ee..244c5bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,12 +6,15 @@ version = 3 name = "alejandra" version = "0.3.1" dependencies = [ + "atty", "clap", "indoc", "rand", "rayon", "rnix", "rowan 0.15.3", + "termion", + "tui", "walkdir", ] @@ -38,6 +41,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cbitset" version = "0.2.0" @@ -234,6 +243,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "os_str_bytes" version = "6.0.0" @@ -304,6 +319,24 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + [[package]] name = "rnix" version = "0.10.1" @@ -392,6 +425,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "text-size" version = "1.1.0" @@ -404,6 +449,31 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +[[package]] +name = "tui" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ed0a32c88b039b73f1b6c5acbd0554bfa5b6be94467375fd947c4de3a02271" +dependencies = [ + "bitflags", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unindent" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 19b4bd79..7440f5bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,12 @@ [dependencies] +atty = "*" clap = { version = "*", features = ["cargo"] } indoc = "*" rand = "*" rayon = "*" rnix = "*" +termion = "*" +tui = { version = "*", default-features = false, features = ["termion"] } rowan = "*" walkdir = "*" diff --git a/README.md b/README.md index f6088145..ccb898a1 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@

+ + + + ## Features - ✔️ **Fast** diff --git a/src/cli.rs b/src/cli.rs index 0d45048a..f3763c7d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ pub fn parse(args: Vec) -> clap::ArgMatches { clap::Command::new("Alejandra") .about("The Uncompromising Nix Code Formatter.") - .version(clap::crate_version!()) + .version(crate::version::VERSION) .arg( clap::Arg::new("debug") .help("Enable debug mode.") @@ -33,3 +33,270 @@ pub fn parse(args: Vec) -> clap::ArgMatches { )) .get_matches_from(args) } + +pub fn stdin(config: crate::config::Config) -> std::io::Result<()> { + use std::io::Read; + + eprintln!("Formatting stdin, run with --help to see all options."); + let mut stdin = String::new(); + std::io::stdin().read_to_string(&mut stdin).unwrap(); + print!("{}", crate::format::string(&config, "stdin".to_string(), stdin)); + + Ok(()) +} + +pub fn simple( + config: crate::config::Config, + paths: Vec, +) -> std::io::Result<()> { + use rayon::prelude::*; + + eprintln!("Formatting {} files.", paths.len()); + + let (results, errors): (Vec<_>, Vec<_>) = paths + .par_iter() + .map(|path| -> std::io::Result { + crate::format::file(&config, path.to_string()).map(|changed| { + if changed { + eprintln!("Formatted: {}", &path); + } + changed + }) + }) + .partition(Result::is_ok); + + eprintln!( + "Changed: {}", + results.into_iter().map(Result::unwrap).filter(|&x| x).count(), + ); + eprintln!("Errors: {}", errors.len(),); + + Ok(()) +} + +pub fn tui( + config: crate::config::Config, + paths: Vec, +) -> std::io::Result<()> { + use rayon::prelude::*; + use termion::{input::TermRead, raw::IntoRawMode}; + + struct FormattedPath { + path: String, + result: std::io::Result, + } + + enum Event { + FormattedPath(FormattedPath), + FormattingFinished, + Input(termion::event::Key), + Tick, + } + + let paths_to_format = paths.len(); + + let stdout = std::io::stderr().into_raw_mode()?; + // let stdout = termion::screen::AlternateScreen::from(stdout); + let backend = tui::backend::TermionBackend::new(stdout); + let mut terminal = tui::Terminal::new(backend)?; + + let (sender, receiver) = std::sync::mpsc::channel(); + + // Listen to user input + let sender_keys = sender.clone(); + std::thread::spawn(move || { + let stdin = std::io::stdin(); + for key in stdin.keys().flatten() { + if let Err(error) = sender_keys.send(Event::Input(key)) { + eprintln!("{}", error); + return; + } + } + }); + + // Listen to the clock + let sender_clock = sender.clone(); + std::thread::spawn(move || { + loop { + if let Err(error) = sender_clock.send(Event::Tick) { + eprintln!("{}", error); + break; + } + std::thread::sleep(std::time::Duration::from_millis(250)); + } + }); + + // Listen to the processed items + let sender_paths = sender.clone(); + let sender_finished = sender.clone(); + std::thread::spawn(move || { + paths.into_par_iter().for_each_with(sender_paths, |sender, path| { + let result = crate::format::file(&config, path.clone()); + + if let Err(error) = sender + .send(Event::FormattedPath(FormattedPath { path, result })) + { + eprintln!("{}", error); + } + }); + + if let Err(error) = sender_finished.send(Event::FormattingFinished) { + eprintln!("{}", error); + } + }); + + terminal.clear()?; + + let mut finished = false; + let mut paths_with_errors: usize = 0; + let mut paths_changed: usize = 0; + let mut paths_unchanged: usize = 0; + let mut formatted_paths = std::collections::LinkedList::new(); + + while !finished { + loop { + match receiver.try_recv() { + Ok(event) => match event { + Event::FormattedPath(formatted_path) => { + match formatted_path.result { + Ok(changed) => { + if changed { + paths_changed += 1; + } else { + paths_unchanged += 1; + } + } + Err(_) => { + paths_with_errors += 1; + } + }; + + formatted_paths.push_back(formatted_path); + if formatted_paths.len() > 8 { + formatted_paths.pop_front(); + } + } + Event::FormattingFinished => { + finished = true; + } + Event::Input(key) => { + match key { + termion::event::Key::Ctrl('c') => { + return Err( + std::io::ErrorKind::Interrupted.into() + ); + } + _ => {} + }; + } + Event::Tick => { + break; + } + }, + Err(_) => {} + } + } + + terminal.draw(|frame| { + let sizes = tui::layout::Layout::default() + .constraints([ + tui::layout::Constraint::Length(3), + tui::layout::Constraint::Length(3), + tui::layout::Constraint::Max(8), + tui::layout::Constraint::Length(0), + ]) + .split(frame.size()); + let size = tui::layout::Rect::new(0, 0, 0, 0) + .union(sizes[0]) + .union(sizes[1]) + .union(sizes[2]); + + let header = tui::widgets::Paragraph::new(vec![ + tui::text::Spans::from(vec![ + tui::text::Span::styled( + "Alejandra", + tui::style::Style::default() + .fg(tui::style::Color::Green), + ), + tui::text::Span::raw(" "), + tui::text::Span::raw(crate::version::VERSION), + ]), + tui::text::Spans::from(vec![tui::text::Span::raw( + "The Uncompromising Nix Code Formatter", + )]), + ]) + .alignment(tui::layout::Alignment::Center) + .style( + tui::style::Style::default() + .bg(tui::style::Color::Black) + .fg(tui::style::Color::White), + ); + + let progress = tui::widgets::Gauge::default() + .block( + tui::widgets::Block::default() + .borders(tui::widgets::Borders::ALL) + .title(format!( + " Formatting ({} changed, {} unchanged, {} \ + errors) ", + paths_changed, paths_unchanged, paths_with_errors + )), + ) + .gauge_style( + tui::style::Style::default() + .fg(tui::style::Color::Green) + .bg(tui::style::Color::Black) + .add_modifier(tui::style::Modifier::ITALIC), + ) + .percent( + (100 * (paths_changed + + paths_unchanged + + paths_with_errors) + / paths_to_format) as u16, + ) + .style( + tui::style::Style::default() + .bg(tui::style::Color::Black) + .fg(tui::style::Color::White), + ); + let logger = tui::widgets::Paragraph::new( + formatted_paths + .iter() + .map(|formatted_path| { + tui::text::Spans::from(vec![ + match &formatted_path.result { + Ok(changed) => tui::text::Span::styled( + if *changed { + "CHANGED " + } else { + "UNCHANGED " + }, + tui::style::Style::default() + .fg(tui::style::Color::Green), + ), + Err(_) => tui::text::Span::styled( + "ERROR ", + tui::style::Style::default() + .fg(tui::style::Color::Red), + ), + }, + tui::text::Span::raw(formatted_path.path.clone()), + ]) + }) + .collect::>(), + ) + .style( + tui::style::Style::default() + .bg(tui::style::Color::Black) + .fg(tui::style::Color::White), + ); + + frame.render_widget(header, sizes[0]); + frame.render_widget(progress, sizes[1]); + frame.render_widget(logger, sizes[2]); + frame.set_cursor(size.width, size.height); + })?; + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 0d67be42..61531a01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,4 @@ pub mod format; pub mod position; pub mod rules; pub mod utils; +pub mod version; diff --git a/src/main.rs b/src/main.rs index 3d95dc58..f4282cd2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -use rayon::prelude::*; -use std::io::Read; - fn main() -> std::io::Result<()> { let matches = alejandra::cli::parse(std::env::args().collect()); @@ -12,36 +9,17 @@ fn main() -> std::io::Result<()> { let paths: Vec = alejandra::find::nix_files(paths.collect()); - eprintln!("Formatting {} files.", paths.len()); - - let (results, errors): (Vec<_>, Vec<_>) = paths - .par_iter() - .map(|path| -> std::io::Result { - alejandra::format::file(&config, path.to_string()).map( - |changed| { - if changed { - eprintln!("Formatted: {}", &path); - } - changed - }, - ) - }) - .partition(Result::is_ok); - eprintln!( - "Errors/Changed/Formatted: {}/{}/{}", - errors.len(), - results.into_iter().map(Result::unwrap).filter(|&x| x).count(), - paths.len() - ); + if atty::is(atty::Stream::Stderr) + && atty::is(atty::Stream::Stdin) + && atty::is(atty::Stream::Stdout) + { + alejandra::cli::tui(config, paths)?; + } else { + alejandra::cli::simple(config, paths)?; + } } None => { - eprintln!("Formatting stdin."); - let mut stdin = String::new(); - std::io::stdin().read_to_string(&mut stdin).unwrap(); - print!( - "{}", - alejandra::format::string(&config, "stdin".to_string(), stdin) - ); + alejandra::cli::stdin(config)?; } } diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 00000000..725f9e2a --- /dev/null +++ b/src/version.rs @@ -0,0 +1 @@ +pub const VERSION: &str = clap::crate_version!();