Files
medicallib_rust/examples/demo_app.rs
T
zack3d 5cf6bbda48 feat(organs): add bloodstream organ and patient coupling
Introduce OrganType::Bloodstream and new organ module, exporting
Bloodstream, PerfusionState, and MetabolicState.

- Patient:
  - initialize_default now attaches a bloodstream model
  - add with_bloodstream() builder and with_organ support
  - update() couples bloodstream with heart, lungs, brain, spinal cord,
    kidneys, liver, pancreas, stomach, intestines, gallbladder,
    esophagus, bladder, and spleen
  - bloodstream ingests hemodynamics, respiratory exchange, renal,
    hepatic, splenic, and nutrient feedback; propagates perfusion and
    metabolic signals back to organs
  - blood metrics (SpO2, hemoglobin, hematocrit, glucose) can be driven
    by bloodstream

- Signals:
  - Lungs: add O2 delivery, CO2 elimination, V/Q ratio
  - Liver: add detox and ammonia clearance
  - Kidneys: add plasma volume, urea excretion, erythropoietin

- FFI:
  - add ML_ORGAN_BLOODSTREAM (Rust and C header) and mapping in
    ml_patient_organ_summary

- Examples/Tests:
  - demo monitors "Bloodstream"
  - add tests for bloodstream coupling and organ discovery
  - lib tests updated to include Bloodstream

Rationale: centralize systemic transport and inter-organ homeostasis for
richer physiology simulation and expose it to C consumers via FFI.
2025-09-28 16:10:23 -07:00

839 lines
27 KiB
Rust

