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!();