2024-08-24 01:14:12 +03:00
|
|
|
use crossterm::{
|
|
|
|
cursor::MoveTo,
|
2024-09-01 23:04:09 +03:00
|
|
|
style::{Attribute, Color, SetAttribute, SetForegroundColor},
|
2024-08-24 01:14:12 +03:00
|
|
|
terminal::{Clear, ClearType},
|
2024-08-26 00:53:50 +03:00
|
|
|
Command, QueueableCommand,
|
2024-08-24 01:14:12 +03:00
|
|
|
};
|
2024-09-04 03:19:45 +03:00
|
|
|
use std::{
|
|
|
|
fmt, fs,
|
|
|
|
io::{self, BufRead, StdoutLock, Write},
|
|
|
|
};
|
2024-08-24 01:14:12 +03:00
|
|
|
|
2024-10-03 01:28:42 +03:00
|
|
|
pub const PROGRESS_FAILED_COLOR: Color = Color::Red;
|
|
|
|
pub const PROGRESS_SUCCESS_COLOR: Color = Color::Green;
|
|
|
|
pub const PROGRESS_PENDING_COLOR: Color = Color::Blue;
|
|
|
|
|
2024-08-26 05:29:58 +03:00
|
|
|
pub struct MaxLenWriter<'a, 'b> {
|
|
|
|
pub stdout: &'a mut StdoutLock<'b>,
|
|
|
|
len: usize,
|
|
|
|
max_len: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, 'b> MaxLenWriter<'a, 'b> {
|
|
|
|
#[inline]
|
|
|
|
pub fn new(stdout: &'a mut StdoutLock<'b>, max_len: usize) -> Self {
|
|
|
|
Self {
|
|
|
|
stdout,
|
|
|
|
len: 0,
|
|
|
|
max_len,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Additional is for emojis that take more space.
|
|
|
|
#[inline]
|
|
|
|
pub fn add_to_len(&mut self, additional: usize) {
|
|
|
|
self.len += additional;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait CountedWrite<'a> {
|
|
|
|
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()>;
|
|
|
|
fn write_str(&mut self, unicode: &str) -> io::Result<()>;
|
|
|
|
fn stdout(&mut self) -> &mut StdoutLock<'a>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, 'b> CountedWrite<'b> for MaxLenWriter<'a, 'b> {
|
|
|
|
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
|
|
|
let n = ascii.len().min(self.max_len.saturating_sub(self.len));
|
2024-08-26 05:41:26 +03:00
|
|
|
if n > 0 {
|
|
|
|
self.stdout.write_all(&ascii[..n])?;
|
|
|
|
self.len += n;
|
|
|
|
}
|
2024-08-26 05:29:58 +03:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
|
|
|
|
if let Some((ind, c)) = unicode
|
|
|
|
.char_indices()
|
|
|
|
.take(self.max_len.saturating_sub(self.len))
|
|
|
|
.last()
|
|
|
|
{
|
|
|
|
self.stdout
|
|
|
|
.write_all(&unicode.as_bytes()[..ind + c.len_utf8()])?;
|
|
|
|
self.len += ind + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn stdout(&mut self) -> &mut StdoutLock<'b> {
|
|
|
|
self.stdout
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> CountedWrite<'a> for StdoutLock<'a> {
|
|
|
|
#[inline]
|
|
|
|
fn write_ascii(&mut self, ascii: &[u8]) -> io::Result<()> {
|
|
|
|
self.write_all(ascii)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn write_str(&mut self, unicode: &str) -> io::Result<()> {
|
|
|
|
self.write_all(unicode.as_bytes())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn stdout(&mut self) -> &mut StdoutLock<'a> {
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-03 01:28:42 +03:00
|
|
|
/// Simple terminal progress bar
|
2024-08-26 05:29:58 +03:00
|
|
|
pub fn progress_bar<'a>(
|
|
|
|
writer: &mut impl CountedWrite<'a>,
|
2024-08-24 18:17:56 +03:00
|
|
|
progress: u16,
|
|
|
|
total: u16,
|
|
|
|
line_width: u16,
|
2024-10-03 01:28:42 +03:00
|
|
|
) -> io::Result<()> {
|
|
|
|
progress_bar_with_success(writer, 0, 0, progress, total, line_width)
|
|
|
|
}
|
|
|
|
/// Terminal progress bar with three states (pending + failed + success)
|
|
|
|
pub fn progress_bar_with_success<'a>(
|
|
|
|
writer: &mut impl CountedWrite<'a>,
|
|
|
|
pending: u16,
|
|
|
|
failed: u16,
|
|
|
|
success: u16,
|
|
|
|
total: u16,
|
|
|
|
line_width: u16,
|
2024-08-24 18:17:56 +03:00
|
|
|
) -> io::Result<()> {
|
2024-09-01 23:04:09 +03:00
|
|
|
debug_assert!(total < 1000);
|
2024-10-03 01:28:42 +03:00
|
|
|
debug_assert!((pending + failed + success) <= total);
|
2024-08-24 18:17:56 +03:00
|
|
|
|
|
|
|
const PREFIX: &[u8] = b"Progress: [";
|
|
|
|
const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
|
2024-09-01 23:04:09 +03:00
|
|
|
const POSTFIX_WIDTH: u16 = "] xxx/xxx".len() as u16;
|
2024-08-24 18:17:56 +03:00
|
|
|
const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH;
|
|
|
|
const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4;
|
|
|
|
|
|
|
|
if line_width < MIN_LINE_WIDTH {
|
2024-08-26 05:29:58 +03:00
|
|
|
writer.write_ascii(b"Progress: ")?;
|
|
|
|
// Integers are in ASCII.
|
2024-10-03 01:28:42 +03:00
|
|
|
return writer.write_ascii(format!("{}/{total}", failed + success).as_bytes());
|
2024-08-24 18:17:56 +03:00
|
|
|
}
|
|
|
|
|
2024-08-26 05:29:58 +03:00
|
|
|
let stdout = writer.stdout();
|
2024-08-24 18:17:56 +03:00
|
|
|
stdout.write_all(PREFIX)?;
|
|
|
|
|
|
|
|
let width = line_width - WRAPPER_WIDTH;
|
2024-10-03 01:28:42 +03:00
|
|
|
let mut failed_end = (width * failed) / total;
|
|
|
|
let mut success_end = (width * (failed + success)) / total;
|
|
|
|
let mut pending_end = (width * (failed + success + pending)) / total;
|
|
|
|
|
|
|
|
// In case the range boundaries overlap, "pending" has priority over both
|
|
|
|
// "failed" and "success" (don't show the bar as "complete" when we are
|
|
|
|
// still checking some things).
|
|
|
|
// "Failed" has priority over "success" (don't show 100% success if we
|
|
|
|
// have some failures, at the risk of showing 100% failures even with
|
|
|
|
// a few successes).
|
|
|
|
//
|
|
|
|
// "Failed" already has priority over "success" because it's displayed
|
|
|
|
// first. But "pending" is last so we need to fix "success"/"failed".
|
|
|
|
if pending > 0 {
|
|
|
|
pending_end = pending_end.max(1);
|
|
|
|
if pending_end == success_end {
|
|
|
|
success_end -= 1;
|
|
|
|
}
|
|
|
|
if pending_end == failed_end {
|
|
|
|
failed_end -= 1;
|
|
|
|
}
|
2024-08-24 18:17:56 +03:00
|
|
|
|
2024-10-03 01:28:42 +03:00
|
|
|
// This will replace the last character of the "pending" range with
|
|
|
|
// the arrow char ('>'). This ensures that even if the progress bar
|
|
|
|
// is filled (everything either done or pending), we'll still see
|
|
|
|
// the '>' as long as we are not fully done.
|
|
|
|
pending_end -= 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if failed > 0 {
|
|
|
|
stdout.queue(SetForegroundColor(PROGRESS_FAILED_COLOR))?;
|
|
|
|
for _ in 0..failed_end {
|
|
|
|
stdout.write_all(b"#")?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
stdout.queue(SetForegroundColor(PROGRESS_SUCCESS_COLOR))?;
|
|
|
|
for _ in failed_end..success_end {
|
2024-08-24 18:17:56 +03:00
|
|
|
stdout.write_all(b"#")?;
|
|
|
|
}
|
|
|
|
|
2024-10-03 01:28:42 +03:00
|
|
|
if pending > 0 {
|
|
|
|
stdout.queue(SetForegroundColor(PROGRESS_PENDING_COLOR))?;
|
|
|
|
|
|
|
|
for _ in success_end..pending_end {
|
|
|
|
stdout.write_all(b"#")?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if pending_end < width {
|
2024-08-24 18:17:56 +03:00
|
|
|
stdout.write_all(b">")?;
|
|
|
|
}
|
|
|
|
|
2024-10-03 01:28:42 +03:00
|
|
|
let width_minus_filled = width - pending_end;
|
2024-08-24 18:17:56 +03:00
|
|
|
if width_minus_filled > 1 {
|
|
|
|
let red_part_width = width_minus_filled - 1;
|
|
|
|
stdout.queue(SetForegroundColor(Color::Red))?;
|
|
|
|
for _ in 0..red_part_width {
|
|
|
|
stdout.write_all(b"-")?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-01 23:04:09 +03:00
|
|
|
stdout.queue(SetForegroundColor(Color::Reset))?;
|
|
|
|
|
2024-10-03 01:28:42 +03:00
|
|
|
write!(stdout, "] {:>3}/{}", failed + success, total)
|
2024-08-24 18:17:56 +03:00
|
|
|
}
|
|
|
|
|
2024-08-08 03:45:18 +03:00
|
|
|
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
2024-08-24 01:14:12 +03:00
|
|
|
stdout
|
|
|
|
.queue(MoveTo(0, 0))?
|
|
|
|
.queue(Clear(ClearType::All))?
|
|
|
|
.queue(Clear(ClearType::Purge))
|
|
|
|
.map(|_| ())
|
2024-08-08 03:45:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
|
|
|
|
stdout.flush()?;
|
|
|
|
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
|
2024-08-26 23:03:09 +03:00
|
|
|
stdout.write_all(b"\n")
|
2024-08-08 03:45:18 +03:00
|
|
|
}
|
2024-08-26 00:53:50 +03:00
|
|
|
|
2024-09-04 03:19:45 +03:00
|
|
|
/// Canonicalize, convert to string and remove verbatim part on Windows.
|
|
|
|
pub fn canonicalize(path: &str) -> Option<String> {
|
|
|
|
fs::canonicalize(path)
|
|
|
|
.ok()?
|
|
|
|
.into_os_string()
|
|
|
|
.into_string()
|
|
|
|
.ok()
|
|
|
|
.map(|mut path| {
|
|
|
|
// Windows itself can't handle its verbatim paths.
|
|
|
|
if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
|
|
|
|
path.drain(..4);
|
|
|
|
}
|
|
|
|
|
|
|
|
path
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-08-26 05:29:58 +03:00
|
|
|
pub fn terminal_file_link<'a>(
|
|
|
|
writer: &mut impl CountedWrite<'a>,
|
|
|
|
path: &str,
|
2024-09-04 03:19:45 +03:00
|
|
|
canonical_path: &str,
|
2024-08-26 05:29:58 +03:00
|
|
|
color: Color,
|
|
|
|
) -> io::Result<()> {
|
|
|
|
writer
|
|
|
|
.stdout()
|
2024-08-26 00:53:50 +03:00
|
|
|
.queue(SetForegroundColor(color))?
|
|
|
|
.queue(SetAttribute(Attribute::Underlined))?;
|
2024-08-26 05:29:58 +03:00
|
|
|
writer.stdout().write_all(b"\x1b]8;;file://")?;
|
|
|
|
writer.stdout().write_all(canonical_path.as_bytes())?;
|
|
|
|
writer.stdout().write_all(b"\x1b\\")?;
|
|
|
|
// Only this part is visible.
|
|
|
|
writer.write_str(path)?;
|
|
|
|
writer.stdout().write_all(b"\x1b]8;;\x1b\\")?;
|
|
|
|
writer
|
|
|
|
.stdout()
|
2024-08-26 01:48:12 +03:00
|
|
|
.queue(SetForegroundColor(Color::Reset))?
|
|
|
|
.queue(SetAttribute(Attribute::NoUnderline))?;
|
2024-08-26 00:53:50 +03:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn write_ansi(output: &mut Vec<u8>, command: impl Command) {
|
|
|
|
struct FmtWriter<'a>(&'a mut Vec<u8>);
|
|
|
|
|
|
|
|
impl fmt::Write for FmtWriter<'_> {
|
|
|
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
|
|
|
self.0.extend_from_slice(s.as_bytes());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let _ = command.write_ansi(&mut FmtWriter(output));
|
|
|
|
}
|