Mechanical formatting pass to satisfy rustfmt line-width limits and clippy pedantic/nursery lints. No behavioral changes. Formatting (rustfmt line wrapping): - action/sync.rs: multiline tuple destructure, function call args in tests - state/sync.rs: if-let chain formatting, remove unnecessary Vec collect - view/sync.rs: multiline array entries, format!(), vec! literals - view/doctor.rs: multiline floor_char_boundary chain - view/scope_picker.rs: multiline format!() with floor_char_boundary - view/stats.rs: multiline render_stat_row call - view/mod.rs: multiline assert!() in test - app/update.rs: multiline enum variant destructure - entity_cache.rs: multiline assert_eq!() with messages - render_cache.rs: multiline retain() closure - session.rs: multiline serde_json/File::create/parent() chains Clippy: - action/sync.rs: #[allow(clippy::too_many_arguments)] on test helper Import/module ordering (alphabetical): - state/mod.rs: move scope_picker mod + pub use to sorted position - view/mod.rs: move scope_picker, stats, sync mod + use to sorted position - view/scope_picker.rs: sort use imports (ScopeContext before ScopePickerState)
292 lines
8.2 KiB
Rust
292 lines
8.2 KiB
Rust
//! Doctor screen view — health check results.
|
|
//!
|
|
//! Renders a vertical list of health checks with colored status
|
|
//! indicators (green PASS, yellow WARN, red FAIL).
|
|
|
|
use ftui::core::geometry::Rect;
|
|
use ftui::render::cell::{Cell, PackedRgba};
|
|
use ftui::render::drawing::Draw;
|
|
use ftui::render::frame::Frame;
|
|
|
|
use crate::state::doctor::{DoctorState, HealthStatus};
|
|
|
|
use super::{TEXT, TEXT_MUTED};
|
|
|
|
/// Pass green.
|
|
const PASS_FG: PackedRgba = PackedRgba::rgb(0x87, 0x9A, 0x39);
|
|
/// Warning yellow.
|
|
const WARN_FG: PackedRgba = PackedRgba::rgb(0xD0, 0xA2, 0x15);
|
|
/// Fail red.
|
|
const FAIL_FG: PackedRgba = PackedRgba::rgb(0xD1, 0x4D, 0x41);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public entry point
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Render the doctor screen.
|
|
pub fn render_doctor(frame: &mut Frame<'_>, state: &DoctorState, area: Rect) {
|
|
if area.width < 10 || area.height < 3 {
|
|
return;
|
|
}
|
|
|
|
let max_x = area.right();
|
|
|
|
if !state.loaded {
|
|
// Not yet loaded — show centered prompt.
|
|
let msg = "Loading health checks...";
|
|
let x = area.x + area.width.saturating_sub(msg.len() as u16) / 2;
|
|
let y = area.y + area.height / 2;
|
|
frame.print_text_clipped(
|
|
x,
|
|
y,
|
|
msg,
|
|
Cell {
|
|
fg: TEXT_MUTED,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Title.
|
|
let overall = state.overall_status();
|
|
let title_fg = status_color(overall);
|
|
let title = format!("Doctor — {}", overall.label());
|
|
frame.print_text_clipped(
|
|
area.x + 2,
|
|
area.y + 1,
|
|
&title,
|
|
Cell {
|
|
fg: title_fg,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
|
|
// Summary line.
|
|
let pass_count = state.count_by_status(HealthStatus::Pass);
|
|
let warn_count = state.count_by_status(HealthStatus::Warn);
|
|
let fail_count = state.count_by_status(HealthStatus::Fail);
|
|
let summary = format!(
|
|
"{} passed, {} warnings, {} failed",
|
|
pass_count, warn_count, fail_count
|
|
);
|
|
frame.print_text_clipped(
|
|
area.x + 2,
|
|
area.y + 2,
|
|
&summary,
|
|
Cell {
|
|
fg: TEXT_MUTED,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
|
|
// Health check rows.
|
|
let rows_start_y = area.y + 4;
|
|
let name_width = 16u16;
|
|
|
|
for (i, check) in state.checks.iter().enumerate() {
|
|
let y = rows_start_y + i as u16;
|
|
if y >= area.bottom().saturating_sub(2) {
|
|
break;
|
|
}
|
|
|
|
// Status badge.
|
|
let badge = format!("[{}]", check.status.label());
|
|
let badge_fg = status_color(check.status);
|
|
frame.print_text_clipped(
|
|
area.x + 2,
|
|
y,
|
|
&badge,
|
|
Cell {
|
|
fg: badge_fg,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
|
|
// Check name.
|
|
let name_x = area.x + 2 + 7; // "[PASS] " = 7 chars
|
|
let name = format!("{:<width$}", check.name, width = name_width as usize);
|
|
frame.print_text_clipped(
|
|
name_x,
|
|
y,
|
|
&name,
|
|
Cell {
|
|
fg: TEXT,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
|
|
// Detail text.
|
|
let detail_x = name_x + name_width;
|
|
let max_detail = area.right().saturating_sub(detail_x + 1) as usize;
|
|
let detail = if check.detail.len() > max_detail {
|
|
format!(
|
|
"{}...",
|
|
&check.detail[..check
|
|
.detail
|
|
.floor_char_boundary(max_detail.saturating_sub(3))]
|
|
)
|
|
} else {
|
|
check.detail.clone()
|
|
};
|
|
frame.print_text_clipped(
|
|
detail_x,
|
|
y,
|
|
&detail,
|
|
Cell {
|
|
fg: TEXT_MUTED,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
}
|
|
|
|
// Hint at bottom.
|
|
let hint_y = area.bottom().saturating_sub(1);
|
|
frame.print_text_clipped(
|
|
area.x + 2,
|
|
hint_y,
|
|
"Esc: back | lore doctor (full check)",
|
|
Cell {
|
|
fg: TEXT_MUTED,
|
|
..Cell::default()
|
|
},
|
|
max_x,
|
|
);
|
|
}
|
|
|
|
/// Map health status to a display color.
|
|
fn status_color(status: HealthStatus) -> PackedRgba {
|
|
match status {
|
|
HealthStatus::Pass => PASS_FG,
|
|
HealthStatus::Warn => WARN_FG,
|
|
HealthStatus::Fail => FAIL_FG,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::state::doctor::HealthCheck;
|
|
use ftui::render::grapheme_pool::GraphemePool;
|
|
|
|
macro_rules! with_frame {
|
|
($width:expr, $height:expr, |$frame:ident| $body:block) => {{
|
|
let mut pool = GraphemePool::new();
|
|
let mut $frame = Frame::new($width, $height, &mut pool);
|
|
$body
|
|
}};
|
|
}
|
|
|
|
fn sample_checks() -> Vec<HealthCheck> {
|
|
vec![
|
|
HealthCheck {
|
|
name: "Config".into(),
|
|
status: HealthStatus::Pass,
|
|
detail: "/home/user/.config/lore/config.json".into(),
|
|
},
|
|
HealthCheck {
|
|
name: "Database".into(),
|
|
status: HealthStatus::Pass,
|
|
detail: "schema v12".into(),
|
|
},
|
|
HealthCheck {
|
|
name: "Projects".into(),
|
|
status: HealthStatus::Warn,
|
|
detail: "0 projects configured".into(),
|
|
},
|
|
HealthCheck {
|
|
name: "FTS Index".into(),
|
|
status: HealthStatus::Fail,
|
|
detail: "No documents indexed".into(),
|
|
},
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_not_loaded() {
|
|
with_frame!(80, 24, |frame| {
|
|
let state = DoctorState::default();
|
|
let area = frame.bounds();
|
|
render_doctor(&mut frame, &state, area);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_with_checks() {
|
|
with_frame!(80, 24, |frame| {
|
|
let mut state = DoctorState::default();
|
|
state.apply_checks(sample_checks());
|
|
let area = frame.bounds();
|
|
render_doctor(&mut frame, &state, area);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_all_pass() {
|
|
with_frame!(80, 24, |frame| {
|
|
let mut state = DoctorState::default();
|
|
state.apply_checks(vec![HealthCheck {
|
|
name: "Config".into(),
|
|
status: HealthStatus::Pass,
|
|
detail: "ok".into(),
|
|
}]);
|
|
let area = frame.bounds();
|
|
render_doctor(&mut frame, &state, area);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_tiny_terminal() {
|
|
with_frame!(8, 2, |frame| {
|
|
let mut state = DoctorState::default();
|
|
state.apply_checks(sample_checks());
|
|
let area = frame.bounds();
|
|
render_doctor(&mut frame, &state, area);
|
|
// Should not panic.
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_narrow_terminal_truncates() {
|
|
with_frame!(40, 20, |frame| {
|
|
let mut state = DoctorState::default();
|
|
state.apply_checks(vec![HealthCheck {
|
|
name: "Database".into(),
|
|
status: HealthStatus::Pass,
|
|
detail: "This is a very long detail string that should be truncated".into(),
|
|
}]);
|
|
let area = frame.bounds();
|
|
render_doctor(&mut frame, &state, area);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_render_many_checks_clips() {
|
|
with_frame!(80, 10, |frame| {
|
|
let mut state = DoctorState::default();
|
|
let mut checks = Vec::new();
|
|
for i in 0..20 {
|
|
checks.push(HealthCheck {
|
|
name: format!("Check {i}"),
|
|
status: HealthStatus::Pass,
|
|
detail: "ok".into(),
|
|
});
|
|
}
|
|
state.apply_checks(checks);
|
|
let area = frame.bounds();
|
|
render_doctor(&mut frame, &state, area);
|
|
// Should clip without panicking.
|
|
});
|
|
}
|
|
}
|