1
Fork 0
mirror of https://github.com/RGBCube/alejandra synced 2025-07-30 12:07:46 +00:00

feat: add a text user interface

This commit is contained in:
Kevin Amado 2022-02-20 19:42:42 -05:00
parent 3275487e18
commit 45ad71e6ee
8 changed files with 360 additions and 32 deletions

View file

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

70
Cargo.lock generated
View file

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

View file

@ -1,9 +1,12 @@
[dependencies]
atty = "*"
clap = { version = "*", features = ["cargo"] }
indoc = "*"
rand = "*"
rayon = "*"
rnix = "*"
termion = "*"
tui = { version = "*", default-features = false, features = ["termion"] }
rowan = "*"
walkdir = "*"

View file

@ -48,6 +48,10 @@
</a>
</p>
<a href="https://asciinema.org/a/470438" target="_blank">
<img src="https://asciinema.org/a/470438.svg" />
</a>
## Features
- ✔️ **Fast**

View file

@ -1,7 +1,7 @@
pub fn parse(args: Vec<String>) -> 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<String>) -> 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<String>,
) -> std::io::Result<()> {
use rayon::prelude::*;
eprintln!("Formatting {} files.", paths.len());
let (results, errors): (Vec<_>, Vec<_>) = paths
.par_iter()
.map(|path| -> std::io::Result<bool> {
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<String>,
) -> std::io::Result<()> {
use rayon::prelude::*;
use termion::{input::TermRead, raw::IntoRawMode};
struct FormattedPath {
path: String,
result: std::io::Result<bool>,
}
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::<Vec<tui::text::Spans>>(),
)
.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(())
}

View file

@ -8,3 +8,4 @@ pub mod format;
pub mod position;
pub mod rules;
pub mod utils;
pub mod version;

View file

@ -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<String> =
alejandra::find::nix_files(paths.collect());
eprintln!("Formatting {} files.", paths.len());
let (results, errors): (Vec<_>, Vec<_>) = paths
.par_iter()
.map(|path| -> std::io::Result<bool> {
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)?;
}
}

1
src/version.rs Normal file
View file

@ -0,0 +1 @@
pub const VERSION: &str = clap::crate_version!();