use medicallib_rust::{
bmi_measurement, calculate_bmi, classify_bmi, BloodPressure, EkgLead, Heart, Measurement,
OrganType, Patient, Result as MedicalResult, VitalSign,
};
use std::fmt::Write as _;
use std::io::{self, Write};
use std::sync::OnceLock;
const EXTRA_ORGANS: [OrganType; 11] = [
OrganType::Brain,
OrganType::SpinalCord,
OrganType::Stomach,
OrganType::Liver,
OrganType::Gallbladder,
OrganType::Pancreas,
OrganType::Intestines,
OrganType::Esophagus,
OrganType::Kidneys,
OrganType::Bladder,
OrganType::Spleen,
];
const MONITORED_ORGANS: [OrganType; 14] = [
OrganType::Heart,
OrganType::Bloodstream,
OrganType::Lungs,
OrganType::Brain,
OrganType::SpinalCord,
OrganType::Stomach,
OrganType::Liver,
OrganType::Gallbladder,
OrganType::Pancreas,
OrganType::Intestines,
OrganType::Esophagus,
OrganType::Kidneys,
OrganType::Bladder,
OrganType::Spleen,
];
const VITAL_SIGNS: [VitalSign; 6] = [
VitalSign::HeartRate,
VitalSign::RespiratoryRate,
VitalSign::SystolicBP,
VitalSign::DiastolicBP,
VitalSign::TemperatureC,
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
tick [dt] Advance the simulation by dt seconds (default: configured step)
run <steps> [dt] Run multiple ticks back-to-back
set dt <seconds> Update the default tick size
set arrhythmia <on|off> Force arrhythmic behaviour on the heart
set tone <value> Set heart autonomic tone (-1.0..=1.0)
set svr <value> Set heart systemic vascular resistance (mmHg*min/L)
set ekg <lead...> Configure EKG leads (e.g., set ekg I II V1 V5)
set glucose <mg/dL> Override blood glucose
set spo2 <percent> Override blood SpO2 (0-100)
set bp <systolic> <diastolic> Override brachial blood pressure
set bmi <weight_kg> <height_m> Update the tracked BMI inputs
bmi <weight_kg> <height_m> Compute BMI on the fly without storing it
summary Print the aggregate patient summary string
organs Print one-line summaries for every organ
reset Reset the patient and vitals to defaults
quit | exit | q Leave the monitor
(empty input) Advance once using the configured step size
"#;
struct MonitorState {
patient: Patient,
sim_time: f32,
tick_seconds: f32,
bmi_inputs: (f32, f32),
}
impl MonitorState {
fn new() -> MedicalResult<Self> {
let mut patient = Patient::new("monitor")?.initialize_default().with_lungs();
for organ in EXTRA_ORGANS {
patient = patient.with_organ(organ);
}
Ok(Self {
patient,
sim_time: 0.0,
tick_seconds: 0.5,
bmi_inputs: (82.0, 1.84),
})
}
fn reset(&mut self) -> MedicalResult<()> {
*self = Self::new()?;
Ok(())
}
fn advance(&mut self, dt: f32) {
if dt <= 0.0 {
return;
}
self.patient.update(dt);
self.sim_time += dt;
}
fn heart_mut(&mut self) -> Option<&mut Heart> {
self.patient.find_organ_typed_mut::<Heart>()
}
}
enum CommandOutcome {
Continue(String),
Exit,
}
fn main() -> MedicalResult<()> {
let mut state = MonitorState::new()?;
let mut status = String::from("Type 'help' to list available commands.");
let stdin = io::stdin();
let mut input = String::new();
loop {
render_dashboard(&state, &status);
print!("{}", prompt_text());
io::stdout().flush().expect("flush stdout");
input.clear();
let read = match stdin.read_line(&mut input) {
Ok(n) => n,
Err(err) => {
status = format!("Failed to read input: {err}");
continue;
}
};
if read == 0 {
status = String::from("End of input detected, shutting down.");
render_dashboard(&state, &status);
println!();
break;
}
let trimmed = input.trim();
if trimmed.is_empty() {
let dt = state.tick_seconds;
state.advance(dt);
status = format!("Advanced simulation by {:.2} s using the default step.", dt);
continue;
}
match handle_command(&mut state, trimmed) {
Ok(CommandOutcome::Continue(message)) => {
status = message;
}
Ok(CommandOutcome::Exit) => {
status = String::from("Exiting monitor.");
render_dashboard(&state, &status);
println!();
break;
}
Err(err) => {
status = format!("Error: {err}");
}
}
}
println!("Goodbye!");
Ok(())
}
fn handle_command(state: &mut MonitorState, input: &str) -> Result<CommandOutcome, String> {
let mut parts = input.split_whitespace();
let cmd = parts
.next()
.ok_or_else(|| String::from("expected a command"))?
.to_ascii_lowercase();
match cmd.as_str() {
"help" => Ok(CommandOutcome::Continue(String::from(HELP_TEXT))),
"tick" => {
let dt = match parts.next() {
Some(raw) => parse_f32(raw)?,
None => state.tick_seconds,
};
if dt <= 0.0 {
return Err(String::from("dt must be greater than 0"));
}
state.advance(dt);
Ok(CommandOutcome::Continue(format!(
"Advanced simulation by {:.2} s.",
dt
)))
}
"run" => {
let steps_raw = parts
.next()
.ok_or_else(|| String::from("run expects a number of steps"))?;
let steps: usize = steps_raw
.parse()
.map_err(|_| format!("could not parse steps '{steps_raw}'"))?;
if steps == 0 {
return Err(String::from("steps must be greater than 0"));
}
let dt = match parts.next() {
Some(raw) => parse_f32(raw)?,
None => state.tick_seconds,
};
if dt <= 0.0 {
return Err(String::from("dt must be greater than 0"));
}
for _ in 0..steps {
state.advance(dt);
}
Ok(CommandOutcome::Continue(format!(
"Ran {steps} step(s) at {:.2} s per step.",
dt
)))
}
"set" => handle_set_command(state, parts),
"summary" => Ok(CommandOutcome::Continue(state.patient.patient_summary())),
"organs" => {
let mut details = String::new();
for organ in MONITORED_ORGANS {
let _ = writeln!(
details,
"{} -> {}",
organ_label(organ),
organ_snapshot(&state.patient, organ)
);
}
Ok(CommandOutcome::Continue(details))
}
"reset" => {
state.reset().map_err(|err| err.to_string())?;
Ok(CommandOutcome::Continue(String::from(
"Patient and monitor reset to defaults.",
)))
}
"bmi" => {
let weight_raw = parts
.next()
.ok_or_else(|| String::from("bmi expects weight in kg"))?;
let height_raw = parts
.next()
.ok_or_else(|| String::from("bmi expects height in m"))?;
let weight = parse_f32(weight_raw)?;
let height = parse_f32(height_raw)?;
let value = calculate_bmi(weight, height).map_err(|err| err.to_string())?;
let measurement = Measurement::new(value, "kg/m^2");
let class = classify_bmi(value);
Ok(CommandOutcome::Continue(format!(
"BMI {:.2} {} => {:?}",
measurement.value, measurement.unit, class
)))
}
"quit" | "exit" | "q" => Ok(CommandOutcome::Exit),
other => Err(format!("unknown command '{other}'")),
}
}
fn handle_set_command<'a>(
state: &mut MonitorState,
mut parts: impl Iterator<Item = &'a str>,
) -> Result<CommandOutcome, String> {
let field = parts
.next()
.ok_or_else(|| String::from("set expects a field to modify"))?
.to_ascii_lowercase();
match field.as_str() {
"dt" | "step" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set dt expects a numeric value"))?;
let dt = parse_f32(raw)?;
if dt <= 0.0 {
return Err(String::from("step size must be > 0"));
}
state.tick_seconds = dt;
Ok(CommandOutcome::Continue(format!(
"Default tick size set to {:.2} s.",
dt
)))
}
"arrhythmia" => {
let flag = parse_toggle(parts.next())?;
let heart = state
.heart_mut()
.ok_or_else(|| String::from("heart organ is not present"))?;
heart.arrhythmia = flag;
Ok(CommandOutcome::Continue(format!(
"Heart arrhythmia forcing set to {}.",
yes_no(flag)
)))
}
"tone" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set tone expects a value"))?;
let value = parse_f32(raw)?;
let clamped = value.clamp(-1.0, 1.0);
let heart = state
.heart_mut()
.ok_or_else(|| String::from("heart organ is not present"))?;
heart.autonomic_tone = clamped;
Ok(CommandOutcome::Continue(format!(
"Heart autonomic tone set to {:+.2} (input {:+.2}).",
clamped, value
)))
}
"svr" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set svr expects a value"))?;
let value = parse_f32(raw)?;
if value <= 0.0 {
return Err(String::from("systemic vascular resistance must be > 0"));
}
let heart = state
.heart_mut()
.ok_or_else(|| String::from("heart organ is not present"))?;
heart.systemic_vascular_resistance = value;
Ok(CommandOutcome::Continue(format!(
"Heart systemic vascular resistance set to {:.2} mmHg*min/L.",
value
)))
}
"ekg" => {
let tokens: Vec<_> = parts.collect();
if tokens.is_empty() {
return Err(String::from("set ekg expects one or more lead identifiers"));
}
let mut leads = Vec::with_capacity(tokens.len());
for token in tokens {
leads.push(parse_lead(token)?);
}
let summary = leads
.iter()
.map(|lead| lead_label(*lead))
.collect::<Vec<_>>()
.join(", ");
state.patient.configure_ekg_leads(leads);
Ok(CommandOutcome::Continue(format!(
"EKG leads set to {summary}."
)))
}
"glucose" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set glucose expects mg/dL"))?;
let value = parse_f32(raw)?;
state.patient.blood.glucose_mg_dl = value;
Ok(CommandOutcome::Continue(format!(
"Blood glucose set to {:.1} mg/dL.",
value
)))
}
"spo2" => {
let raw = parts
.next()
.ok_or_else(|| String::from("set spo2 expects a percentage"))?;
let value = parse_f32(raw)?.clamp(0.0, 100.0);
state.patient.blood.spo2_pct = value;
Ok(CommandOutcome::Continue(format!(
"Blood SpO2 set to {:.0}%.",
value
)))
}
"bp" | "bloodpressure" => {
let systolic_raw = parts
.next()
.ok_or_else(|| String::from("set bp expects systolic and diastolic values"))?;
let diastolic_raw = parts
.next()
.ok_or_else(|| String::from("set bp expects systolic and diastolic values"))?;
let systolic = parse_u16(systolic_raw)?;
let diastolic = parse_u16(diastolic_raw)?;
if systolic <= diastolic {
return Err(String::from("systolic must be greater than diastolic"));
}
state.patient.blood_pressure = BloodPressure {
systolic,
diastolic,
};
Ok(CommandOutcome::Continue(format!(
"Blood pressure set to {}/{} mmHg.",
systolic, diastolic
)))
}
"bmi" => {
let weight_raw = parts
.next()
.ok_or_else(|| String::from("set bmi expects weight in kg"))?;
let height_raw = parts
.next()
.ok_or_else(|| String::from("set bmi expects height in m"))?;
let weight = parse_f32(weight_raw)?;
let height = parse_f32(height_raw)?;
let measurement = bmi_measurement(weight, height).map_err(|err| err.to_string())?;
let class = classify_bmi(measurement.value);
state.bmi_inputs = (weight, height);
Ok(CommandOutcome::Continue(format!(
"Tracked BMI updated -> {:.2} {} ({:?}).",
measurement.value, measurement.unit, class
)))
}
other => Err(format!("unknown field '{other}'")),
}
}
fn render_dashboard(state: &MonitorState, status: &str) {
clear_screen();
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!(
"{}",
stat_line(
"Blood pressure",
format!(
"{} {}",
accent(format!("{bp}")),
validity_tag(bp.validate())
)
)
);
let blood = &state.patient.blood;
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));
println!("{}", section_line("Electrocardiogram"));
match state.patient.ekg_snapshot() {
Some(snapshot) => {
println!(
"{}",
stat_line(
"Rhythm",
format!(
"{:?} | {:.0} bpm | RR {:.3} s",
snapshot.rhythm, snapshot.heart_rate_bpm, snapshot.rr_interval_s
)
)
);
println!(
"{}",
stat_line(
"Axis",
format!(
"{:+.0} deg | variability {:.2}",
snapshot.frontal_axis_deg, snapshot.variability_index
)
)
);
for (idx, chunk) in snapshot.lead_samples.chunks(6).enumerate() {
let label = if idx == 0 { "Leads" } else { "" };
let body = chunk
.iter()
.map(|sample| {
format!("{} {:+.2}mV", lead_label(sample.lead), sample.amplitude_mv)
})
.collect::<Vec<_>>()
.join(" ");
println!("{}", stat_line(label, body));
}
}
None => {
let message = if state.patient.ekg_monitor().is_some() {
muted("monitor waiting for first snapshot")
} else {
muted("monitor not configured")
};
println!("{}", stat_line("Status", message));
}
}
println!();
let (weight, height) = state.bmi_inputs;
let bmi_line = match bmi_measurement(weight, height) {
Ok(measurement) => {
let class = classify_bmi(measurement.value);
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) => colorize(
COLOR_ERROR,
format!("{:.1} kg / {:.2} m -> error ({err})", weight, height),
),
};
println!("{}", stat_line("BMI inputs", bmi_line));
println!();
println!("{}", section_line("Heart"));
if let Some(heart) = state.patient.find_organ_typed::<Heart>() {
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!(
"{}",
stat_line(
"Heart",
colorize(COLOR_WARNING, "<not attached>".to_string())
)
);
}
println!();
println!("{}", section_line("Organ Snapshots"));
for organ in MONITORED_ORGANS {
println!("{}", organ_line(&state.patient, organ));
}
println!();
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 {
patient
.organ_summary(kind)
.unwrap_or_else(|err| format!("n/a ({err})"))
}
fn organ_label(kind: OrganType) -> &'static str {
match kind {
OrganType::Heart => "Heart",
OrganType::Bloodstream => "Bloodstream",
OrganType::Lungs => "Lungs",
OrganType::Brain => "Brain",
OrganType::SpinalCord => "Spinal cord",
OrganType::Stomach => "Stomach",
OrganType::Liver => "Liver",
OrganType::Gallbladder => "Gallbladder",
OrganType::Pancreas => "Pancreas",
OrganType::Intestines => "Intestines",
OrganType::Esophagus => "Esophagus",
OrganType::Kidneys => "Kidneys",
OrganType::Bladder => "Bladder",
OrganType::Spleen => "Spleen",
}
}
fn parse_f32(raw: &str) -> Result<f32, String> {
raw.parse::<f32>()
.map_err(|_| format!("unable to parse '{raw}' as a decimal number"))
}
fn parse_u16(raw: &str) -> Result<u16, String> {
raw.parse::<u16>()
.map_err(|_| format!("unable to parse '{raw}' as an integer"))
}
fn parse_toggle(value: Option<&str>) -> Result<bool, String> {
let raw = value.ok_or_else(|| String::from("expected on/off"))?;
match raw.to_ascii_lowercase().as_str() {
"on" | "true" | "1" | "yes" => Ok(true),
"off" | "false" | "0" | "no" => Ok(false),
other => Err(format!("expected on/off but received '{other}'")),
}
}
fn parse_lead(raw: &str) -> Result<EkgLead, String> {
let upper = raw.trim().to_ascii_uppercase();
match upper.as_str() {
"I" => Ok(EkgLead::I),
"II" => Ok(EkgLead::II),
"III" => Ok(EkgLead::III),
"AVR" => Ok(EkgLead::AVR),
"AVL" => Ok(EkgLead::AVL),
"AVF" => Ok(EkgLead::AVF),
"V1" => Ok(EkgLead::V1),
"V2" => Ok(EkgLead::V2),
"V3" => Ok(EkgLead::V3),
"V4" => Ok(EkgLead::V4),
"V5" => Ok(EkgLead::V5),
"V6" => Ok(EkgLead::V6),
other => Err(format!("unknown lead '{other}'")),
}
}
fn lead_label(lead: EkgLead) -> &'static str {
match lead {
EkgLead::I => "I",
EkgLead::II => "II",
EkgLead::III => "III",
EkgLead::AVR => "aVR",
EkgLead::AVL => "aVL",
EkgLead::AVF => "aVF",
EkgLead::V1 => "V1",
EkgLead::V2 => "V2",
EkgLead::V3 => "V3",
EkgLead::V4 => "V4",
EkgLead::V5 => "V5",
EkgLead::V6 => "V6",
}
}
fn yes_no(value: bool) -> &'static str {
if value {
"yes"
} else {
"no"
}
}
fn mean_arterial_pressure(bp: BloodPressure) -> f32 {
let systolic = bp.systolic as f32;
let diastolic = bp.diastolic as f32;
diastolic + (systolic - diastolic) / 3.0
}
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()
})
}