use anyhow::Result;
use crate::data::{History, MonitorData, Thresholds};
use crate::source::DataSource;
use crate::ui::summary::SortColumn;
use crate::ui::BottleneckSortColumn;
use crate::ui::Theme;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum View {
Summary,
Bottleneck,
DataFlow,
}
impl View {
pub fn next(self) -> Self {
match self {
View::Summary => View::Bottleneck,
View::Bottleneck => View::DataFlow,
View::DataFlow => View::Summary,
}
}
pub fn prev(self) -> Self {
match self {
View::Summary => View::DataFlow,
View::Bottleneck => View::Summary,
View::DataFlow => View::Bottleneck,
}
}
pub fn label(&self) -> &'static str {
match self {
View::Summary => "Summary",
View::Bottleneck => "Bottlenecks",
View::DataFlow => "Flow",
}
}
}
#[derive(Debug, Clone)]
pub struct ViewState {
pub view: View,
pub selected_module_index: usize,
pub selected_topic_index: usize,
}
pub struct App {
pub running: bool,
pub current_view: View,
pub show_help: bool,
pub show_detail_overlay: bool,
source: Box<dyn DataSource>,
pub data: Option<MonitorData>,
pub history: History,
pub load_error: Option<String>,
pub thresholds: Thresholds,
pub selected_module_index: usize,
pub selected_topic_index: usize,
pub view_stack: Vec<ViewState>,
pub sort_column: SortColumn,
pub sort_ascending: bool,
pub bottleneck_sort_column: BottleneckSortColumn,
pub bottleneck_sort_ascending: bool,
pub filter_text: String,
pub filter_active: bool,
pub theme: Theme,
pub status_message: Option<(String, std::time::Instant)>,
}
impl App {
pub fn new(source: Box<dyn DataSource>, thresholds: Thresholds) -> Self {
Self {
running: true,
current_view: View::Summary,
show_help: false,
show_detail_overlay: false,
source,
data: None,
history: History::new(),
load_error: None,
thresholds,
selected_module_index: 0,
selected_topic_index: 0,
view_stack: Vec::new(),
sort_column: SortColumn::default(),
sort_ascending: true,
bottleneck_sort_column: BottleneckSortColumn::default(),
bottleneck_sort_ascending: false, filter_text: String::new(),
filter_active: false,
theme: Theme::auto_detect(),
status_message: None,
}
}
pub fn source_description(&self) -> &str {
self.source.description()
}
pub fn set_status_message(&mut self, message: String) {
self.status_message = Some((message, std::time::Instant::now()));
}
pub fn get_status_message(&self) -> Option<&str> {
if let Some((msg, time)) = &self.status_message {
if time.elapsed() < std::time::Duration::from_secs(3) {
return Some(msg);
}
}
None
}
#[allow(dead_code)]
pub fn push_view(&mut self, view: View) {
self.view_stack.push(ViewState {
view: self.current_view,
selected_module_index: self.selected_module_index,
selected_topic_index: self.selected_topic_index,
});
self.current_view = view;
self.selected_topic_index = 0;
}
pub fn pop_view(&mut self) -> bool {
if let Some(state) = self.view_stack.pop() {
self.current_view = state.view;
self.selected_module_index = state.selected_module_index;
self.selected_topic_index = state.selected_topic_index;
true
} else {
false
}
}
pub fn breadcrumb(&self) -> String {
let mut parts: Vec<&str> = self.view_stack.iter().map(|s| s.view.label()).collect();
parts.push(self.current_view.label());
parts.join(" > ")
}
pub fn reload_data(&mut self) -> Result<bool> {
if let Some(err) = self.source.error() {
self.load_error = Some(err.to_string());
return Ok(false);
}
if let Some(snapshot) = self.source.poll() {
let data = MonitorData::from_snapshot(snapshot, &self.thresholds);
self.history.record(&data);
self.data = Some(data);
self.load_error = None;
if let Some(ref data) = self.data {
if self.selected_module_index >= data.modules.len() {
self.selected_module_index = data.modules.len().saturating_sub(1);
}
}
Ok(true)
} else {
Ok(false)
}
}
pub fn next_view(&mut self) {
self.current_view = self.current_view.next();
self.selected_topic_index = 0;
}
pub fn prev_view(&mut self) {
self.current_view = self.current_view.prev();
self.selected_topic_index = 0;
}
pub fn set_view(&mut self, view: View) {
self.current_view = view;
self.selected_topic_index = 0;
}
pub fn select_next(&mut self) {
self.select_next_n(1);
}
pub fn select_prev(&mut self) {
self.select_prev_n(1);
}
pub fn select_next_n(&mut self, n: usize) {
match self.current_view {
View::Summary => {
if let Some(ref data) = self.data {
let filtered_count = self.filtered_module_count(data);
let max = filtered_count.saturating_sub(1);
self.selected_module_index = (self.selected_module_index + n).min(max);
}
}
View::DataFlow => {
if let Some(ref data) = self.data {
let max = data.modules.len().saturating_sub(1);
self.selected_module_index = (self.selected_module_index + n).min(max);
}
}
View::Bottleneck => {
if let Some(ref data) = self.data {
let count = self.filtered_bottleneck_count(data);
let max = count.saturating_sub(1);
self.selected_topic_index = (self.selected_topic_index + n).min(max);
}
}
}
}
pub fn select_prev_n(&mut self, n: usize) {
match self.current_view {
View::Summary | View::DataFlow => {
self.selected_module_index = self.selected_module_index.saturating_sub(n);
}
View::Bottleneck => {
self.selected_topic_index = self.selected_topic_index.saturating_sub(n);
}
}
}
pub fn select_first(&mut self) {
match self.current_view {
View::Summary | View::DataFlow => {
self.selected_module_index = 0;
}
View::Bottleneck => {
self.selected_topic_index = 0;
}
}
}
pub fn select_last(&mut self) {
match self.current_view {
View::Summary => {
if let Some(ref data) = self.data {
let filtered_count = self.filtered_module_count(data);
self.selected_module_index = filtered_count.saturating_sub(1);
}
}
View::DataFlow => {
if let Some(ref data) = self.data {
self.selected_module_index = data.modules.len().saturating_sub(1);
}
}
View::Bottleneck => {
if let Some(ref data) = self.data {
let count = self.filtered_bottleneck_count(data);
self.selected_topic_index = count.saturating_sub(1);
}
}
}
}
fn filtered_module_count(&self, data: &MonitorData) -> usize {
if self.filter_text.is_empty() {
return data.modules.len();
}
data.modules
.iter()
.filter(|m| self.matches_filter(&m.name))
.count()
}
pub fn get_selected_module_raw_index(&self) -> Option<usize> {
let data = self.data.as_ref()?;
match self.current_view {
View::Summary => {
let mut modules: Vec<(usize, &crate::data::ModuleData)> = data
.modules
.iter()
.enumerate()
.filter(|(_, m)| self.matches_filter(&m.name))
.collect();
crate::ui::summary::sort_modules_by(
&mut modules,
self.sort_column,
self.sort_ascending,
);
modules.get(self.selected_module_index).map(|(idx, _)| *idx)
}
View::DataFlow => {
if self.selected_module_index < data.modules.len() {
Some(self.selected_module_index)
} else {
None
}
}
View::Bottleneck => {
None
}
}
}
pub fn enter_detail(&mut self) {
if self.current_view == View::Summary || self.current_view == View::Bottleneck {
self.show_detail_overlay = true;
}
}
pub fn go_back(&mut self) {
if self.show_detail_overlay {
self.show_detail_overlay = false;
return;
}
if !self.pop_view() {
if self.current_view != View::Summary {
self.current_view = View::Summary;
}
}
}
pub fn close_overlay(&mut self) {
self.show_detail_overlay = false;
}
pub fn toggle_help(&mut self) {
self.show_help = !self.show_help;
}
pub fn cycle_sort(&mut self) {
match self.current_view {
View::Summary => self.sort_column = self.sort_column.next(),
View::Bottleneck => self.bottleneck_sort_column = self.bottleneck_sort_column.next(),
_ => {}
}
}
pub fn toggle_sort_direction(&mut self) {
match self.current_view {
View::Summary => self.sort_ascending = !self.sort_ascending,
View::Bottleneck => self.bottleneck_sort_ascending = !self.bottleneck_sort_ascending,
_ => {}
}
}
pub fn start_filter(&mut self) {
self.filter_active = true;
}
pub fn cancel_filter(&mut self) {
self.filter_active = false;
}
pub fn clear_filter(&mut self) {
self.filter_text.clear();
self.filter_active = false;
}
pub fn filter_push(&mut self, c: char) {
self.filter_text.push(c);
}
pub fn filter_pop(&mut self) {
self.filter_text.pop();
}
pub fn matches_filter(&self, name: &str) -> bool {
if self.filter_text.is_empty() {
return true;
}
name.to_lowercase()
.contains(&self.filter_text.to_lowercase())
}
fn filtered_bottleneck_count(&self, data: &MonitorData) -> usize {
if self.filter_text.is_empty() {
return data.unhealthy_topics().len();
}
let search = self.filter_text.to_lowercase();
data.unhealthy_topics()
.iter()
.filter(|(module, topic)| {
module.name.to_lowercase().contains(&search)
|| topic.topic().to_lowercase().contains(&search)
})
.count()
}
pub fn quit(&mut self) {
self.running = false;
}
pub fn export_state(&self, path: &std::path::Path) -> anyhow::Result<()> {
use std::io::Write;
let Some(ref data) = self.data else {
anyhow::bail!("No data to export");
};
let mut export = serde_json::Map::new();
let mut summary = serde_json::Map::new();
summary.insert(
"total_modules".to_string(),
serde_json::json!(data.modules.len()),
);
let healthy = data
.modules
.iter()
.filter(|m| m.health == crate::data::HealthStatus::Healthy)
.count();
let warning = data
.modules
.iter()
.filter(|m| m.health == crate::data::HealthStatus::Warning)
.count();
let critical = data
.modules
.iter()
.filter(|m| m.health == crate::data::HealthStatus::Critical)
.count();
summary.insert("healthy".to_string(), serde_json::json!(healthy));
summary.insert("warning".to_string(), serde_json::json!(warning));
summary.insert("critical".to_string(), serde_json::json!(critical));
export.insert("summary".to_string(), serde_json::Value::Object(summary));
let modules: Vec<serde_json::Value> = data
.modules
.iter()
.map(|m| {
serde_json::json!({
"name": m.name,
"total_read": m.total_read,
"total_written": m.total_written,
"health": format!("{:?}", m.health)
})
})
.collect();
export.insert("modules".to_string(), serde_json::Value::Array(modules));
let json = serde_json::to_string_pretty(&serde_json::Value::Object(export))?;
let mut file = std::fs::File::create(path)?;
file.write_all(json.as_bytes())?;
Ok(())
}
}