mtui 0.7.3

An extensive Modbus client for your terminal.
Documentation
use super::App;
use crate::config::Column;
use crate::interpretator::{fmt_num, format_ago, graph_value};
use crate::num_ops::cycle;
use crate::register::{RegisterCell, RegisterCellValue, RegisterType};
use crate::state::{InspectMode, Popup, ReadPanel};
use chrono::{DateTime, Local, Utc};
use std::collections::VecDeque;

const INSPECT_COLUMNS: &[Column] = &[
    Column::U16,
    Column::I16,
    Column::U8s,
    Column::I8s,
    Column::Hex,
    Column::Hex32,
    Column::F16,
    Column::Bcd,
    Column::Bcd32,
    Column::U32,
    Column::I32,
    Column::U32M10K,
    Column::I32M10K,
    Column::U64,
    Column::I64,
    Column::F32,
    Column::F64,
    Column::Ascii,
    Column::Bits,
    Column::Custom,
];

impl App {
    pub fn open_inspect(&mut self) {
        self.read_mut().popup = Some(Popup::Inspect(InspectMode::default()));
    }

    pub fn inspect_cycle(&mut self, forward: bool) {
        if let Some(mode) = self.popup_as_mut::<InspectMode>() {
            *mode = cycle(&InspectMode::ALL, *mode, forward);
        }
    }

    pub fn open_about(&mut self) {
        self.read_mut().popup = Some(Popup::About);
    }

    pub fn open_stats(&mut self) {
        self.read_mut().popup = Some(Popup::Stats);
    }

    pub fn panel_cell_at(&self, index: usize) -> Option<RegisterCell> {
        match self.read().panel {
            ReadPanel::Main | ReadPanel::Pinned | ReadPanel::Matrix => {
                self.pinned_registers.get(index).copied()
            }
            ReadPanel::Labeled => self.labels.keys().nth(index).copied(),
            ReadPanel::Custom => self.custom_rules.keys().nth(index).copied(),
        }
    }

    pub fn panel_window(&self, start: usize, count: usize) -> Vec<RegisterCell> {
        match self.read().panel {
            ReadPanel::Main | ReadPanel::Pinned | ReadPanel::Matrix => self
                .pinned_registers
                .iter()
                .skip(start)
                .take(count)
                .copied()
                .collect(),
            ReadPanel::Labeled => self
                .labels
                .keys()
                .skip(start)
                .take(count)
                .copied()
                .collect(),
            ReadPanel::Custom => self
                .custom_rules
                .keys()
                .skip(start)
                .take(count)
                .copied()
                .collect(),
        }
    }

    pub fn panel_len(&self) -> u16 {
        let len = match self.read().panel {
            ReadPanel::Main | ReadPanel::Pinned | ReadPanel::Matrix => self.pinned_registers.len(),
            ReadPanel::Labeled => self.labels.len(),
            ReadPanel::Custom => self.custom_rules.len(),
        };
        len as u16
    }

    pub fn cursor_cell(&self) -> RegisterCell {
        let (panel, register_type, position, index) = {
            let p = self.read();
            (p.panel, p.register_type, p.position, p.pinned_index)
        };
        match panel {
            ReadPanel::Main | ReadPanel::Matrix => (register_type, position),
            _ => self
                .panel_cell_at(index as usize)
                .unwrap_or((register_type, position)),
        }
    }

    pub fn cell_value(&self, cell: RegisterCell) -> Option<u16> {
        self.read_log.get(&cell).map(|&(value, _)| value)
    }

    pub fn cell_changed(&self, cell: RegisterCell) -> bool {
        self.changed.get(&cell).copied().unwrap_or(false)
    }

