mtui 0.7.1

An extensive Modbus client (TCP, RTU & mock) for your terminal.
Documentation
use super::{save_config, App, ImportPayload};
use crate::config::{Config, CustomRules, Startup};
use crate::custom::CustomRule;
use crate::modbus::ModbusDevice;
use crate::register::RegisterCell;
use crate::state::{DumpParams, ImportParams, Outcome, Popup, State, StatusMessage};
use std::collections::BTreeMap;
use std::fs;

impl App {
    pub fn open_dump(&mut self) {
        self.read_mut().popup = Some(Popup::Dump(DumpParams::default()));
    }

    fn parse_import(data: &str) -> Option<ImportPayload> {
        let payload: ImportPayload = serde_json::from_str(data.trim()).ok()?;
        (payload.total() > 0).then_some(payload)
    }

    pub fn paste_import(&mut self, data: &str) {
        match Self::parse_import(data) {
            Some(payload) => {
                let params = ImportParams {
                    pins: payload.pins(),
                    labels: payload.labels(),
                    rules: payload.rules(),
                };
                self.pending_import = Some(payload);
                self.read_mut().popup = Some(Popup::Import(params));
            }
            None => self.set_read_status(StatusMessage::warn(
                "Pasted text isn't pinned/labels/custom data",
            )),
        }
    }

    pub fn cancel_import(&mut self) {
        self.pending_import = None;
        self.close_popup();
    }

    pub fn apply_import(&mut self) {
        let Some(payload) = self.pending_import.take() else {
            self.close_popup();
            return;
        };

        let mut pins = 0;
        if let Some(p) = payload.pinned_registers {
            let incoming: Vec<RegisterCell> = p.into();
            pins = incoming.len();
            for cell in incoming {
                if !self.pinned_registers.contains(&cell) {
                    self.pinned_registers.push(cell);
                }
            }
            self.pinned_registers.sort();
            self.pinned_registers.dedup();
        }

        let mut labels = 0;
        if let Some(l) = payload.labels {
            let incoming: BTreeMap<RegisterCell, String> = l.into();
            labels = incoming.len();
            self.labels.extend(incoming);
        }

        let mut rules = 0;
        if let Some(r) = payload.custom_rules {
            let incoming: BTreeMap<RegisterCell, CustomRule> = r.into();
            rules = incoming.len();
            self.custom_rules.extend(incoming);
        }

        self.dirty = true;
        self.close_popup();
        log::info!("Imported {pins} pin(s), {labels} label(s), {rules} rule(s) from clipboard");
        self.set_read_status(StatusMessage::ok(format!(
            "Imported {pins} pin(s), {labels} label(s), {rules} rule(s)"
        )));
    }

    pub(super) fn persist_config(&mut self) -> Outcome {
        self.config.labels = (&self.labels).into();

        let rebuilt: CustomRules = (&self.custom_rules).into();
        self.config.custom_rules.holdings = rebuilt.holdings;
        self.config.custom_rules.inputs = rebuilt.inputs;
        self.config.custom_rules.coils = rebuilt.coils;
        self.config.custom_rules.discretes = rebuilt.discretes;

        self.config.pinned_registers = self.pinned_registers.as_slice().into();

        self.config.interpretations = self.interpreter.config();
        if let State::Read(p) = &self.state {
            self.config.startup = Startup {
                address: p.position,
                register_type: p.register_type,
                panel: p.panel,
            };
        }

        save_config(&self.config_path, &self.config)
            .map(|()| format!("Saved to {}", self.config_path))
            .map_err(|e| format!("Save failed: {e}"))
    }

    pub fn config_path(&self) -> &str {
        &self.config_path
    }

    pub(super) async fn load_config_from(&mut self, path: &str) -> Outcome {
        if path.is_empty() {
            return Err("Load failed: enter a file name".to_string());
        }
        let content = fs::read_to_string(path).map_err(|e| format!("Load failed: {e}"))?;
        let config: Config =
            serde_json::from_str(&content).map_err(|e| format!("Load failed: {e}"))?;

        let device = ModbusDevice::new(&config.device)
            .await
            .map_err(|e| format!("Load failed: device: {e}"))?;

        self.apply_config(config, Some(device));
        self.dirty = true;

        let read = self.startup_read_params();
        if let Some(s) = self.settings_mut() {
            s.previous = read;
        }

        Ok(format!("Loaded {path}"))
    }

    pub fn commit_dump(&mut self) {
        let result = self.dump_read_log();
        if let Some(Popup::Dump(d)) = &mut self.read_mut().popup {
            d.result = Some(result);
        }
    }
}