Compare commits
3 Commits
69de65ee89
...
f7fe3b0bb0
| Author | SHA1 | Date | |
|---|---|---|---|
| f7fe3b0bb0 | |||
| 0b68d803e8 | |||
| 01743c3c75 |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -114,9 +114,9 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.18.2"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
|
||||
checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
|
||||
dependencies = [
|
||||
"console",
|
||||
"portable-atomic",
|
||||
|
||||
@@ -15,7 +15,7 @@ opt-level = 3
|
||||
[dependencies]
|
||||
color-eyre = "0.6.5"
|
||||
console = "0.16.1"
|
||||
indicatif = "0.18.2"
|
||||
indicatif = "0.18.3"
|
||||
owo-colors = "4.2.3"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = { version = "1.0.145", features = ["raw_value"] }
|
||||
|
||||
@@ -15,7 +15,9 @@ use crate::action_raw::RawAction;
|
||||
#[derive(Clone, Copy, Debug, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[derive(Default)]
|
||||
pub enum ActionType {
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
CopyPath = 100,
|
||||
FileTransfer = 101,
|
||||
@@ -32,11 +34,6 @@ pub enum ActionType {
|
||||
FetchTree = 112,
|
||||
}
|
||||
|
||||
impl Default for ActionType {
|
||||
fn default() -> Self {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// --- BuildStepId
|
||||
|
||||
@@ -1,140 +1,46 @@
|
||||
pub const DOWNLOAD_STYLE: owo_colors::Style = owo_colors::Style::new().blue().bold();
|
||||
pub const EXTRACT_STYLE: owo_colors::Style = owo_colors::Style::new().green().bold();
|
||||
use std::{borrow::Cow, fmt::Display, rc::Rc};
|
||||
|
||||
use indicatif::HumanBytes;
|
||||
use owo_colors::Style;
|
||||
|
||||
use crate::multibar::MultiBar;
|
||||
|
||||
pub enum StyledStringInner<'a> {
|
||||
Cow(Cow<'a, str>),
|
||||
Styled(Box<StyledString<'a>>),
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for StyledStringInner<'a> {
|
||||
fn from(value: Cow<'a, str>) -> Self {
|
||||
StyledStringInner::Cow(value)
|
||||
}
|
||||
}
|
||||
impl<'a> From<StyledString<'a>> for StyledStringInner<'a> {
|
||||
fn from(value: StyledString<'a>) -> Self {
|
||||
StyledStringInner::Styled(Box::new(value))
|
||||
}
|
||||
}
|
||||
impl<'a> StyledStringInner<'a> {
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
StyledStringInner::Cow(cow) => cow.len(),
|
||||
StyledStringInner::Styled(styled) => styled.len(),
|
||||
}
|
||||
}
|
||||
pub fn pad_to_width(self, width: usize) -> StyledStringInner<'a> {
|
||||
let len = self.len();
|
||||
if len == width {
|
||||
self
|
||||
} else if len > width {
|
||||
match self {
|
||||
StyledStringInner::Cow(cow) => StyledStringInner::Cow(Cow::Owned(
|
||||
cow.chars()
|
||||
.take(width.saturating_sub(2))
|
||||
.collect::<String>()
|
||||
+ "..",
|
||||
)),
|
||||
StyledStringInner::Styled(styled) => styled.pad_to_width(width).into(),
|
||||
}
|
||||
} else {
|
||||
match self {
|
||||
StyledStringInner::Cow(cow) => StyledStringInner::Cow(Cow::Owned(format!(
|
||||
"{}{}",
|
||||
cow,
|
||||
" ".repeat(width - len)
|
||||
))),
|
||||
StyledStringInner::Styled(styled) => styled.pad_to_width(width).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyledString<'a> {
|
||||
inner: StyledStringInner<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> StyledString<'a> {
|
||||
pub fn new(inner: impl Into<StyledStringInner<'a>>, style: Style) -> Self {
|
||||
Self {
|
||||
inner: inner.into(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
pub fn pad_to_width(self, width: usize) -> StyledString<'a> {
|
||||
if self.inner.len() == width {
|
||||
self
|
||||
} else {
|
||||
StyledString {
|
||||
inner: self.inner.pad_to_width(width),
|
||||
style: self.style,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for StyledStringInner<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StyledStringInner::Cow(cow) => write!(f, "{}", cow),
|
||||
StyledStringInner::Styled(styled) => write!(f, "{}", styled),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for StyledString<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.style.style(self.inner.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// {prefix} {name} | {work_per_sec} | {download_done_human} | {extract_done_human} [bar]
|
||||
//
|
||||
//
|
||||
const NAME_WIDTH_CUT: usize = 4;
|
||||
const NAME_WIDTH_MIN: usize = 5;
|
||||
const INFO_WIDTH: usize = 12;
|
||||
use crate::{
|
||||
multibar::MultiBar,
|
||||
util::{
|
||||
AStrExt, DOWNLOAD_STYLE, EXTRACT_STYLE, INFO_WIDTH, NAME_WIDTH_CUT, NAME_WIDTH_MIN,
|
||||
pad_string,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn style_bar<'s, const N: usize>(
|
||||
prefix: StyledString,
|
||||
name: StyledString,
|
||||
work_per_sec: StyledString,
|
||||
prefix: &str,
|
||||
name: &str,
|
||||
work_per_sec: &str,
|
||||
download_done: usize,
|
||||
extract_done: usize,
|
||||
bar: MultiBar<'s, N>,
|
||||
|
||||
width: u16,
|
||||
) -> String {
|
||||
let name = name.pad_to_width((width as usize / NAME_WIDTH_CUT).max(NAME_WIDTH_MIN));
|
||||
let name = pad_string(name, (width as usize / NAME_WIDTH_CUT).max(NAME_WIDTH_MIN));
|
||||
let status = format!("{} {}", prefix, name);
|
||||
let remaining_width = width.saturating_sub((name.len() + prefix.len()) as u16 + 6);
|
||||
let remaining_width =
|
||||
width.saturating_sub((name.no_ansi_len() + prefix.no_ansi_len()) as u16 + 6);
|
||||
if remaining_width == 0 {
|
||||
return status;
|
||||
}
|
||||
let possible_chunks = remaining_width.saturating_div(INFO_WIDTH as u16 + 3);
|
||||
let possible_chunks = possible_chunks.min(3);
|
||||
let leftover_width = remaining_width.saturating_sub(possible_chunks * (INFO_WIDTH as u16 + 3));
|
||||
let download_done_human = StyledString::new(
|
||||
Cow::Owned(HumanBytes(download_done as u64).to_string()),
|
||||
DOWNLOAD_STYLE,
|
||||
)
|
||||
.pad_to_width(INFO_WIDTH);
|
||||
let extract_done_human = StyledString::new(
|
||||
Cow::Owned(HumanBytes(extract_done as u64).to_string()),
|
||||
EXTRACT_STYLE,
|
||||
)
|
||||
.pad_to_width(INFO_WIDTH);
|
||||
let work_per_sec = work_per_sec.pad_to_width(INFO_WIDTH);
|
||||
let download_done_human = pad_string(
|
||||
DOWNLOAD_STYLE
|
||||
.style(HumanBytes(download_done as u64).to_string())
|
||||
.to_string(),
|
||||
INFO_WIDTH,
|
||||
);
|
||||
|
||||
let extract_done_human = pad_string(
|
||||
EXTRACT_STYLE
|
||||
.style(HumanBytes(extract_done as u64).to_string())
|
||||
.to_string(),
|
||||
INFO_WIDTH,
|
||||
);
|
||||
let work_per_sec = pad_string(work_per_sec, INFO_WIDTH);
|
||||
let bar = bar.scale(leftover_width as u64).to_string();
|
||||
|
||||
match possible_chunks {
|
||||
@@ -148,104 +54,3 @@ pub fn style_bar<'s, const N: usize>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn format(width: u16) {
|
||||
// let name = pad_string(&name, (width / 4) as usize);
|
||||
// let status = format!("Download {} |", name.purple().bold());
|
||||
// let status_len = (width / 4) as usize + 10;
|
||||
//
|
||||
// if self.download_expected == 0 || self.extract_expected == 0 {
|
||||
// self.bar.set_message(format!("{status}"));
|
||||
// self.bar.tick();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// let total_expected = self.download_expected + self.extract_expected;
|
||||
// let total_done = self.download_done + self.extract_done;
|
||||
//
|
||||
// let dl_percent = ((self.download_done as f64 / self.download_expected as f64) * 100.0) as u64;
|
||||
// let ex_percent = ((self.extract_done as f64 / self.extract_expected as f64) * 100.0) as u64;
|
||||
//
|
||||
// let download_per_sec =
|
||||
// HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64).to_string();
|
||||
// let extract_per_sec =
|
||||
// HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64).to_string();
|
||||
// let download_done_human = HumanBytes(self.download_done).to_string();
|
||||
// let download_done_human = pad_string(&download_done_human, INFO_WIDTH)
|
||||
// .blue()
|
||||
// .bold()
|
||||
// .to_string();
|
||||
// let extract_done_human = HumanBytes(self.extract_done).to_string();
|
||||
// let extract_done_human = pad_string(&extract_done_human, INFO_WIDTH)
|
||||
// .green()
|
||||
// .bold()
|
||||
// .to_string();
|
||||
// let download_per_sec = pad_string(&format!("{download_per_sec}/s"), INFO_WIDTH)
|
||||
// .blue()
|
||||
// .bold()
|
||||
// .to_string();
|
||||
// let extract_per_sec = pad_string(&format!("{extract_per_sec}/s"), INFO_WIDTH)
|
||||
// .green()
|
||||
// .bold()
|
||||
// .to_string();
|
||||
// let work_per_sec = if self.download_done < self.download_expected {
|
||||
// download_per_sec
|
||||
// } else {
|
||||
// extract_per_sec
|
||||
// };
|
||||
//
|
||||
// let display = format!("{work_per_sec} | {download_done_human} | {extract_done_human} ");
|
||||
// let display_length = (INFO_WIDTH * 3) + 9;
|
||||
//
|
||||
// // + 6 to account for final format
|
||||
// let total_length = status_len + display_length + 4;
|
||||
//
|
||||
// let min = dl_percent.min(ex_percent);
|
||||
// let dl = dl_percent.saturating_sub(min);
|
||||
// let len = total_length as u16;
|
||||
// // if width <= len {
|
||||
// // self.bar.set_message(format!(
|
||||
// // "{}: {}/{}",
|
||||
// // name,
|
||||
// // total_done,
|
||||
// // if total_expected == 0 {
|
||||
// // "-".to_string()
|
||||
// // } else {
|
||||
// // total_expected.to_string()
|
||||
// // }
|
||||
// // ));
|
||||
// // self.bar.tick();
|
||||
// // return;
|
||||
// // }
|
||||
// let msg = match width {
|
||||
// 0..60 => {
|
||||
// format!(
|
||||
// "{}: {}/{}",
|
||||
// name,
|
||||
// total_done,
|
||||
// if total_expected == 0 {
|
||||
// "-".to_string()
|
||||
// } else {
|
||||
// total_expected.to_string()
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// _ => {
|
||||
// let bar = MultiBar([
|
||||
// BarSegment::Dynamic(&DONE_CHAR, min),
|
||||
// BarSegment::Dynamic(&DOWNLOAD_CHAR, dl),
|
||||
// BarSegment::Dynamic(" ", 100 - min - dl),
|
||||
// ])
|
||||
// .scale((width.saturating_sub(total_length as u16)) as u64);
|
||||
// let bar = if width > display_length as u16 + status_len as u16 {
|
||||
// format!("[{}]", bar)
|
||||
// } else {
|
||||
// String::new()
|
||||
// };
|
||||
//
|
||||
// format!("{status} {display} {bar}",)
|
||||
// }
|
||||
// };
|
||||
// self.bar.set_message(msg);
|
||||
// self.bar.tick();
|
||||
// }
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
use std::{cell::RefCell, rc::Rc, sync::LazyLock, time::Instant};
|
||||
|
||||
use console::style;
|
||||
use indicatif::{HumanBytes, MultiProgress, ProgressBar};
|
||||
|
||||
use crate::{
|
||||
estimator::Estimator,
|
||||
multibar::{BarSegment, MultiBar},
|
||||
pad_string,
|
||||
};
|
||||
|
||||
static DOWNLOAD_CHAR: LazyLock<String> = LazyLock::new(|| style("#").blue().bold().to_string());
|
||||
static DONE_CHAR: LazyLock<String> = LazyLock::new(|| style("#").green().bold().to_string());
|
||||
|
||||
const INFO_WIDTH: usize = 12;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DownloadBar {
|
||||
pub download_estimator: Rc<RefCell<Estimator>>,
|
||||
pub extract_estimator: Rc<RefCell<Estimator>>,
|
||||
pub bar: ProgressBar,
|
||||
pub name: String,
|
||||
pub download_expected: Rc<RefCell<u64>>,
|
||||
pub download_done: Rc<RefCell<u64>>,
|
||||
pub extract_expected: Rc<RefCell<u64>>,
|
||||
pub extract_done: Rc<RefCell<u64>>,
|
||||
pub started_at: Instant,
|
||||
}
|
||||
|
||||
impl DownloadBar {
|
||||
pub fn new(bar: ProgressBar, name: String, width: u16) -> Self {
|
||||
bar.set_style(indicatif::ProgressStyle::with_template("{msg}").unwrap());
|
||||
let new_self = Self {
|
||||
name,
|
||||
bar,
|
||||
download_estimator: Rc::new(RefCell::new(Estimator::new(Instant::now()))),
|
||||
extract_estimator: Rc::new(RefCell::new(Estimator::new(Instant::now()))),
|
||||
download_expected: Rc::new(RefCell::new(0)),
|
||||
download_done: Rc::new(RefCell::new(0)),
|
||||
extract_expected: Rc::new(RefCell::new(0)),
|
||||
extract_done: Rc::new(RefCell::new(0)),
|
||||
started_at: Instant::now(),
|
||||
};
|
||||
new_self.update(width);
|
||||
new_self
|
||||
}
|
||||
|
||||
pub fn update(&self, width: u16) {
|
||||
let download_done = (*self.download_done.borrow());
|
||||
let download_expected = (*self.download_expected.borrow());
|
||||
let extract_done = *self.extract_done.borrow();
|
||||
let extract_expected = *self.extract_expected.borrow();
|
||||
|
||||
self.download_estimator
|
||||
.borrow_mut()
|
||||
.record(download_done, Instant::now());
|
||||
self.extract_estimator
|
||||
.borrow_mut()
|
||||
.record(extract_done, Instant::now());
|
||||
|
||||
let name = pad_string(&self.name, (width / 4) as usize);
|
||||
let status = format!("Download {} |", style(name).magenta().bold());
|
||||
let status_len = (width / 4) as usize + 10;
|
||||
|
||||
if download_expected == 0 || extract_expected == 0 {
|
||||
let text = pad_string("Pending...", width as usize / 3);
|
||||
self.bar.set_message(format!("{status}"));
|
||||
return;
|
||||
}
|
||||
|
||||
let total_expected = download_expected + extract_expected;
|
||||
let total_done = download_done + extract_done;
|
||||
|
||||
let dl_percent = ((download_done as f64 / download_expected as f64) * 100.0) as u64;
|
||||
let ex_percent = ((extract_done as f64 / extract_expected as f64) * 100.0) as u64;
|
||||
|
||||
let download_per_sec = HumanBytes(
|
||||
self.download_estimator
|
||||
.borrow()
|
||||
.steps_per_second(Instant::now()) as u64,
|
||||
)
|
||||
.to_string();
|
||||
let download_done_human = HumanBytes(download_done).to_string();
|
||||
let download_done_human = style(pad_string(&download_done_human, INFO_WIDTH))
|
||||
.blue()
|
||||
.bold()
|
||||
.to_string();
|
||||
let extract_done_human = HumanBytes(extract_done).to_string();
|
||||
let extract_done_human = style(pad_string(&extract_done_human, INFO_WIDTH))
|
||||
.green()
|
||||
.bold()
|
||||
.to_string();
|
||||
let download_per_sec = style(pad_string(&format!("{download_per_sec}/s"), INFO_WIDTH))
|
||||
.blue()
|
||||
.bold()
|
||||
.to_string();
|
||||
|
||||
let display = format!("{download_per_sec} | {download_done_human} | {extract_done_human} ");
|
||||
let display_length = (INFO_WIDTH * 3) + 9;
|
||||
|
||||
// + 6 to account for final format
|
||||
let total_length = status_len + display_length + 4;
|
||||
|
||||
let min = dl_percent.min(ex_percent);
|
||||
let dl = dl_percent.saturating_sub(min);
|
||||
let bar = MultiBar([
|
||||
BarSegment::Dynamic(&DONE_CHAR, min),
|
||||
BarSegment::Dynamic(&DOWNLOAD_CHAR, dl),
|
||||
BarSegment::Dynamic(" ", 100 - min - dl),
|
||||
])
|
||||
.scale((width - total_length as u16) as u64);
|
||||
|
||||
let msg = match width {
|
||||
0..50 => {
|
||||
format!(
|
||||
"{}: {}/{}",
|
||||
self.name,
|
||||
total_done,
|
||||
if total_expected == 0 {
|
||||
"-".to_string()
|
||||
} else {
|
||||
total_expected.to_string()
|
||||
}
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
format!("{status} {display} [{bar}]",)
|
||||
}
|
||||
};
|
||||
self.bar.set_message(msg);
|
||||
self.bar.tick();
|
||||
}
|
||||
}
|
||||
|
||||
fn str_len(s: &str) -> usize {
|
||||
s.chars()
|
||||
.filter(|x| x.is_alphanumeric() || x.is_whitespace())
|
||||
.count()
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
use std::{borrow::Cow, sync::LazyLock, time::Instant};
|
||||
use std::{borrow::Cow, time::Instant};
|
||||
|
||||
use indicatif::{HumanBytes, ProgressBar};
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
|
||||
use crate::{
|
||||
action::{Action, BuildStepId, ResultFields, StartFields},
|
||||
download_display::{DOWNLOAD_STYLE, EXTRACT_STYLE, StyledString, style_bar},
|
||||
download_display::style_bar,
|
||||
estimator::Estimator,
|
||||
handlers::{Handler, fetch::FetchHandler},
|
||||
handlers::Handler,
|
||||
multibar::{BarSegment, MultiBar},
|
||||
nix_path, pad_string,
|
||||
nix_path,
|
||||
util::{DONE_CHAR, DOWNLOAD_CHAR, DOWNLOAD_STYLE, EXTRACT_STYLE},
|
||||
};
|
||||
|
||||
static DOWNLOAD_CHAR: LazyLock<String> = LazyLock::new(|| "-".blue().bold().to_string());
|
||||
static DONE_CHAR: LazyLock<String> = LazyLock::new(|| "#".green().bold().to_string());
|
||||
const INFO_WIDTH: usize = 13;
|
||||
|
||||
pub struct SubstituteHandler;
|
||||
|
||||
impl Handler for SubstituteHandler {
|
||||
@@ -26,7 +23,7 @@ impl Handler for SubstituteHandler {
|
||||
) -> color_eyre::Result<bool> {
|
||||
match action {
|
||||
Action::Start {
|
||||
start_type: StartFields::Substitute { source, target },
|
||||
start_type: StartFields::Substitute { .. },
|
||||
id,
|
||||
..
|
||||
} => {
|
||||
@@ -109,32 +106,27 @@ impl DownloadHandler {
|
||||
};
|
||||
let name = nix_path::extract_package_name_string(&self.path)
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let name = pad_string(&name, (width / 4) as usize);
|
||||
let status = format!("Download {} |", name.purple().bold());
|
||||
let status_len = (width / 4) as usize + 10;
|
||||
|
||||
if self.download_expected == 0 || self.extract_expected == 0 {
|
||||
bar.set_message(format!("{status}"));
|
||||
bar.set_message(format!("Downloading {name}"));
|
||||
bar.tick();
|
||||
return;
|
||||
}
|
||||
|
||||
let work_per_sec = if self.download_done < self.download_expected {
|
||||
StyledString::new(
|
||||
Cow::Owned(format!(
|
||||
DOWNLOAD_STYLE
|
||||
.style(format!(
|
||||
"{}",
|
||||
HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64)
|
||||
)),
|
||||
DOWNLOAD_STYLE,
|
||||
)
|
||||
))
|
||||
.to_string()
|
||||
} else {
|
||||
StyledString::new(
|
||||
Cow::Owned(format!(
|
||||
EXTRACT_STYLE
|
||||
.style(format!(
|
||||
"{}",
|
||||
HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64)
|
||||
)),
|
||||
EXTRACT_STYLE,
|
||||
)
|
||||
))
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let dl_percent =
|
||||
@@ -148,9 +140,9 @@ impl DownloadHandler {
|
||||
BarSegment::Dynamic(" ", 100 - min - dl),
|
||||
]);
|
||||
let msg = style_bar(
|
||||
StyledString::new(Cow::Owned("Downloading".to_string()), Style::new()),
|
||||
StyledString::new(Cow::Owned(name), Style::new().purple()),
|
||||
work_per_sec,
|
||||
"Downloading",
|
||||
&(name.purple().to_string()),
|
||||
&work_per_sec,
|
||||
self.download_done as usize,
|
||||
self.extract_done as usize,
|
||||
mbar,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::borrow::Cow;
|
||||
use indicatif::ProgressBar;
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
use crate::{action::Action, handlers::Handler, nix_path, pad_string};
|
||||
use crate::{action::Action, handlers::Handler, nix_path, util::pad_string};
|
||||
|
||||
pub struct FetchHandler {
|
||||
id: u64,
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use std::{borrow::Cow, collections::HashMap, sync::LazyLock, time::Instant};
|
||||
use std::{borrow::Cow, collections::HashMap, time::Instant};
|
||||
|
||||
use indicatif::{HumanBytes, ProgressBar};
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
|
||||
use crate::{
|
||||
action::{Action, ActionType, BuildStepId, ResultFields, StartFields},
|
||||
download_display::{DOWNLOAD_STYLE, EXTRACT_STYLE, StyledString, style_bar},
|
||||
download_display::style_bar,
|
||||
estimator::Estimator,
|
||||
handlers::{Handler, fetch::FetchHandler},
|
||||
handlers::Handler,
|
||||
multibar::{BarSegment, MultiBar},
|
||||
nix_path, pad_string,
|
||||
util::{DONE_CHAR, DOWNLOAD_CHAR, DOWNLOAD_STYLE, EXTRACT_STYLE, INPROGRESS_CHAR},
|
||||
};
|
||||
|
||||
static DOWNLOAD_CHAR: LazyLock<String> = LazyLock::new(|| "-".blue().bold().to_string());
|
||||
static DONE_CHAR: LazyLock<String> = LazyLock::new(|| "#".green().bold().to_string());
|
||||
static INPROGRESS_CHAR: LazyLock<String> = LazyLock::new(|| "-".purple().bold().to_string());
|
||||
|
||||
pub struct CopyPathsHandler;
|
||||
|
||||
impl Handler for CopyPathsHandler {
|
||||
@@ -30,7 +26,6 @@ impl Handler for CopyPathsHandler {
|
||||
id,
|
||||
..
|
||||
} => {
|
||||
state.println(format!("CopyPaths start"))?;
|
||||
let progress = state.add_pb(ProgressBar::new(1));
|
||||
|
||||
progress.set_style(indicatif::ProgressStyle::with_template("{msg}").unwrap());
|
||||
@@ -103,11 +98,15 @@ impl SubstitutionStatusHandler {
|
||||
}
|
||||
let dl_percent = ((self.get_done() as f64 / self.max_transfer as f64) * 100.0) as u64;
|
||||
let ex_percent = ((self.get_unpacked() as f64 / self.max_copy as f64) * 100.0) as u64;
|
||||
let expected = (((self.get_running() + self.get_running_copy()) as f64
|
||||
/ (self.max_transfer + self.max_copy) as f64)
|
||||
* 100.0) as u64;
|
||||
|
||||
let dl_expected_percent =
|
||||
((self.get_running() as f64 / self.max_transfer as f64) * 100.0) as u64;
|
||||
let ex_expected_percent =
|
||||
((self.get_running_copy() as f64 / self.max_copy as f64) * 100.0) as u64;
|
||||
|
||||
let min = dl_percent.min(ex_percent);
|
||||
let dl = dl_percent.saturating_sub(min);
|
||||
let expected = dl_expected_percent.max(ex_expected_percent);
|
||||
let exp = expected.saturating_sub(min + dl);
|
||||
let mbar = MultiBar([
|
||||
BarSegment::Dynamic(&DONE_CHAR, min),
|
||||
@@ -117,32 +116,24 @@ impl SubstitutionStatusHandler {
|
||||
]);
|
||||
|
||||
let work_per_sec = if self.get_done() < self.max_transfer {
|
||||
StyledString::new(
|
||||
Cow::Owned(format!(
|
||||
DOWNLOAD_STYLE
|
||||
.style(format!(
|
||||
"{}",
|
||||
HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64)
|
||||
)),
|
||||
DOWNLOAD_STYLE,
|
||||
)
|
||||
))
|
||||
.to_string()
|
||||
} else {
|
||||
StyledString::new(
|
||||
Cow::Owned(format!(
|
||||
EXTRACT_STYLE
|
||||
.style(format!(
|
||||
"{}",
|
||||
HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64)
|
||||
)),
|
||||
EXTRACT_STYLE,
|
||||
)
|
||||
))
|
||||
.to_string()
|
||||
};
|
||||
let msg = style_bar(
|
||||
StyledString::new(Cow::Owned("Downloading".to_string()), Style::new()),
|
||||
StyledString::new(
|
||||
Cow::Owned(format!(
|
||||
"{}/{} {min} {dl} {expected}",
|
||||
self.state_self[0], self.state_self[1]
|
||||
)),
|
||||
Style::new().purple(),
|
||||
),
|
||||
work_per_sec,
|
||||
"Downloading",
|
||||
&(format!("{}/{}", self.state_self[0].purple(), self.state_self[1])),
|
||||
&work_per_sec,
|
||||
self.get_done() as usize,
|
||||
self.get_unpacked() as usize,
|
||||
mbar,
|
||||
@@ -161,7 +152,7 @@ impl Handler for SubstitutionStatusHandler {
|
||||
) -> color_eyre::Result<bool> {
|
||||
match action {
|
||||
Action::Start {
|
||||
start_type: StartFields::CopyPath { path, .. },
|
||||
start_type: StartFields::CopyPath { .. },
|
||||
id,
|
||||
..
|
||||
} => {
|
||||
@@ -178,24 +169,24 @@ impl Handler for SubstitutionStatusHandler {
|
||||
}
|
||||
|
||||
Action::Result {
|
||||
id,
|
||||
fields:
|
||||
ResultFields::SetExpected {
|
||||
action: ActionType::FileTransfer,
|
||||
expected,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
self.max_transfer = *expected;
|
||||
self.draw_bar(state.term_width);
|
||||
Ok(true)
|
||||
}
|
||||
Action::Result {
|
||||
id,
|
||||
fields:
|
||||
ResultFields::SetExpected {
|
||||
action: ActionType::CopyPath,
|
||||
expected,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
self.max_copy = *expected;
|
||||
self.draw_bar(state.term_width);
|
||||
|
||||
172
src/main.rs
172
src/main.rs
@@ -1,30 +1,21 @@
|
||||
use std::sync::LazyLock;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use console::Term;
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
pub mod action;
|
||||
pub mod action_raw;
|
||||
pub mod download_display;
|
||||
pub mod download_pb;
|
||||
pub mod estimator;
|
||||
pub mod handlers;
|
||||
pub mod multibar;
|
||||
pub mod nix_path;
|
||||
pub mod state;
|
||||
pub mod state_manager;
|
||||
|
||||
pub fn pad_string(s: &str, width: usize) -> String {
|
||||
if s.len() >= width {
|
||||
s.to_string()
|
||||
} else {
|
||||
let mut padded = s.to_string();
|
||||
padded.push_str(&" ".repeat(width - s.len()));
|
||||
padded
|
||||
}
|
||||
}
|
||||
pub mod util;
|
||||
|
||||
fn main() -> Result<(), color_eyre::Report> {
|
||||
color_eyre::install().unwrap();
|
||||
@@ -32,168 +23,11 @@ fn main() -> Result<(), color_eyre::Report> {
|
||||
let lines = std::fs::read_to_string("build.log")?;
|
||||
let mut state = State::new(Term::stderr().size().1);
|
||||
|
||||
// let lines = lines.lines().take(0).collect::<Vec<_>>().join("\n");
|
||||
// let term = Term::stdout();
|
||||
// let mut state = State {
|
||||
// multi_progress: MultiProgress::new(),
|
||||
// manager: StateManager::new(),
|
||||
// separator: None,
|
||||
// width: term.size().1,
|
||||
// current_width: term.size().1,
|
||||
// term,
|
||||
// };
|
||||
|
||||
for line in lines.lines() {
|
||||
let line = line.strip_prefix("@nix ").unwrap_or(line);
|
||||
sleep(Duration::from_millis(1));
|
||||
let action = action::Action::parse(line)?;
|
||||
state.handle(&action)?;
|
||||
// match action {
|
||||
// action::Action::Msg { level, msg } => {
|
||||
// state.println(format!("MSG (level {level}): {msg}"))?;
|
||||
// }
|
||||
// action::Action::Start {
|
||||
// start_type,
|
||||
// id,
|
||||
// level: _,
|
||||
// parent,
|
||||
// text: _,
|
||||
// } => {
|
||||
// // state.progress.println(format!("START {start_type:?}"));
|
||||
// if let Some(parent) = state.manager.get(*parent) {
|
||||
// let mut child = parent.clone();
|
||||
// child.merge(&mut state, start_type);
|
||||
// child.tick(&mut state);
|
||||
// state.manager.insert_parent(*id, child);
|
||||
// };
|
||||
// // match start_type {
|
||||
// // StartFields::Substitute { source, target } => {
|
||||
// // state
|
||||
// // .manager
|
||||
// // .insert_parent(*id, BuildState::new(Some(source.to_string()), None));
|
||||
// //
|
||||
// // let build_state = state.manager.get_mut(*id).unwrap();
|
||||
// // build_state.state = BuildEnumState::Substituting;
|
||||
// // }
|
||||
// // StartFields::CopyPath {
|
||||
// // path,
|
||||
// // origin,
|
||||
// // destination,
|
||||
// // } => {
|
||||
// // state.manager.add_child(*id, *parent);
|
||||
// // }
|
||||
// // StartFields::QueryPathInfo { path, source } => {
|
||||
// // state.progress.println(format!(
|
||||
// // "START QueryPathInfo (id: {}, parent: {}): path={}",
|
||||
// // id, parent, path
|
||||
// // ))?;
|
||||
// // }
|
||||
// // StartFields::FileTransfer { target } => {
|
||||
// // // state.progress.println(format!(
|
||||
// // // "START FileTransfer (id: {}, parent: {}): target={}",
|
||||
// // // id, parent, target
|
||||
// // // ))?;
|
||||
// // if let Some(parent) = state.manager.get(*parent) {
|
||||
// // state
|
||||
// // .manager
|
||||
// // .insert_parent(*id, BuildState::new(parent.path.clone(), None));
|
||||
// // let build_state = state.manager.get_mut(*id).unwrap();
|
||||
// // build_state.state = BuildEnumState::Downloading;
|
||||
// // };
|
||||
// //
|
||||
// // // state.manager.add_child(*id, *parent);
|
||||
// // // Add child ID mapping to parent
|
||||
// // }
|
||||
// // _ => {}
|
||||
// // };
|
||||
// }
|
||||
// action::Action::Stop { id } => {
|
||||
// state.manager.remove(*id);
|
||||
// // Stop will only return Some when the last reference is stopped
|
||||
// // if let Some(build_state) = state.manager.stop(*id) {
|
||||
// // if let Some(pb) = &build_state.progress_bar {
|
||||
// // pb.finish_and_clear();
|
||||
// // state.progress.remove(pb);
|
||||
// // }
|
||||
// // state.progress.println(format!(
|
||||
// // "Completed: {}",
|
||||
// // build_state.path.as_deref().unwrap_or("unknown")
|
||||
// // ))?;
|
||||
// // }
|
||||
// }
|
||||
// action::Action::Result { id, fields } => match fields {
|
||||
// action::ResultFields::FetchStatus(status) => {
|
||||
// state.println(format!(
|
||||
// "RESULT FetchStatus (id: {}): status={}",
|
||||
// id, status
|
||||
// ))?;
|
||||
// }
|
||||
// action::ResultFields::Progress {
|
||||
// done,
|
||||
// expected,
|
||||
// running,
|
||||
// failed,
|
||||
// } => {
|
||||
// if expected == 0 {
|
||||
// continue;
|
||||
// };
|
||||
// if let Some(mut child) = state.manager.take(*id) {
|
||||
// child.progress(&mut state, done, expected);
|
||||
// state.manager.insert_parent(*id, child);
|
||||
// }
|
||||
// // sleep(Duration::from_millis(1));
|
||||
// // if let Some(build_state) = state.manager.get_mut(*id)
|
||||
// // && expected > 0
|
||||
// // {
|
||||
// // let percentage = if expected == 0 {
|
||||
// // 0
|
||||
// // } else {
|
||||
// // (done * 100 / expected) as u64
|
||||
// // };
|
||||
// // match &build_state.progress_bar {
|
||||
// // Some(pb) => {
|
||||
// // if percentage > pb.position() {
|
||||
// // pb.set_position(percentage);
|
||||
// // };
|
||||
// // }
|
||||
// // None => {
|
||||
// // state.progress.println(format!(
|
||||
// // "Creating progress bar for id {} (done: {}, expected: {})",
|
||||
// // id, done, expected
|
||||
// // ))?;
|
||||
// // let n = match build_state.state {
|
||||
// // BuildEnumState::Downloading => "Downloading",
|
||||
// // BuildEnumState::Substituting => "Substituting",
|
||||
// // _ => "Processing",
|
||||
// // };
|
||||
// // let name =
|
||||
// // nix_path::extract_full_name(build_state.path.as_ref().unwrap());
|
||||
// // let pb = state.progress.add(
|
||||
// // ProgressBar::new(100)
|
||||
// // .with_style(
|
||||
// // ProgressStyle::default_bar()
|
||||
// // .template("{msg} [{bar:40.cyan/blue}] {pos:>3}%")
|
||||
// // .unwrap(),
|
||||
// // )
|
||||
// // .with_message(format!(
|
||||
// // "{n} {}",
|
||||
// // name.as_deref().unwrap_or("unknown")
|
||||
// // )),
|
||||
// // );
|
||||
// // pb.set_position(percentage);
|
||||
// // build_state.progress_bar = Some(pb);
|
||||
// // }
|
||||
// // };
|
||||
// // } else {
|
||||
// // // state.progress.println(format!(
|
||||
// // // "RESULT Progress (id: {}): done={}, expected={}, running={}, failed={}",
|
||||
// // // id, done, expected, running, failed
|
||||
// // // ))?;
|
||||
// // };
|
||||
// }
|
||||
// _ => {}
|
||||
// },
|
||||
// }
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -16,7 +16,7 @@ impl<'s> BarSegment<'s> {
|
||||
|
||||
fn length(&self) -> u64 {
|
||||
match self {
|
||||
BarSegment::Dynamic(c, len) => *len,
|
||||
BarSegment::Dynamic(_c, len) => *len,
|
||||
BarSegment::Static(c) => c.len() as u64,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn extract_package_name(path: &str) -> Option<Vec<&str>> {
|
||||
|
||||
let name_parts: Vec<&str> = parts
|
||||
.iter()
|
||||
.take_while(|part| !part.chars().next().map_or(false, |c| c.is_ascii_digit()))
|
||||
.take_while(|part| !part.chars().next().is_some_and(|c| c.is_ascii_digit()))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{io, marker::PhantomData, rc::Rc};
|
||||
use std::{io, rc::Rc};
|
||||
|
||||
use console::style;
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
use console::{Term, style};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||
use std::{borrow::Cow, collections::HashMap, io, time::Instant};
|
||||
|
||||
use crate::{action::StartFields, download_pb::DownloadBar, nix_path};
|
||||
|
||||
pub struct State<'a> {
|
||||
pub multi_progress: MultiProgress,
|
||||
pub manager: StateManager<'a>,
|
||||
pub separator: Option<ProgressBar>,
|
||||
pub term: Term,
|
||||
pub width: u16,
|
||||
pub current_width: u16,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
pub fn add_pb(&mut self, pb: ProgressBar) -> ProgressBar {
|
||||
let separator = self.separator.get_or_insert_with(|| {
|
||||
let separator = ProgressBar::new_spinner()
|
||||
.with_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template(&style("··{wide_msg:<}").dim().to_string())
|
||||
.expect("invalid template"),
|
||||
)
|
||||
.with_message("·".repeat(512))
|
||||
.with_finish(ProgressFinish::AndClear);
|
||||
|
||||
let separator = self.multi_progress.insert(0, separator);
|
||||
separator.set_length(0);
|
||||
separator
|
||||
});
|
||||
|
||||
self.multi_progress.insert_after(separator, pb)
|
||||
// self.progress.add(pb)
|
||||
}
|
||||
pub fn add_pb_before(&mut self, before: &ProgressBar, pb: ProgressBar) -> ProgressBar {
|
||||
self.multi_progress.insert_before(before, pb)
|
||||
}
|
||||
pub fn add_pb_after(&mut self, after: &ProgressBar, pb: ProgressBar) -> ProgressBar {
|
||||
self.multi_progress.insert_after(after, pb)
|
||||
}
|
||||
pub fn println<I: AsRef<str>>(&self, msg: I) -> io::Result<()> {
|
||||
self.multi_progress.println(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BuildEnumState<'a> {
|
||||
Init,
|
||||
Unknown,
|
||||
Realise,
|
||||
Builds,
|
||||
CopyPaths,
|
||||
Substitute {
|
||||
source: Cow<'a, str>,
|
||||
target: Cow<'a, str>,
|
||||
},
|
||||
SubstituteCopy {
|
||||
path: Cow<'a, str>,
|
||||
bar: DownloadBar,
|
||||
},
|
||||
SubstituteFetch {
|
||||
path: Cow<'a, str>,
|
||||
bar: DownloadBar,
|
||||
},
|
||||
Query {
|
||||
path: Cow<'a, str>,
|
||||
source: Cow<'a, str>,
|
||||
},
|
||||
QueryFetch {
|
||||
target: Cow<'a, str>,
|
||||
},
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildState<'a> {
|
||||
pub path: Option<String>,
|
||||
pub progress_bar: Option<ProgressBar>,
|
||||
pub state: BuildEnumState<'a>,
|
||||
}
|
||||
impl<'a> BuildState<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
progress_bar: None,
|
||||
state: BuildEnumState::Init,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BuildState<'a> {
|
||||
pub fn merge(&mut self, state: &mut State, start_type: StartFields<'a>) {
|
||||
let state = match (&self.state, start_type.clone()) {
|
||||
(BuildEnumState::Init, StartFields::QueryPathInfo { path, source }) => {
|
||||
BuildEnumState::Query { path, source }
|
||||
}
|
||||
(BuildEnumState::Query { path, source }, StartFields::FileTransfer { target }) => {
|
||||
BuildEnumState::QueryFetch { target }
|
||||
}
|
||||
(BuildEnumState::Init, StartFields::Realise) => BuildEnumState::Realise,
|
||||
(BuildEnumState::Init, StartFields::Builds) => BuildEnumState::Builds,
|
||||
(BuildEnumState::Init, StartFields::CopyPaths) => BuildEnumState::CopyPaths,
|
||||
(BuildEnumState::Init, StartFields::Substitute { source, target }) => {
|
||||
BuildEnumState::Substitute { source, target }
|
||||
}
|
||||
(BuildEnumState::Substitute { .. }, StartFields::CopyPath { path, .. }) => {
|
||||
let name =
|
||||
nix_path::extract_package_name_string(&path).unwrap_or("unknown".to_string());
|
||||
let bar = state.add_pb(ProgressBar::new(100));
|
||||
let bar = DownloadBar::new(bar, name, state.width);
|
||||
BuildEnumState::SubstituteCopy { path, bar }
|
||||
}
|
||||
(BuildEnumState::SubstituteCopy { path, bar }, StartFields::FileTransfer { .. }) => {
|
||||
BuildEnumState::SubstituteFetch {
|
||||
path: path.clone(),
|
||||
bar: bar.clone(),
|
||||
}
|
||||
}
|
||||
(_, StartFields::Unknown) => BuildEnumState::Unknown,
|
||||
_ => unimplemented!(
|
||||
"Unsupported state transition from {:?} with {:?}",
|
||||
self.state,
|
||||
start_type
|
||||
),
|
||||
};
|
||||
self.state = state;
|
||||
}
|
||||
pub fn tick(&mut self, state: &mut State<'a>) {
|
||||
match &self.state {
|
||||
BuildEnumState::QueryFetch { target } => {
|
||||
let pb = state.add_pb(ProgressBar::new_spinner());
|
||||
pb.set_message(format!("Fetching info for {}", target));
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
self.progress_bar = Some(pb);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
pub fn progress(&mut self, state: &mut State<'a>, done: u64, expected: u64) {
|
||||
match &self.state {
|
||||
BuildEnumState::SubstituteFetch { bar, .. } => {
|
||||
bar.download_expected.replace(expected);
|
||||
bar.download_done.replace(done);
|
||||
bar.update(state.width);
|
||||
}
|
||||
BuildEnumState::SubstituteCopy { path, bar } => {
|
||||
bar.extract_expected.replace(expected);
|
||||
bar.extract_done.replace(done);
|
||||
bar.update(state.width);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StateManager<'a> {
|
||||
states: HashMap<u64, BuildState<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> StateManager<'a> {
|
||||
pub fn new() -> Self {
|
||||
let mut states = HashMap::new();
|
||||
states.insert(0, BuildState::new());
|
||||
Self { states }
|
||||
}
|
||||
pub fn get(&self, id: u64) -> Option<&BuildState<'a>> {
|
||||
self.states.get(&id)
|
||||
}
|
||||
pub fn get_mut(&mut self, id: u64) -> Option<&mut BuildState<'a>> {
|
||||
self.states.get_mut(&id)
|
||||
}
|
||||
pub fn insert_parent(&mut self, id: u64, state: BuildState<'a>) {
|
||||
self.states.insert(id, state);
|
||||
}
|
||||
pub fn get_or_insert(&mut self, id: u64) -> &mut BuildState<'a> {
|
||||
self.states.entry(id).or_insert_with(BuildState::new)
|
||||
}
|
||||
pub fn take(&mut self, id: u64) -> Option<BuildState<'a>> {
|
||||
self.states.remove(&id)
|
||||
}
|
||||
pub fn remove(&mut self, id: u64) {
|
||||
if let Some(state) = self.states.get(&id) {
|
||||
if let Some(pb) = &state.progress_bar {
|
||||
pb.finish_and_clear();
|
||||
self.states.remove(&id);
|
||||
}
|
||||
}
|
||||
self.states.remove(&id);
|
||||
}
|
||||
}
|
||||
67
src/util.rs
Normal file
67
src/util.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::{borrow::Cow, sync::LazyLock};
|
||||
|
||||
use console::strip_ansi_codes;
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
|
||||
pub const DOWNLOAD_STYLE: Style = Style::new().blue().bold();
|
||||
pub const EXTRACT_STYLE: Style = Style::new().green().bold();
|
||||
pub const NAME_WIDTH_CUT: usize = 4;
|
||||
pub const NAME_WIDTH_MIN: usize = 5;
|
||||
pub const INFO_WIDTH: usize = 12;
|
||||
pub static DOWNLOAD_CHAR: LazyLock<String> = LazyLock::new(|| "-".blue().bold().to_string());
|
||||
pub static DONE_CHAR: LazyLock<String> = LazyLock::new(|| "=".green().bold().to_string());
|
||||
pub static INPROGRESS_CHAR: LazyLock<String> = LazyLock::new(|| "-".purple().bold().to_string());
|
||||
|
||||
pub trait StrExt<'a>: Into<Cow<'a, str>> {
|
||||
fn pad_string(self, width: usize) -> String {
|
||||
let s = self.into();
|
||||
let len = s.no_ansi_len();
|
||||
if len == width {
|
||||
s.to_string()
|
||||
} else if len > width {
|
||||
s.chars().take(width.saturating_sub(2)).collect::<String>() + ".."
|
||||
} else {
|
||||
let mut padded = s.to_string();
|
||||
padded.push_str(&" ".repeat(width - len));
|
||||
padded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AStrExt {
|
||||
fn no_ansi_len(&self) -> usize;
|
||||
}
|
||||
impl AStrExt for String {
|
||||
fn no_ansi_len(&self) -> usize {
|
||||
let s = strip_ansi_codes(self);
|
||||
s.len()
|
||||
}
|
||||
}
|
||||
impl AStrExt for &str {
|
||||
fn no_ansi_len(&self) -> usize {
|
||||
let s = strip_ansi_codes(self);
|
||||
s.len()
|
||||
}
|
||||
}
|
||||
impl AStrExt for str {
|
||||
fn no_ansi_len(&self) -> usize {
|
||||
let s = strip_ansi_codes(self);
|
||||
s.len()
|
||||
}
|
||||
}
|
||||
impl<'a, T: Into<Cow<'a, str>>> StrExt<'a> for T {}
|
||||
|
||||
pub fn pad_string<'a, S: Into<Cow<'a, str>>>(s: S, width: usize) -> String {
|
||||
let s = s.into();
|
||||
let trimmed = strip_ansi_codes(&s);
|
||||
let len = trimmed.len();
|
||||
if len == width {
|
||||
s.to_string()
|
||||
} else if len > width {
|
||||
s.chars().take(width.saturating_sub(2)).collect::<String>() + ".."
|
||||
} else {
|
||||
let mut padded = s.to_string();
|
||||
padded.push_str(&" ".repeat(width - len));
|
||||
padded
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user