Compare commits

...

5 Commits

Author SHA1 Message Date
3b0dcdf76d fix bar formatting 2025-11-15 21:36:13 +04:00
2bc4fdcc79 more reworking bar + new log format 2025-11-15 09:20:01 +00:00
f7fe3b0bb0 more reworking 2025-11-15 08:26:07 +00:00
0b68d803e8 cargo clippy 2025-11-15 07:54:29 +00:00
01743c3c75 cleanup 2025-11-15 07:51:32 +00:00
23 changed files with 270700 additions and 825 deletions

180376
1763196716.log Normal file

File diff suppressed because it is too large Load Diff

86558
1763196781.log Normal file

File diff suppressed because it is too large Load Diff

1534
1763197645.log Normal file

File diff suppressed because it is too large Load Diff

1997
1763197700.log Normal file

File diff suppressed because it is too large Load Diff

4
Cargo.lock generated
View File

@@ -114,9 +114,9 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]] [[package]]
name = "indicatif" name = "indicatif"
version = "0.18.2" version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
dependencies = [ dependencies = [
"console", "console",
"portable-atomic", "portable-atomic",

View File

@@ -1,5 +1,13 @@
cargo-features = ["codegen-backend"] cargo-features = ["codegen-backend"]
[[bin]]
name = "record"
path = "src/bin/record.rs"
[[bin]]
name = "main"
path = "src/bin/main.rs"
[package] [package]
name = "nix-output" name = "nix-output"
version = "0.1.0" version = "0.1.0"
@@ -15,7 +23,7 @@ opt-level = 3
[dependencies] [dependencies]
color-eyre = "0.6.5" color-eyre = "0.6.5"
console = "0.16.1" console = "0.16.1"
indicatif = "0.18.2" indicatif = "0.18.3"
owo-colors = "4.2.3" owo-colors = "4.2.3"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.145", features = ["raw_value"] } serde_json = { version = "1.0.145", features = ["raw_value"] }

View File

@@ -66,7 +66,7 @@
cargo-nextest cargo-nextest
cargo-valgrind cargo-valgrind
pkg-config pkg-config
nix.dev # nix.dev
boost.dev boost.dev
]; ];
}; };

View File

@@ -15,7 +15,9 @@ use crate::action_raw::RawAction;
#[derive(Clone, Copy, Debug, Deserialize_repr)] #[derive(Clone, Copy, Debug, Deserialize_repr)]
#[repr(u8)] #[repr(u8)]
#[derive(Eq, PartialEq)] #[derive(Eq, PartialEq)]
#[derive(Default)]
pub enum ActionType { pub enum ActionType {
#[default]
Unknown = 0, Unknown = 0,
CopyPath = 100, CopyPath = 100,
FileTransfer = 101, FileTransfer = 101,
@@ -32,11 +34,6 @@ pub enum ActionType {
FetchTree = 112, FetchTree = 112,
} }
impl Default for ActionType {
fn default() -> Self {
Self::Unknown
}
}
// --- // ---
// --- BuildStepId // --- BuildStepId

33
src/bin/main.rs Normal file
View File

@@ -0,0 +1,33 @@
use std::thread::sleep;
use std::time::{Duration, Instant};
use color_eyre::eyre::ContextCompat;
use console::Term;
use nix_output::action;
use nix_output::state::State;
fn main() -> Result<(), color_eyre::Report> {
color_eyre::install().unwrap();
let mut args = std::env::args().skip(1);
let first = args.next().context("Failed to get first arg")?;
let lines = std::fs::read_to_string(first)?;
let mut state = State::new(Term::stderr().size().1);
let start = Instant::now();
for line in lines.lines() {
let mut time = line.split("~");
let instant = time.next().unwrap(); // impossible to fail
let instant: u128 = instant.parse()?;
let elapsed = start.elapsed().as_millis();
if instant > elapsed {
sleep(Duration::from_millis(instant.saturating_sub(elapsed) as u64));
}
let line = time.collect::<String>();
let line = line.strip_prefix("@nix ").unwrap_or(&line);
let action = action::Action::parse(line)?;
state.handle(&action)?;
}
Ok(())
}

27
src/bin/record.rs Normal file
View File

@@ -0,0 +1,27 @@
use core::time;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
time::{Instant, SystemTime},
};
fn main() -> Result<(), color_eyre::Report> {
color_eyre::install().unwrap();
let stderr = std::io::stdin();
let mut lines = BufReader::new(stderr).lines();
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let mut file = File::options()
.create_new(true)
.write(true)
.truncate(true)
.open(format!("{}.log", now.as_secs()))?;
let start = Instant::now();
while let Some(Ok(next)) = lines.next() {
let line = format!("{}~{next}\n", start.elapsed().as_millis());
file.write_all(line.as_bytes())?;
}
file.flush()?;
Ok(())
}