    pub fn inspect_lines(&self, mode: InspectMode) -> (RegisterCell, Vec<(&'static str, String)>) {
        let cell = self.cursor_cell();
        if mode != InspectMode::Now {
            return (cell, self.inspect_aggregates(cell, mode));
        }
        let (kind, addr) = cell;
        let Some(&(value, time)) = self.read_log.get(&cell) else {
            return (cell, Vec::new());
        };
        let neighbor = |offset: u16| {
            self.read_log
                .get(&(kind, addr.saturating_add(offset)))
                .map(|&(v, _)| v)
        };
        let custom = self.custom_value(cell, value, self.config.device.word_order, &neighbor);
        let label = self.labels.get(&cell).map(String::as_str);
        let mut lines = vec![
            (
                "read at",
                time.with_timezone(&Local)
                    .format("%H:%M:%S.%3f")
                    .to_string(),
            ),
            ("ago", format_ago(Utc::now().signed_duration_since(time))),
        ];
        lines.extend(self.interpreter.interpret_all(
            value,
            [neighbor(1), neighbor(2), neighbor(3)],
            custom.as_deref(),
            label,
        ));
        (cell, lines)
    }

    fn inspect_aggregates(
        &self,
        cell: RegisterCell,
        mode: InspectMode,
    ) -> Vec<(&'static str, String)> {
        let samples = self.value_history(cell).map_or(0, VecDeque::len);
        if samples == 0 {
            return Vec::new();
        }
        let mut lines = vec![("samples", samples.to_string())];
        for &column in INSPECT_COLUMNS {
            lines.push((column.name(), self.aggregate_text(cell, column, mode)));
        }
        lines.push(("label", self.labels.get(&cell).cloned().unwrap_or_default()));
        lines
    }

    fn aggregate_text(&self, cell: RegisterCell, column: Column, mode: InspectMode) -> String {
        let series = self.column_history(cell, column);
        if series.is_empty() {
            return "--".to_string();
        }
        let is_float = column.graph_is_float() || series.iter().any(|v| v.fract() != 0.0);
        match mode {
            InspectMode::Min => fmt_num(
                series.iter().copied().fold(f64::INFINITY, f64::min),
                is_float,
            ),
            InspectMode::Max => fmt_num(
                series.iter().copied().fold(f64::NEG_INFINITY, f64::max),
                is_float,
            ),
            InspectMode::Avg => {
                let avg = series.iter().sum::<f64>() / series.len() as f64;
                if is_float {
                    fmt_num(avg, true)
                } else {
                    format!("{avg:.1}")
                }
            }
            InspectMode::Now => unreachable!("aggregates are not computed for the now mode"),
        }
    }

    pub fn column_history(&self, cell: RegisterCell, column: Column) -> Vec<f64> {
        let order = self.config.device.word_order;
        if column == Column::Custom {
            let Some(rule) = self.custom_rule(cell) else {
                return Vec::new();
            };
            let width = rule.repr.register_count();
            return self.combined_history(cell, width, |regs| rule.numeric(regs, order));
        }
        let Some(width) = column.graph_width() else {
            return Vec::new();
        };
        self.combined_history(cell, width, |regs| graph_value(column, order, regs))
    }

    fn combined_history<F>(&self, cell: RegisterCell, width: usize, mut value: F) -> Vec<f64>
    where
        F: FnMut(&[u16]) -> Option<f64>,
    {
        let (kind, address) = cell;
        let mut histories = Vec::with_capacity(width);
        for offset in 0..width as u16 {
            match self.value_history((kind, address.wrapping_add(offset))) {
                Some(history) => histories.push(history),
                None => return Vec::new(),
            }
        }

        let len = histories.iter().map(|h| h.len()).min().unwrap_or(0);
        let mut regs = vec![0u16; width];
        let mut values = Vec::with_capacity(len);
        for i in 0..len {
            for (k, history) in histories.iter().enumerate() {
                regs[k] = history[history.len() - len + i];
            }
            if let Some(v) = value(&regs) {
                values.push(v);
            }
        }
        values
    }

    pub fn custom_count(&self) -> usize {
        self.custom_rules.len()
    }

    pub fn value_history(&self, cell: RegisterCell) -> Option<&VecDeque<u16>> {
        self.value_history.get(&cell)
    }

    pub fn read_count(&self) -> usize {
        self.read_log.len()
    }

    pub fn label_count(&self) -> usize {
        self.labels.len()
    }

    pub fn cell_row(&self, cell: RegisterCell, now: DateTime<Local>) -> Option<(String, bool)> {
        let (kind, addr) = cell;
        let &(value, time) = self.read_log.get(&cell)?;
        let neighbor = |offset: u16| {
            self.read_log
                .get(&(kind, addr.saturating_add(offset)))
                .map(|&(v, _)| v)
        };
        let custom = self.custom_value(cell, value, self.config.device.word_order, &neighbor);
        let label = self.labels.get(&cell).map(String::as_str);
        let row = self.interpreter.format_row(
            addr,
            value,
            [neighbor(1), neighbor(2), neighbor(3)],
            time.with_timezone(&Local),
            now,
            custom.as_deref(),
            label,
        );
        Some((row, self.cell_changed(cell)))
    }

    pub fn ascii_string_for(&self, cells: impl Iterator<Item = RegisterCell>) -> String {
        let values: Vec<RegisterCellValue> = cells
            .filter_map(|cell| self.read_log.get(&cell).map(|&(value, _)| (cell, value)))
            .collect();
        self.interpreter.ascii_string(&values)
    }

    pub fn label_text(&self, register_type: RegisterType, address: u16) -> Option<String> {
        self.labels.get(&(register_type, address)).cloned()
    }
}