use std::io::Write;
use owo_colors::{OwoColorize, Stream};
use whyno_core::checks::{CheckReport, CoreLayer, LayerResult, MacLayer};
use whyno_core::fix::{commands, scoring, Fix, FixPlan};
use whyno_core::state::{Probe, SystemState};
use super::layer_name;
use crate::error::OutputError;
pub fn render(
report: &CheckReport,
plan: &FixPlan,
state: &SystemState,
writer: &mut dyn Write,
) -> Result<(), OutputError> {
write_header(state, writer)?;
writeln!(writer)?;
for (layer, result) in &report.core_results {
write_layer_line(layer, result, writer)?;
write_fixes_for_layer(layer, plan, writer)?;
}
for (layer, result) in &report.mac_results {
write_mac_layer_line(*layer, result, writer)?;
}
write_warnings(&plan.warnings, writer)?;
Ok(())
}
fn write_header(state: &SystemState, writer: &mut dyn Write) -> Result<(), OutputError> {
let s = &state.subject;
let groups = format_groups(&s.groups);
let caps_suffix = match s.capabilities {
Probe::Known(bitmask) => format!(", caps={bitmask:#018x}"),
_ => String::new(),
};
let target_path = state.walk.last().map(|c| c.path.display().to_string());
let target = target_path.as_deref().unwrap_or("/");
let op = super::layer_name::operation_name(state.operation);
writeln!(
writer,
" Subject: uid={}, gid={}, groups=[{}]{}",
s.uid, s.gid, groups, caps_suffix
)?;
writeln!(writer, " Operation: {op}")?;
writeln!(writer, " Target: {target}")?;
Ok(())
}
fn format_groups(groups: &[u32]) -> String {
groups
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
fn write_layer_line(
layer: CoreLayer,
result: &LayerResult,
writer: &mut dyn Write,
) -> Result<(), OutputError> {
let name = layer_name::padded(layer);
write_result_line(name, result, writer)
}
fn write_mac_layer_line(
layer: MacLayer,
result: &LayerResult,
writer: &mut dyn Write,
) -> Result<(), OutputError> {
let name = layer_name::padded_mac(layer);
write_result_line(name, result, writer)
}
fn write_result_line(
name: &str,
result: &LayerResult,
writer: &mut dyn Write,
) -> Result<(), OutputError> {
match result {
LayerResult::Pass { detail, warnings } => {
let tag = "[PASS]"
.if_supports_color(Stream::Stdout, |s| s.green())
.to_string();
writeln!(writer, " {tag} {name} -- {detail}")?;
for warning in warnings {
let prefix = "\u{26a0}"
.if_supports_color(Stream::Stdout, |s| s.yellow())
.to_string();
writeln!(writer, " {prefix} {warning}")?;
}
}
LayerResult::Fail { detail, .. } => {
let tag = "[FAIL]"
.if_supports_color(Stream::Stdout, |s| s.red())
.to_string();
writeln!(writer, " {tag} {name} -- {detail}")?;
}
LayerResult::Degraded { reason } => {
let tag = "[SKIP]"
.if_supports_color(Stream::Stdout, |s| s.yellow())
.to_string();
writeln!(writer, " {tag} {name} -- {reason}")?;
}
_ => {
writeln!(writer, " [????] {name} -- unknown result")?;
}
}
Ok(())
}
fn write_fixes_for_layer(
layer: CoreLayer,
plan: &FixPlan,
writer: &mut dyn Write,
) -> Result<(), OutputError> {
let fixes: Vec<&Fix> = plan.fixes.iter().filter(|f| f.layer == layer).collect();
for fix in fixes {
write_fix_line(fix, writer)?;
}
Ok(())
}
fn write_fix_line(fix: &Fix, writer: &mut dyn Write) -> Result<(), OutputError> {
let cmd = commands::render_command(&fix.action);
let warning = if scoring::needs_warning(fix.impact) {
" (!)"
} else {
""
};
writeln!(
writer,
" Fix: {cmd} [impact: {}/6]{warning}",
fix.impact
)?;
Ok(())
}
fn write_warnings(warnings: &[String], writer: &mut dyn Write) -> Result<(), OutputError> {
if warnings.is_empty() {
return Ok(());
}
writeln!(writer)?;
for warning in warnings {
writeln!(writer, " Warning: {warning}")?;
}
Ok(())
}
#[cfg(test)]
#[path = "checklist_tests.rs"]
mod tests;