use aisling::{Loader, LoaderConfig, LoaderKind, LoaderProgress};
use std::env;
use std::io::{self, Write};
const LOADER_WIDTH: usize = 56;
pub fn text(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
skip_escape(&mut chars);
continue;
}
match ch {
'\n' | '\t' => output.push(ch),
'\r' => {}
ch if ch.is_control() => {}
ch => output.push(ch),
}
}
output
}
pub fn inline(input: &str) -> String {
let sanitized = text(input);
let mut output = String::with_capacity(sanitized.len());
let mut last_was_space = false;
for ch in sanitized.chars() {
if ch.is_whitespace() {
if !last_was_space {
output.push(' ');
last_was_space = true;
}
} else {
output.push(ch);
last_was_space = false;
}
}
output.trim().to_string()
}
fn skip_escape(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
match chars.next() {
Some('[') => skip_csi(chars),
Some(']') | Some('P') | Some('^') | Some('_') | Some('X') => skip_string_escape(chars),
Some(_) | None => {}
}
}
fn skip_csi(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
for ch in chars.by_ref() {
if ('@'..='~').contains(&ch) {
break;
}
}
}
fn skip_string_escape(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
while let Some(ch) = chars.next() {
if ch == '\x07' {
break;
}
if ch == '\x1b' && chars.next() == Some('\\') {
break;
}
}
}
pub struct Progress {
enabled: bool,
loader: Loader,
current: u64,
total: u64,
tick: usize,
}
impl Progress {
pub fn new(label: &str, total: usize, requested: bool) -> Self {
let enabled = requested && total > 1 && progress_allowed() && stderr_is_terminal();
let loader = Loader::with_config(
LoaderKind::Tqdm,
LoaderConfig::default()
.with_size(LOADER_WIDTH, 1)
.with_label(inline(label))
.with_unit("pkg")
.with_fraction(true)
.without_color(),
);
Self {
enabled,
loader,
current: 0,
total: total as u64,
tick: 0,
}
}
pub fn advance(&mut self, amount: usize) {
if !self.enabled {
return;
}
self.current = self.current.saturating_add(amount as u64).min(self.total);
self.render();
}
pub fn finish(&mut self) {
if !self.enabled {
return;
}
self.current = self.total;
self.render();
let _ = writeln!(io::stderr());
self.enabled = false;
}
fn render(&mut self) {
let progress = LoaderProgress::from_counts(self.current, self.total.max(1));
let line = self.loader.line(self.tick, progress);
let _ = write!(io::stderr(), "\r{line}\x1b[K");
let _ = io::stderr().flush();
self.tick = self.tick.wrapping_add(1);
}
}
impl Drop for Progress {
fn drop(&mut self) {
if self.enabled {
let _ = writeln!(io::stderr());
}
}
}
fn progress_allowed() -> bool {
!matches!(
env::var("KNOTT_PROGRESS").as_deref(),
Ok("0") | Ok("false") | Ok("off")
)
}
#[cfg(unix)]
fn stderr_is_terminal() -> bool {
unsafe { libc::isatty(libc::STDERR_FILENO) == 1 }
}
#[cfg(not(unix))]
fn stderr_is_terminal() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_terminal_control_sequences() {
let input = "pkg\x1b[31m-red\x1b[0m\x07\rname\nnext\x1b]0;title\x07line";
assert_eq!(text(input), "pkg-redname\nnextline");
}
#[test]
fn inline_collapses_multiline_control_text() {
let input = " hello\n\t\x1b[2Jworld\r\x08 ";
assert_eq!(inline(input), "hello world");
}
}