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:
2025-09-24 03:06:22 -07:00
parent 886484919d
commit a7638c411a
+255 -64
View File
@@ -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!(
"Simulation time: {:>7.2} s | Step: {:>4.2} s | Organs: {}",
state.sim_time,
state.tick_seconds,
MONITORED_ORGANS.len()
);
let vitals = VITAL_SIGNS
.iter()
.map(|v| format!("{:?}", v))
.collect::<Vec<_>>()
.join(", ");
println!("Tracked vital signs: {vitals}");
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!(
"{}",
stat_line(
"Simulation time",
accent(format!("{:.2} s", state.sim_time))
)
);
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!();
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()
})
}