mirror of
https://github.com/RGBCube/alejandra
synced 2025-07-31 12:37:45 +00:00
feat: add a text user interface
This commit is contained in:
parent
3275487e18
commit
45ad71e6ee
8 changed files with 360 additions and 32 deletions
|
@ -17,6 +17,10 @@ Types of changes
|
||||||
- Security in case of vulnerabilities.
|
- 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
|
## [0.3.1] - 2022-02-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
70
Cargo.lock
generated
70
Cargo.lock
generated
|
@ -6,12 +6,15 @@ version = 3
|
||||||
name = "alejandra"
|
name = "alejandra"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atty",
|
||||||
"clap",
|
"clap",
|
||||||
"indoc",
|
"indoc",
|
||||||
"rand",
|
"rand",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rnix",
|
"rnix",
|
||||||
"rowan 0.15.3",
|
"rowan 0.15.3",
|
||||||
|
"termion",
|
||||||
|
"tui",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -38,6 +41,12 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cbitset"
|
name = "cbitset"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -234,6 +243,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numtoa"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_str_bytes"
|
name = "os_str_bytes"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
|
@ -304,6 +319,24 @@ dependencies = [
|
||||||
"num_cpus",
|
"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]]
|
[[package]]
|
||||||
name = "rnix"
|
name = "rnix"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
@ -392,6 +425,18 @@ dependencies = [
|
||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "text-size"
|
name = "text-size"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -404,6 +449,31 @@ version = "0.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
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]]
|
[[package]]
|
||||||
name = "unindent"
|
name = "unindent"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
atty = "*"
|
||||||
clap = { version = "*", features = ["cargo"] }
|
clap = { version = "*", features = ["cargo"] }
|
||||||
indoc = "*"
|
indoc = "*"
|
||||||
rand = "*"
|
rand = "*"
|
||||||
rayon = "*"
|
rayon = "*"
|
||||||
rnix = "*"
|
rnix = "*"
|
||||||
|
termion = "*"
|
||||||
|
tui = { version = "*", default-features = false, features = ["termion"] }
|
||||||
rowan = "*"
|
rowan = "*"
|
||||||
walkdir = "*"
|
walkdir = "*"
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,10 @@
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<a href="https://asciinema.org/a/470438" target="_blank">
|
||||||
|
<img src="https://asciinema.org/a/470438.svg" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ✔️ **Fast**
|
- ✔️ **Fast**
|
||||||
|
|
269
src/cli.rs
269
src/cli.rs
|
@ -1,7 +1,7 @@
|
||||||
pub fn parse(args: Vec<String>) -> clap::ArgMatches {
|
pub fn parse(args: Vec<String>) -> clap::ArgMatches {
|
||||||
clap::Command::new("Alejandra")
|
clap::Command::new("Alejandra")
|
||||||
.about("The Uncompromising Nix Code Formatter.")
|
.about("The Uncompromising Nix Code Formatter.")
|
||||||
.version(clap::crate_version!())
|
.version(crate::version::VERSION)
|
||||||
.arg(
|
.arg(
|
||||||
clap::Arg::new("debug")
|
clap::Arg::new("debug")
|
||||||
.help("Enable debug mode.")
|
.help("Enable debug mode.")
|
||||||
|
@ -33,3 +33,270 @@ pub fn parse(args: Vec<String>) -> clap::ArgMatches {
|
||||||
))
|
))
|
||||||
.get_matches_from(args)
|
.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(())
|
||||||
|
}
|
||||||
|
|
|
@ -8,3 +8,4 @@ pub mod format;
|
||||||
pub mod position;
|
pub mod position;
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod version;
|
||||||
|
|
40
src/main.rs
40
src/main.rs
|
@ -1,6 +1,3 @@
|
||||||
use rayon::prelude::*;
|
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
let matches = alejandra::cli::parse(std::env::args().collect());
|
let matches = alejandra::cli::parse(std::env::args().collect());
|
||||||
|
|
||||||
|
@ -12,36 +9,17 @@ fn main() -> std::io::Result<()> {
|
||||||
let paths: Vec<String> =
|
let paths: Vec<String> =
|
||||||
alejandra::find::nix_files(paths.collect());
|
alejandra::find::nix_files(paths.collect());
|
||||||
|
|
||||||
eprintln!("Formatting {} files.", paths.len());
|
if atty::is(atty::Stream::Stderr)
|
||||||
|
&& atty::is(atty::Stream::Stdin)
|
||||||
let (results, errors): (Vec<_>, Vec<_>) = paths
|
&& atty::is(atty::Stream::Stdout)
|
||||||
.par_iter()
|
{
|
||||||
.map(|path| -> std::io::Result<bool> {
|
alejandra::cli::tui(config, paths)?;
|
||||||
alejandra::format::file(&config, path.to_string()).map(
|
} else {
|
||||||
|changed| {
|
alejandra::cli::simple(config, paths)?;
|
||||||
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()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Formatting stdin.");
|
alejandra::cli::stdin(config)?;
|
||||||
let mut stdin = String::new();
|
|
||||||
std::io::stdin().read_to_string(&mut stdin).unwrap();
|
|
||||||
print!(
|
|
||||||
"{}",
|
|
||||||
alejandra::format::string(&config, "stdin".to_string(), stdin)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1
src/version.rs
Normal file
1
src/version.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const VERSION: &str = clap::crate_version!();
|
Loading…
Add table
Add a link
Reference in a new issue