View File

@@ -1,144 +1,53 @@
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 indicatif::HumanBytes;
use owo_colors::Style;
use crate::multibar::MultiBar; use crate::{
multibar::MultiBar,
pub enum StyledStringInner<'a> { util::{
Cow(Cow<'a, str>), AStrExt, DOWNLOAD_STYLE, EXTRACT_STYLE, INFO_WIDTH, NAME_WIDTH_CUT, NAME_WIDTH_MIN,
Styled(Box<StyledString<'a>>), pad_string,
} },
};
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;
pub fn style_bar<'s, const N: usize>( pub fn style_bar<'s, const N: usize>(
prefix: StyledString, prefix: &str,
name: StyledString, name: &str,
work_per_sec: StyledString, work_per_sec: &str,
download_done: usize, download_done: usize,
extract_done: usize, extract_done: usize,
bar: MultiBar<'s, N>, bar: MultiBar<'s, N>,
width: u16, width: u16,
) -> String { ) -> 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 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 { if remaining_width == 0 {
return status; return status;
} }
let remaining_width = remaining_width.saturating_sub((INFO_WIDTH as u16 + 3) * 2);
let possible_chunks = remaining_width.saturating_div(INFO_WIDTH as u16 + 3); let possible_chunks = remaining_width.saturating_div(INFO_WIDTH as u16 + 3);
let possible_chunks = possible_chunks.min(3); let possible_chunks = possible_chunks.min(3);
let leftover_width = remaining_width.saturating_sub(possible_chunks * (INFO_WIDTH as u16 + 3)); let leftover_width = ((INFO_WIDTH as u16 + 3) * 2)
let download_done_human = StyledString::new( + remaining_width.saturating_sub(possible_chunks * (INFO_WIDTH as u16 + 3));
Cow::Owned(HumanBytes(download_done as u64).to_string()), let download_done_human = pad_string(
DOWNLOAD_STYLE, DOWNLOAD_STYLE
) .style(HumanBytes(download_done as u64).to_string())
.pad_to_width(INFO_WIDTH); .to_string(),
let extract_done_human = StyledString::new( INFO_WIDTH,
Cow::Owned(HumanBytes(extract_done as u64).to_string()), );
EXTRACT_STYLE,
) let extract_done_human = pad_string(
.pad_to_width(INFO_WIDTH); EXTRACT_STYLE
let work_per_sec = work_per_sec.pad_to_width(INFO_WIDTH); .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(); let bar = bar.scale(leftover_width as u64).to_string();
match possible_chunks { match possible_chunks {
0 => format!("{status} | {download_done_human}"), 0 => format!("{status} | [{bar}]"),
1 => format!("{status} | {download_done_human} | [{bar}]"), 1 => format!("{status} | {download_done_human} | [{bar}]"),
2 => format!("{status} | {work_per_sec} | {download_done_human} | [{bar}]"), 2 => format!("{status} | {work_per_sec} | {download_done_human} | [{bar}]"),
_ => { _ => {
@@ -148,104 +57,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();
// }

View File

@@ -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()
}

View File

@@ -1,21 +1,18 @@
use std::{borrow::Cow, sync::LazyLock, time::Instant}; use std::{borrow::Cow, time::Instant};
use indicatif::{HumanBytes, ProgressBar}; use indicatif::{HumanBytes, ProgressBar};
use owo_colors::{OwoColorize, Style}; use owo_colors::{OwoColorize, Style};
use crate::{ use crate::{
action::{Action, BuildStepId, ResultFields, StartFields}, action::{Action, BuildStepId, ResultFields, StartFields},
download_display::{DOWNLOAD_STYLE, EXTRACT_STYLE, StyledString, style_bar}, download_display::style_bar,
estimator::Estimator, estimator::Estimator,
handlers::{Handler, fetch::FetchHandler}, handlers::Handler,
multibar::{BarSegment, MultiBar}, 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; pub struct SubstituteHandler;
impl Handler for SubstituteHandler { impl Handler for SubstituteHandler {
@@ -26,7 +23,7 @@ impl Handler for SubstituteHandler {
) -> color_eyre::Result<bool> { ) -> color_eyre::Result<bool> {
match action { match action {
Action::Start { Action::Start {
start_type: StartFields::Substitute { source, target }, start_type: StartFields::Substitute { .. },
id, id,
.. ..
} => { } => {
@@ -109,32 +106,27 @@ impl DownloadHandler {
}; };
let name = nix_path::extract_package_name_string(&self.path) let name = nix_path::extract_package_name_string(&self.path)
.unwrap_or_else(|| "unknown".to_string()); .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 { if self.download_expected == 0 || self.extract_expected == 0 {
bar.set_message(format!("{status}")); bar.set_message(format!("Downloading {name}"));
bar.tick(); bar.tick();
return; return;
} }
let work_per_sec = if self.download_done < self.download_expected { let work_per_sec = if self.download_done < self.download_expected {
StyledString::new( DOWNLOAD_STYLE
Cow::Owned(format!( .style(format!(
"{}", "{}",
HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64) HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64)
)), ))
DOWNLOAD_STYLE, .to_string()
)
} else { } else {
StyledString::new( EXTRACT_STYLE
Cow::Owned(format!( .style(format!(
"{}", "{}",
HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64) HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64)
)), ))
EXTRACT_STYLE, .to_string()
)
}; };
let dl_percent = let dl_percent =
@@ -148,9 +140,9 @@ impl DownloadHandler {
BarSegment::Dynamic(" ", 100 - min - dl), BarSegment::Dynamic(" ", 100 - min - dl),
]); ]);
let msg = style_bar( let msg = style_bar(
StyledString::new(Cow::Owned("Downloading".to_string()), Style::new()), "Downloading",
StyledString::new(Cow::Owned(name), Style::new().purple()), &(name.purple().to_string()),
work_per_sec, &work_per_sec,
self.download_done as usize, self.download_done as usize,
self.extract_done as usize, self.extract_done as usize,
mbar, mbar,

View File

@@ -3,7 +3,7 @@ use std::borrow::Cow;
use indicatif::ProgressBar; use indicatif::ProgressBar;
use owo_colors::OwoColorize; 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 { pub struct FetchHandler {
id: u64, id: u64,

View File

@@ -14,6 +14,12 @@ impl Handler for GlobalHandler {
action: &crate::action::Action, action: &crate::action::Action,
) -> color_eyre::Result<bool> { ) -> color_eyre::Result<bool> {
match action { match action {
Action::Msg { level, msg } => {
if *level > 4 {
state.println(format!("{level}: {msg}"))?;
}
Ok(true)
}
Action::Start { Action::Start {
start_type: StartFields::QueryPathInfo { path, source }, start_type: StartFields::QueryPathInfo { path, source },
id, id,

View File

@@ -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 indicatif::{HumanBytes, ProgressBar};
use owo_colors::{OwoColorize, Style}; use owo_colors::{OwoColorize, Style};
use crate::{ use crate::{
action::{Action, ActionType, BuildStepId, ResultFields, StartFields}, action::{Action, ActionType, BuildStepId, ResultFields, StartFields},
download_display::{DOWNLOAD_STYLE, EXTRACT_STYLE, StyledString, style_bar}, download_display::style_bar,
estimator::Estimator, estimator::Estimator,
handlers::{Handler, fetch::FetchHandler}, handlers::Handler,
multibar::{BarSegment, MultiBar}, 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; pub struct CopyPathsHandler;
impl Handler for CopyPathsHandler { impl Handler for CopyPathsHandler {
@@ -30,7 +26,6 @@ impl Handler for CopyPathsHandler {
id, id,
.. ..
} => { } => {
state.println(format!("CopyPaths start"))?;
let progress = state.add_pb(ProgressBar::new(1)); let progress = state.add_pb(ProgressBar::new(1));
progress.set_style(indicatif::ProgressStyle::with_template("{msg}").unwrap()); 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 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 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) let dl_expected_percent =
* 100.0) as u64; ((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 min = dl_percent.min(ex_percent);
let dl = dl_percent.saturating_sub(min); let dl = dl_percent.saturating_sub(min);
let expected = dl_expected_percent.max(ex_expected_percent);
let exp = expected.saturating_sub(min + dl); let exp = expected.saturating_sub(min + dl);
let mbar = MultiBar([ let mbar = MultiBar([
BarSegment::Dynamic(&DONE_CHAR, min), BarSegment::Dynamic(&DONE_CHAR, min),
@@ -117,32 +116,24 @@ impl SubstitutionStatusHandler {
]); ]);
let work_per_sec = if self.get_done() < self.max_transfer { let work_per_sec = if self.get_done() < self.max_transfer {
StyledString::new( DOWNLOAD_STYLE
Cow::Owned(format!( .style(format!(
"{}", "{}",
HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64) HumanBytes(self.download_estimator.steps_per_second(Instant::now()) as u64)
)), ))
DOWNLOAD_STYLE, .to_string()
)
} else { } else {
StyledString::new( EXTRACT_STYLE
Cow::Owned(format!( .style(format!(
"{}", "{}",
HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64) HumanBytes(self.extract_estimator.steps_per_second(Instant::now()) as u64)
)), ))
EXTRACT_STYLE, .to_string()
)
}; };
let msg = style_bar( let msg = style_bar(
StyledString::new(Cow::Owned("Downloading".to_string()), Style::new()), "Downloading",
StyledString::new( &(format!("{}/{}", self.state_self[0].purple(), self.state_self[1])),
Cow::Owned(format!( &work_per_sec,
"{}/{} {min} {dl} {expected}",
self.state_self[0], self.state_self[1]
)),
Style::new().purple(),
),
work_per_sec,
self.get_done() as usize, self.get_done() as usize,
self.get_unpacked() as usize, self.get_unpacked() as usize,
mbar, mbar,
@@ -161,7 +152,7 @@ impl Handler for SubstitutionStatusHandler {
) -> color_eyre::Result<bool> { ) -> color_eyre::Result<bool> {
match action { match action {
Action::Start { Action::Start {
start_type: StartFields::CopyPath { path, .. }, start_type: StartFields::CopyPath { .. },
id, id,
.. ..
} => { } => {
@@ -178,24 +169,24 @@ impl Handler for SubstitutionStatusHandler {
} }
Action::Result { Action::Result {
id,
fields: fields:
ResultFields::SetExpected { ResultFields::SetExpected {
action: ActionType::FileTransfer, action: ActionType::FileTransfer,
expected, expected,
}, },
..
} => { } => {
self.max_transfer = *expected; self.max_transfer = *expected;
self.draw_bar(state.term_width); self.draw_bar(state.term_width);
Ok(true) Ok(true)
} }
Action::Result { Action::Result {
id,
fields: fields:
ResultFields::SetExpected { ResultFields::SetExpected {
action: ActionType::CopyPath, action: ActionType::CopyPath,
expected, expected,
}, },
..
} => { } => {
self.max_copy = *expected; self.max_copy = *expected;
self.draw_bar(state.term_width); self.draw_bar(state.term_width);

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod action;
pub mod action_raw;
pub mod download_display;
pub mod estimator;
pub mod handlers;
pub mod multibar;
pub mod nix_path;
pub mod state;
pub mod util;

View File

@@ -1,200 +0,0 @@
use std::thread::sleep;
use std::time::Duration;
use console::Term;
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
}
}
fn main() -> Result<(), color_eyre::Report> {
color_eyre::install().unwrap();
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(())
}

View File

@@ -16,7 +16,7 @@ impl<'s> BarSegment<'s> {
fn length(&self) -> u64 { fn length(&self) -> u64 {
match self { match self {
BarSegment::Dynamic(c, len) => *len, BarSegment::Dynamic(_c, len) => *len,
BarSegment::Static(c) => c.len() as u64, BarSegment::Static(c) => c.len() as u64,
} }
} }

View File

@@ -16,7 +16,7 @@ pub fn extract_package_name(path: &str) -> Option<Vec<&str>> {
let name_parts: Vec<&str> = parts let name_parts: Vec<&str> = parts
.iter() .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() .copied()
.collect(); .collect();

View File

@@ -1,4 +1,4 @@
use std::{io, marker::PhantomData, rc::Rc}; use std::{io, rc::Rc};
use console::style; use console::style;
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};

View File

@@ -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
View 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
}
}