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()
}
pub fn block(input: &str) {
print!("{}", text(input));
}
pub fn line(input: impl AsRef<str>) {
println!("{}", text(input.as_ref()));
}
pub fn inline_line(input: impl AsRef<str>) {
println!("{}", inline(input.as_ref()));
}
pub fn status(label: &str, value: impl AsRef<str>) {
println!(":: {}: {}", inline(label), inline(value.as_ref()));
}
pub fn warning(message: impl AsRef<str>) {
eprintln!("warning: {}", inline(message.as_ref()));
}
pub fn error(message: impl AsRef<str>) {
eprintln!("error: {}", inline(message.as_ref()));
}
pub fn field(label: &str, value: impl AsRef<str>) {
println!("{:<16}: {}", inline(label), inline(value.as_ref()));
}
pub fn join_inline<I, S>(values: I, separator: &str) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut output = String::new();
for value in values {
if !output.is_empty() {
output.push_str(separator);
}
output.push_str(&inline(value.as_ref()));
}
output
}
pub fn prompt(prompt: &str) -> io::Result<()> {
print!("{}", inline(prompt));
io::stdout().flush()
}
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 {
loader: Option<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 = enabled.then(|| {
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 {
loader,
current: 0,
total: total as u64,
tick: 0,
}
}
pub fn advance(&mut self, amount: usize) {
if self.loader.is_none() {
return;
}
self.current = self.current.saturating_add(amount as u64).min(self.total);
self.render();
}
pub fn finish(&mut self) {
if self.loader.is_none() {
return;
}
self.current = self.total;
self.render();
let _ = writeln!(io::stderr());
self.loader = None;
}
fn render(&mut self) {
let Some(loader) = &self.loader else {
return;
};
let progress = LoaderProgress::from_counts(self.current, self.total.max(1));
let line = 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.loader.is_some() {
let _ = writeln!(io::stderr());
}
}
}
fn progress_allowed() -> bool {
env::var("KNOTT_PROGRESS").map_or(true, |value| {
let value = value.trim();
!(value == "0" || value.eq_ignore_ascii_case("false") || value.eq_ignore_ascii_case("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");
}
#[test]
fn joins_inline_values_with_sanitization() {
let values = ["foo", "\x1b[31mbar\x1b[0m", "baz\nqux"];
assert_eq!(join_inline(values, " "), "foo bar baz qux");
}
}