feat(examples): add colorized dashboard to demo
Revamp the console monitor UI in examples/demo_app.rs with ANSI-colored sections and improved layout for readability. - Add color palette, dashboard width, and styling helpers - Introduce banner, section, and stat line builders - Colorize prompt, status lines, and organ snapshots - Display validity tags for measurements and styled warnings/errors - Restructure output into Simulation, Circulation, Heart, Organ Snapshots, and Status sections - Cache TTY color detection via OnceLock and respect NO_COLOR No API changes; improvements are limited to the example app.
This commit is contained in:
+251
-60
@@ -4,6 +4,7 @@ use medicallib_rust::{
|
||||
};
|
||||
use std::fmt::Write as _;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const EXTRA_ORGANS: [OrganType; 11] = [
|
||||
OrganType::Brain,
|
||||
@@ -44,6 +45,19 @@ const VITAL_SIGNS: [VitalSign; 6] = [
|
||||
VitalSign::SpO2,
|
||||
];
|
||||
|
||||
const DASHBOARD_WIDTH: usize = 88;
|
||||
|
||||
const COLOR_RESET: &str = "\x1b[0m";
|
||||
const COLOR_TITLE: &str = "\x1b[38;5;111m";
|
||||
const COLOR_SECTION: &str = "\x1b[38;5;81m";
|
||||
const COLOR_LABEL: &str = "\x1b[38;5;245m";
|
||||
const COLOR_MUTED: &str = "\x1b[38;5;240m";
|
||||
const COLOR_ACCENT: &str = "\x1b[38;5;153m";
|
||||
const COLOR_SUCCESS: &str = "\x1b[38;5;114m";
|
||||
const COLOR_WARNING: &str = "\x1b[38;5;221m";
|
||||
const COLOR_ERROR: &str = "\x1b[38;5;203m";
|
||||
const COLOR_PROMPT: &str = "\x1b[38;5;117m";
|
||||
|
||||
const HELP_TEXT: &str = r#"
|
||||
Available commands:
|
||||
help Show this help text
|
||||
@@ -117,7 +131,7 @@ fn main() -> MedicalResult<()> {
|
||||
|
||||
loop {
|
||||
render_dashboard(&state, &status);
|
||||
print!("monitor> ");
|
||||
print!("{}", prompt_text());
|
||||
io::stdout().flush().expect("flush stdout");
|
||||
|
||||
input.clear();
|
||||
@@ -385,92 +399,164 @@ fn handle_set_command<'a>(
|
||||
|
||||
fn render_dashboard(state: &MonitorState, status: &str) {
|
||||
clear_screen();
|
||||
println!("=== MedicalLib Console Monitor ===");
|
||||
|
||||
println!("{}", banner_line("MedicalLib Console Monitor"));
|
||||
|
||||
let overview_line = [
|
||||
accent(format!("t = {:.2} s", state.sim_time)),
|
||||
muted(" | "),
|
||||
accent(format!("dt = {:.2} s", state.tick_seconds)),
|
||||
muted(" | "),
|
||||
accent(format!("Organs {}", MONITORED_ORGANS.len())),
|
||||
muted(" | "),
|
||||
accent(format!("Vitals {}", VITAL_SIGNS.len())),
|
||||
]
|
||||
.join("");
|
||||
println!(" {overview_line}");
|
||||
println!();
|
||||
|
||||
println!("{}", section_line("Simulation"));
|
||||
println!(
|
||||
"Simulation time: {:>7.2} s | Step: {:>4.2} s | Organs: {}",
|
||||
state.sim_time,
|
||||
state.tick_seconds,
|
||||
MONITORED_ORGANS.len()
|
||||
"{}",
|
||||
stat_line(
|
||||
"Simulation time",
|
||||
accent(format!("{:.2} s", state.sim_time))
|
||||
)
|
||||
);
|
||||
let vitals = VITAL_SIGNS
|
||||
println!(
|
||||
"{}",
|
||||
stat_line("Step size", accent(format!("{:.2} s", state.tick_seconds)))
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
stat_line(
|
||||
"Tracked vitals",
|
||||
accent(
|
||||
VITAL_SIGNS
|
||||
.iter()
|
||||
.map(|v| format!("{:?}", v))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
println!("Tracked vital signs: {vitals}");
|
||||
|
||||
.join(", ")
|
||||
)
|
||||
)
|
||||
);
|
||||
println!();
|
||||
|
||||
println!("{}", section_line("Circulation"));
|
||||
let bp = state.patient.blood_pressure;
|
||||
println!(
|
||||
"Blood pressure : {} (valid: {})",
|
||||
bp,
|
||||
yes_no(bp.validate())
|
||||
"{}",
|
||||
stat_line(
|
||||
"Blood pressure",
|
||||
format!("{} {}", accent(format!("{bp}")), validity_tag(bp.validate()))
|
||||
)
|
||||
);
|
||||
|
||||
let blood = &state.patient.blood;
|
||||
println!(
|
||||
"Blood chemistry : Hgb={:.1} g/dL | Hct={:.1}% | SpO2={:.0}% | Glucose={:.1} mg/dL (valid: {})",
|
||||
blood.hemoglobin_g_dl,
|
||||
blood.hematocrit_pct,
|
||||
blood.spo2_pct,
|
||||
blood.glucose_mg_dl,
|
||||
yes_no(blood.validate())
|
||||
);
|
||||
let mut chemistry_parts = vec![
|
||||
accent(format!("Hgb {:.1} g/dL", blood.hemoglobin_g_dl)),
|
||||
muted(" | "),
|
||||
accent(format!("Hct {:.1}%", blood.hematocrit_pct)),
|
||||
muted(" | "),
|
||||
accent(format!("SpO2 {:.0}%", blood.spo2_pct)),
|
||||
muted(" | "),
|
||||
accent(format!("Glucose {:.1} mg/dL", blood.glucose_mg_dl)),
|
||||
muted(" "),
|
||||
];
|
||||
chemistry_parts.push(validity_tag(blood.validate()));
|
||||
let chemistry = chemistry_parts.join("");
|
||||
println!("{}", stat_line("Blood chemistry", chemistry));
|
||||
|
||||
let (weight, height) = state.bmi_inputs;
|
||||
let bmi_line = match bmi_measurement(weight, height) {
|
||||
Ok(measurement) => {
|
||||
let class = classify_bmi(measurement.value);
|
||||
format!(
|
||||
"Tracked BMI : {:.1} kg / {:.2} m -> {:.2} {} ({:?})",
|
||||
weight, height, measurement.value, measurement.unit, class
|
||||
)
|
||||
vec![
|
||||
accent(format!("{:.1} kg", weight)),
|
||||
muted(" / "),
|
||||
accent(format!("{:.2} m", height)),
|
||||
muted(" -> "),
|
||||
accent(format!("{:.2} {}", measurement.value, measurement.unit)),
|
||||
muted(" ("),
|
||||
accent(format!("{:?}", class)),
|
||||
muted(")"),
|
||||
]
|
||||
.join("")
|
||||
}
|
||||
Err(err) => format!(
|
||||
"Tracked BMI : inputs {:.1} kg / {:.2} m -> error ({err})",
|
||||
weight, height
|
||||
Err(err) => colorize(
|
||||
COLOR_ERROR,
|
||||
format!("{:.1} kg / {:.2} m -> error ({err})", weight, height),
|
||||
),
|
||||
};
|
||||
println!("{bmi_line}");
|
||||
|
||||
println!("{}", stat_line("BMI inputs", bmi_line));
|
||||
println!();
|
||||
|
||||
println!("{}", section_line("Heart"));
|
||||
if let Some(heart) = state.patient.find_organ_typed::<Heart>() {
|
||||
let rate = Measurement::new(heart.heart_rate_bpm, "bpm");
|
||||
println!(
|
||||
"Heart : HR={:.0} {} | Rhythm={:?} | CO={:.1} L/min | Tone={:+.2} | SVR={:.1}",
|
||||
rate.value,
|
||||
rate.unit,
|
||||
heart.rhythm_state,
|
||||
heart.cardiac_output_l_min,
|
||||
heart.autonomic_tone,
|
||||
heart.systemic_vascular_resistance
|
||||
);
|
||||
println!(
|
||||
" Arrhythmia forced={} | EF={:.0}% | MAP~{:.0} mmHg",
|
||||
yes_no(heart.arrhythmia),
|
||||
heart.ejection_fraction * 100.0,
|
||||
mean_arterial_pressure(bp)
|
||||
);
|
||||
let heart_rate = vec![
|
||||
accent(format!("{:.0} bpm", heart.heart_rate_bpm)),
|
||||
muted(" | rhythm "),
|
||||
accent(format!("{:?}", heart.rhythm_state)),
|
||||
]
|
||||
.join("");
|
||||
println!("{}", stat_line("Heart rate", heart_rate));
|
||||
|
||||
let cardiac_output = vec![
|
||||
accent(format!("{:.1} L/min", heart.cardiac_output_l_min)),
|
||||
muted(" | tone "),
|
||||
accent(format!("{:+.2}", heart.autonomic_tone)),
|
||||
muted(" | SVR "),
|
||||
accent(format!("{:.1}", heart.systemic_vascular_resistance)),
|
||||
]
|
||||
.join("");
|
||||
println!("{}", stat_line("Output", cardiac_output));
|
||||
|
||||
let arrhythmia_tag = if heart.arrhythmia {
|
||||
colorize(COLOR_WARNING, "forced arrhythmia".to_string())
|
||||
} else {
|
||||
colorize(COLOR_SUCCESS, "intrinsic rhythm".to_string())
|
||||
};
|
||||
let arrhythmia_line = vec![
|
||||
arrhythmia_tag,
|
||||
muted(" | EF "),
|
||||
accent(format!("{:.0}%", heart.ejection_fraction * 100.0)),
|
||||
muted(" | MAP ~"),
|
||||
accent(format!("{:.0} mmHg", mean_arterial_pressure(bp))),
|
||||
]
|
||||
.join("");
|
||||
println!("{}", stat_line("Arrhythmia", arrhythmia_line));
|
||||
} else {
|
||||
println!("Heart : <not attached>");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Organ snapshots:");
|
||||
for organ in MONITORED_ORGANS {
|
||||
println!(
|
||||
" {:<12} {}",
|
||||
organ_label(organ),
|
||||
organ_snapshot(&state.patient, organ)
|
||||
"{}",
|
||||
stat_line("Heart", colorize(COLOR_WARNING, "<not attached>".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Status:");
|
||||
for line in status.lines() {
|
||||
println!(" {line}");
|
||||
|
||||
println!("{}", section_line("Organ Snapshots"));
|
||||
for organ in MONITORED_ORGANS {
|
||||
println!("{}", organ_line(&state.patient, organ));
|
||||
}
|
||||
println!();
|
||||
println!("(press Enter without typing to step once; type 'help' for commands)");
|
||||
|
||||
println!("{}", section_line("Status"));
|
||||
let mut printed_status = false;
|
||||
for line in status.lines() {
|
||||
let styled = status_line(line);
|
||||
if styled.is_empty() {
|
||||
continue;
|
||||
}
|
||||
println!(" {}", styled);
|
||||
printed_status = true;
|
||||
}
|
||||
if !printed_status {
|
||||
println!(" {}", muted("No recent actions."));
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
muted("Press Enter to advance once or type 'help' for commands.")
|
||||
);
|
||||
}
|
||||
|
||||
fn organ_snapshot(patient: &Patient, kind: OrganType) -> String {
|
||||
@@ -534,3 +620,108 @@ fn clear_screen() {
|
||||
print!("\x1b[2J\x1b[H");
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn prompt_text() -> String {
|
||||
if colors_enabled() {
|
||||
format!("{} ", colorize(COLOR_PROMPT, "monitor>"))
|
||||
} else {
|
||||
String::from("monitor> ")
|
||||
}
|
||||
}
|
||||
|
||||
fn banner_line(title: &str) -> String {
|
||||
let width = DASHBOARD_WIDTH.max(title.len() + 4);
|
||||
colorize(
|
||||
COLOR_TITLE,
|
||||
format!("{:=^width$}", format!(" {title} "), width = width),
|
||||
)
|
||||
}
|
||||
|
||||
fn section_line(title: &str) -> String {
|
||||
colorize(
|
||||
COLOR_SECTION,
|
||||
format!("{:-^width$}", format!(" {title} "), width = DASHBOARD_WIDTH),
|
||||
)
|
||||
}
|
||||
|
||||
fn stat_line(label: &str, value: impl Into<String>) -> String {
|
||||
let label_cell = colorize(COLOR_LABEL, format!("{label:<18}:"));
|
||||
format!(" {label_cell} {}", value.into())
|
||||
}
|
||||
|
||||
fn accent(text: impl Into<String>) -> String {
|
||||
colorize(COLOR_ACCENT, text)
|
||||
}
|
||||
|
||||
fn muted(text: impl Into<String>) -> String {
|
||||
colorize(COLOR_MUTED, text)
|
||||
}
|
||||
|
||||
fn validity_tag(ok: bool) -> String {
|
||||
if ok {
|
||||
colorize(COLOR_SUCCESS, "[OK]".to_string())
|
||||
} else {
|
||||
colorize(COLOR_ERROR, "[CHECK]".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn organ_line(patient: &Patient, kind: OrganType) -> String {
|
||||
let label = colorize(COLOR_ACCENT, format!("{:<12}:", organ_label(kind)));
|
||||
let summary = organ_snapshot(patient, kind);
|
||||
let styled_summary = style_snapshot(&summary);
|
||||
format!(" {label} {styled_summary}")
|
||||
}
|
||||
|
||||
fn style_snapshot(text: &str) -> String {
|
||||
let lowered = text.to_ascii_lowercase();
|
||||
if lowered.contains("error") {
|
||||
colorize(COLOR_ERROR, text.to_string())
|
||||
} else if lowered.starts_with("n/a") {
|
||||
colorize(COLOR_WARNING, text.to_string())
|
||||
} else if lowered.contains("stable") || lowered.contains("normal") {
|
||||
colorize(COLOR_SUCCESS, text.to_string())
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn status_line(line: &str) -> String {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let lowered = trimmed.to_ascii_lowercase();
|
||||
if lowered.contains("error") {
|
||||
colorize(COLOR_ERROR, trimmed.to_string())
|
||||
} else if lowered.contains("warning") || lowered.contains("caution") {
|
||||
colorize(COLOR_WARNING, trimmed.to_string())
|
||||
} else if lowered.starts_with("advanced")
|
||||
|| lowered.starts_with("ran")
|
||||
|| lowered.starts_with("set ")
|
||||
|| lowered.starts_with("blood")
|
||||
|| lowered.starts_with("heart")
|
||||
|| lowered.starts_with("patient")
|
||||
|| lowered.starts_with("tracked")
|
||||
{
|
||||
colorize(COLOR_SUCCESS, trimmed.to_string())
|
||||
} else {
|
||||
accent(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn colorize(code: &str, text: impl Into<String>) -> String {
|
||||
let text = text.into();
|
||||
if colors_enabled() {
|
||||
format!("{code}{text}{COLOR_RESET}")
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
fn colors_enabled() -> bool {
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
*ENABLED.get_or_init(|| {
|
||||
use std::io::IsTerminal;
|
||||
std::env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user