mtui 0.7.1

An extensive Modbus client (TCP, RTU & mock) for your terminal.
Documentation
use super::{build_custom_rule, App};
use crate::custom::{parse_enum, parse_op, CustomRepr};
use crate::modbus::WordOrder;
use crate::num_ops::{cycle, wrap_index};
use crate::register::RegisterCell;
use crate::state::{CustomField, CustomParams, Popup};

impl App {
    pub fn open_custom(&mut self) {
        let (register_type, address) = self.cursor_cell();

        let params = match self.custom_rules.get(&(register_type, address)) {
            Some(rule) => CustomParams {
                address,
                register_type,
                repr: rule.repr,
                ops: rule.ops.clone(),
                enum_map: rule.enum_map.clone(),
                decimals: rule.decimals.map(|d| d.to_string()).unwrap_or_default(),
                prefix: rule.prefix.clone(),
                suffix: rule.suffix.clone(),
                op_buffer: String::new(),
                enum_buffer: String::new(),
                selected: 0,
                existed: true,
                error: None,
            },
            None => CustomParams {
                address,
                register_type,
                repr: CustomRepr::default(),
                ops: Vec::new(),
                enum_map: Vec::new(),
                decimals: String::new(),
                prefix: String::new(),
                suffix: String::new(),
                op_buffer: String::new(),
                enum_buffer: String::new(),
                selected: 0,
                existed: false,
                error: None,
            },
        };
        self.read_mut().popup = Some(Popup::Custom(params));
    }

    fn with_custom(&mut self, f: impl FnOnce(&mut CustomParams)) {
        if let Some(Popup::Custom(c)) = &mut self.read_mut().popup {
            f(c);
        }
    }

    pub fn custom_move(&mut self, down: bool) {
        let n = CustomField::ALL.len() as u16;
        self.with_custom(|c| {
            c.error = None;
            c.selected = wrap_index(c.selected, n, down);
        });
    }

    pub fn custom_cycle(&mut self, field: CustomField, forward: bool) {
        self.with_custom(|c| {
            c.error = None;
            if field == CustomField::Repr {
                c.repr = cycle(&CustomRepr::ALL, c.repr, forward);
            }
        });
    }

    pub fn custom_char(&mut self, field: CustomField, ch: char) {
        self.with_custom(|c| {
            c.error = None;
            match field {
                CustomField::Ops => c.op_buffer.push(ch),
                CustomField::Enum => c.enum_buffer.push(ch),
                CustomField::Decimals => {
                    if ch.is_ascii_digit() && c.decimals.len() < 2 {
                        c.decimals.push(ch);
                    }
                }
                CustomField::Prefix => c.prefix.push(ch),
                CustomField::Suffix => c.suffix.push(ch),
                _ => {}
            }
        });
    }

    pub fn custom_backspace(&mut self, field: CustomField) {
        self.with_custom(|c| {
            c.error = None;
            match field {
                CustomField::Ops => {
                    if c.op_buffer.pop().is_none() {
                        c.ops.pop();
                    }
                }
                CustomField::Enum => {
                    if c.enum_buffer.pop().is_none() {
                        c.enum_map.pop();
                    }
                }
                CustomField::Decimals => {
                    c.decimals.pop();
                }
                CustomField::Prefix => {
                    c.prefix.pop();
                }
                CustomField::Suffix => {
                    c.suffix.pop();
                }
                _ => {}
            }
        });
    }

    pub fn custom_enter(&mut self, field: CustomField) {
        match field {
            CustomField::Ops => self.with_custom(|c| {
                if c.op_buffer.trim().is_empty() {
                    return;
                }
                match parse_op(&c.op_buffer) {
                    Ok(op) => {
                        c.ops.push(op);
                        c.op_buffer.clear();
                    }
                    Err(e) => c.error = Some(format!("op: {e}")),
                }
            }),
            CustomField::Enum => self.with_custom(|c| {
                if c.enum_buffer.trim().is_empty() {
                    return;
                }
                match parse_enum(&c.enum_buffer) {
                    Ok(entry) => {
                        c.enum_map.push(entry);
                        c.enum_buffer.clear();
                    }
                    Err(e) => c.error = Some(format!("enum: {e}")),
                }
            }),
            CustomField::Save => self.commit_custom(),
            CustomField::Remove => self.remove_custom(),
            _ => {}
        }
    }

    pub fn commit_custom(&mut self) {
        let built = match &self.read().popup {
            Some(Popup::Custom(c)) => build_custom_rule(c),
            _ => return,
        };
        match built {
            Ok((cell, rule)) => {
                self.custom_rules.insert(cell, rule);
                self.dirty = true;
                self.read_mut().popup = None;
                log::info!("Custom rule set \u{b7} {:?}@{}", cell.0, cell.1);
            }
            Err(e) => self.with_custom(|c| c.error = Some(e)),
        }
    }

    pub fn remove_custom(&mut self) {
        let cell = match &self.read().popup {
            Some(Popup::Custom(c)) => (c.register_type, c.address),
            _ => return,
        };
        if self.custom_rules.remove(&cell).is_some() {
            self.dirty = true;
            log::info!("Custom rule removed \u{b7} {:?}@{}", cell.0, cell.1);
        }
        self.read_mut().popup = None;
    }

    pub(super) fn custom_value(
        &self,
        cell: RegisterCell,
        value: u16,
        word_order: WordOrder,
        neighbor: &impl Fn(u16) -> Option<u16>,
    ) -> Option<String> {
        let (kind, address) = cell;
        let Some(rule) = self.custom_rules.get(&cell) else {
            if !self.config.custom_rules.show_continuation {
                return None;
            }
            let prev = address.checked_sub(1)?;
            let prev_rule = self.custom_rules.get(&(kind, prev))?;
            return (prev_rule.repr.register_count() == 2).then(|| "part of \u{2191}".to_string());
        };
        let mut words = vec![value];
        if rule.repr.register_count() == 2 {
            if let Some(n) = neighbor(1) {
                words.push(n);
            }
        }
        let formatted = rule.evaluate(&words, word_order);
        (!formatted.is_empty()).then_some(formatted)
    }

    pub fn custom_preview(&self, c: &CustomParams) -> Result<(String, String), String> {
        let (cell, rule) = build_custom_rule(c)?;
        let Some(&(value, _)) = self.read_log.get(&cell) else {
            return Err("no value read yet".to_string());
        };
        let mut words = vec![value];
        if rule.repr.register_count() == 2 {
            match self.read_log.get(&(cell.0, cell.1.saturating_add(1))) {
                Some(&(n, _)) => words.push(n),
                None => return Err("waiting for second register".to_string()),
            }
        }
        let output = rule.evaluate(&words, self.config.device.word_order);
        if output.is_empty() {
            return Err("no output".to_string());
        }
        let input = words
            .iter()
            .map(|w| w.to_string())
            .collect::<Vec<_>>()
            .join(", ");
        Ok((input, output))
    }
}