diff --git a/examples/demo_app.rs b/examples/demo_app.rs index 27e0d02..fc974f3 100644 --- a/examples/demo_app.rs +++ b/examples/demo_app.rs @@ -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::>() - .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::>() + .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::() { - 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 : "); - } - - 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, "".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 { + let label_cell = colorize(COLOR_LABEL, format!("{label:<18}:")); + format!(" {label_cell} {}", value.into()) +} + +fn accent(text: impl Into) -> String { + colorize(COLOR_ACCENT, text) +} + +fn muted(text: impl Into) -> 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 { + let text = text.into(); + if colors_enabled() { + format!("{code}{text}{COLOR_RESET}") + } else { + text + } +} + +fn colors_enabled() -> bool { + static ENABLED: OnceLock = OnceLock::new(); + *ENABLED.get_or_init(|| { + use std::io::IsTerminal; + std::env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal() + }) +}