use anyhow::Result;
use clap::ValueEnum;
use console::style;
use crossterm::{
cursor::{MoveToColumn, MoveToPreviousLine},
terminal::{self, Clear, ClearType},
ExecutableCommand,
};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::{stdout, Write};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, ValueEnum, Eq, PartialEq)]
pub enum LogLevel {
Cargo,
Errors,
Verbose,
}
#[derive(Debug, Clone, Copy, ValueEnum, Eq, PartialEq)]
pub enum LogStatus {
Started,
Stopped,
FinishedWithSuccess,
FinishedWithError,
}
#[derive(Debug, Clone)]
pub struct NixMessage {
pub action: String,
pub message_type: Option<u64>,
pub content: String,
pub level: Option<u64>,
pub file: Option<String>,
}
pub type Id = u64;
pub struct LogOne {
pub colored: bool,
log_level: LogLevel,
status_line_active: bool,
targets: HashMap<String, u64>,
last_stats: Option<(u64, u64, u64, u64)>,
last_targets_hash: u64,
pub nix_log_buffers: HashMap<Id, Vec<NixMessage>>,
pub nix_log_buffers_state: HashMap<Id, LogStatus>,
pub cargo_log_buffers: HashMap<Id, Vec<String>>,
pub cargo_log_buffers_state: HashMap<Id, LogStatus>,
pub drv_to_id: HashMap<String, u64>,
active: bool,
}
impl Drop for LogOne {
fn drop(&mut self) {
self.shutdown();
}
}
impl LogOne {
pub fn new(colored: bool, log_level: LogLevel) -> Self {
Self {
colored,
log_level,
status_line_active: false,
targets: HashMap::new(),
last_stats: None,
last_targets_hash: 0,
nix_log_buffers: HashMap::new(),
nix_log_buffers_state: HashMap::new(),
cargo_log_buffers: HashMap::new(),
cargo_log_buffers_state: HashMap::new(),
drv_to_id: HashMap::new(),
active: true,
}
}
pub fn shutdown(&mut self) {
if self.active {
self.active = false;
let ids: Vec<u64> = self.nix_log_buffers.keys().cloned().collect();
if self.level() == LogLevel::Verbose {
for id in ids {
self.print_log_buffer_by_id(id);
}
}
}
}
pub fn level(&self) -> LogLevel {
self.log_level
}
fn snapshot_targets(&self) -> Vec<(String, u64)> {
let mut snapshot: Vec<(String, u64)> = self
.targets
.iter()
.filter(|(_, &count)| count > 0)
.map(|(name, &count)| (name.clone(), count))
.collect();
snapshot.sort_by(|a, b| a.0.cmp(&b.0)); snapshot
}
pub fn target_add(&mut self, create_name: String) -> Result<()> {
self.targets
.entry(create_name)
.and_modify(|c| *c += 1)
.or_insert(1);
Ok(())
}
pub fn target_remove(&mut self, create_name: String) -> Result<()> {
if let Some(count) = self.targets.get_mut(&create_name) {
*count -= 1;
if *count == 0 {
self.targets.remove(&create_name);
}
}
Ok(())
}
pub fn clear_status(&mut self) {
if self.status_line_active {
let mut stdout = stdout();
stdout.execute(MoveToPreviousLine(1)).unwrap();
stdout.execute(MoveToColumn(0)).unwrap();
stdout.execute(Clear(ClearType::CurrentLine)).unwrap();
self.status_line_active = false;
}
}
pub fn draw_status(&mut self) {
if let Some((done, expected, running, failed)) = self.last_stats {
let base_status = format!(
"[ {} Done | {} Expected | {} Running | {} Failed ]",
done, expected, running, failed
);
let targets_snapshot = self.snapshot_targets();
let display_line = self.format_status_with_targets(&base_status, &targets_snapshot);
if self.colored {
let styled_base = format!(
"[ {} Done | {} Expected | {} Running | {} Failed ]",
style(done).green(),
style(expected).green(),
style(running).yellow(),
style(failed).red()
);
let targets_part = self.get_targets_display(&base_status, &targets_snapshot);
if targets_part.is_empty() {
println!("{}", styled_base);
} else {
println!("{} {}", styled_base, targets_part);
}
} else {
println!("{}", display_line);
}
self.status_line_active = true;
stdout().flush().unwrap();
}
}
pub fn update_stats(&mut self, done: u64, expected: u64, running: u64, failed: u64) {
let current_stats = (done, expected, running, failed);
let targets_snapshot = self.snapshot_targets();
let current_targets_hash = self.calculate_targets_hash(&targets_snapshot);
let stats_changed = self.last_stats.as_ref() != Some(¤t_stats);
let targets_changed = self.last_targets_hash != current_targets_hash;
if !stats_changed && !targets_changed {
return; }
if self.status_line_active {
let mut stdout = stdout();
stdout.execute(MoveToPreviousLine(1)).unwrap();
stdout.execute(MoveToColumn(0)).unwrap();
stdout.execute(Clear(ClearType::CurrentLine)).unwrap();
}
let base_status = format!(
"[ {} Done | {} Expected | {} Running | {} Failed ]",
done, expected, running, failed
);
let display_line = self.format_status_with_targets(&base_status, &targets_snapshot);
if self.colored {
let styled_base = format!(
"[ {} Done | {} Expected | {} Running | {} Failed ]",
style(done).green(),
style(expected).green(),
style(running).yellow(),
style(failed).red()
);
let targets_part = self.get_targets_display(&base_status, &targets_snapshot);
if targets_part.is_empty() {
println!("{}", styled_base);
} else {
println!("{} {}", styled_base, targets_part);
}
} else {
println!("{}", display_line);
}
self.status_line_active = true;
self.last_stats = Some(current_stats);
self.last_targets_hash = current_targets_hash;
stdout().flush().unwrap();
}
pub fn print_log_buffer_by_drv(&mut self, drv: String) {
let id: Id = match self.drv_to_id.get(&drv) {
Some(id) => *id,
None => return,
};
self.print_log_buffer(id, drv)
}
pub fn print_log_buffer_by_id(&mut self, id: Id) {
let drv: String = match self.drv_to_id.iter().find(|(_, &v)| v == id) {
Some((k, _)) => k.clone(),
None => return,
};
self.print_log_buffer(id, drv)
}
fn print_log_buffer(&mut self, id: Id, drv: String) {
let buffer = self.nix_log_buffers.remove(&id);
if let Some(buffer) = buffer {
self.clear_status();
println!("Build log for '{}':", drv);
for message in buffer {
match message.message_type {
Some(101) => {
if self.colored {
println!(" {}", style(&message.content).dim());
} else {
println!(" {}", message.content);
}
}
Some(104) => {
if self.colored {
println!(" {}", style(&message.content).cyan());
} else {
println!(" {}", message.content);
}
}
_ => {
println!(" {}", message.content);
}
}
}
println!(); stdout().flush().unwrap();
self.draw_status();
}
}
pub fn print_message(&mut self, level: u64, msg: &str, file: Option<&str>) {
if self.status_line_active {
let mut stdout = stdout();
stdout.execute(MoveToPreviousLine(1)).unwrap();
stdout.execute(MoveToColumn(0)).unwrap();
stdout.execute(Clear(ClearType::CurrentLine)).unwrap();
self.status_line_active = false;
}
let formatted_msg = if let Some(file_path) = file {
format!("{}: {}", file_path, msg)
} else {
format!("{}", msg)
};
if self.colored {
let styled_msg = match level {
0 => style(formatted_msg).red(),
1 => style(formatted_msg).yellow(),
2 => style(formatted_msg).blue(),
3 => style(formatted_msg).green(),
_ => style(formatted_msg).dim(),
};
println!("{}", styled_msg);
} else {
println!("{}", formatted_msg);
}
stdout().flush().unwrap();
self.draw_status();
}
pub fn clear_status_line(&mut self) {
if self.status_line_active {
let mut stdout = stdout();
stdout.execute(MoveToPreviousLine(1)).unwrap();
stdout.execute(MoveToColumn(0)).unwrap();
stdout.execute(Clear(ClearType::CurrentLine)).unwrap();
self.status_line_active = false;
stdout.flush().unwrap();
}
}
fn format_status_with_targets(
&self,
base_status: &str,
targets_snapshot: &[(String, u64)],
) -> String {
let targets_display = self.get_targets_display(base_status, targets_snapshot);
if targets_display.is_empty() {
base_status.to_string()
} else {
format!("{} {}", base_status, targets_display)
}
}
fn get_targets_display(&self, base_status: &str, targets_snapshot: &[(String, u64)]) -> String {
if targets_snapshot.is_empty() {
return String::new();
}
let terminal_width = terminal::size().map(|(w, _)| w as usize).unwrap_or(80);
let base_len = base_status.len();
let space_for_targets = if terminal_width > base_len + 1 {
terminal_width - base_len - 1 } else {
return String::new(); };
let mut result = String::new();
let mut first = true;
for (name, count) in targets_snapshot {
let target_display = if *count > 1 {
format!("{} (×{})", name, count)
} else {
name.clone()
};
let addition = if first {
target_display
} else {
format!(", {}", target_display)
};
if result.len() + addition.len() <= space_for_targets {
result.push_str(&addition);
first = false;
} else {
break;
}
}
result
}
fn calculate_targets_hash(&self, targets_snapshot: &[(String, u64)]) -> u64 {
let mut hasher = DefaultHasher::new();
targets_snapshot.hash(&mut hasher);
hasher.finish()
}
}