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:
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.
|
||||
-->
|
||||
|
||||
### 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
70
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[dependencies]
|
||||
atty = "*"
|
||||
clap = { version = "*", features = ["cargo"] }
|
||||
indoc = "*"
|
||||
rand = "*"
|
||||
rayon = "*"
|
||||
rnix = "*"
|
||||
termion = "*"
|
||||
tui = { version = "*", default-features = false, features = ["termion"] }
|
||||
rowan = "*"
|
||||
walkdir = "*"
|
||||
|
||||
|
|
|
@ -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**
|
||||
|
|
269
src/cli.rs
269
src/cli.rs
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -8,3 +8,4 @@ pub mod format;
|
|||
pub mod position;
|
||||
pub mod rules;
|
||||
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<()> {
|
||||
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
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