mtui 0.7.4

An extensive Modbus client for your terminal.
Documentation
use crate::app::App;
use crate::input::KeyCode;
use crate::state::{CustomField, CustomParams};
use crate::tui::draw_state::{edit_value, field_row};
use crate::tui::hints::{self, Hint};
use crate::tui::theme::Theme;
use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use ratatui::Frame;

fn section(field: CustomField) -> &'static str {
    match field {
        CustomField::Repr | CustomField::WordOrder | CustomField::Next => "DECODE",
        CustomField::Ops | CustomField::Enum | CustomField::Bits => "MAP",
        CustomField::Decimals | CustomField::Prefix | CustomField::Suffix => "FORMAT",
    }
}

fn joined<T>(items: &[T], display: impl Fn(&T) -> String) -> String {
    if items.is_empty() {
        "(none)".to_string()
    } else {
        items.iter().map(display).collect::<Vec<_>>().join("  ")
    }
}

pub(super) fn draw(frame: &mut Frame, area: Rect, theme: &Theme, app: &App, c: &CustomParams) {
    let sel = c.current_field();

    let kb = &app.config.keybinds;
    let footer = [
        Hint::pair(kb.move_up, kb.move_down, "Field"),
        Hint::pair(KeyCode::Left, KeyCode::Right, "Change"),
        Hint::key(kb.action, "Save"),
        Hint::key(KeyCode::Delete, "Remove"),
        Hint::key(kb.exit, "Close"),
    ];
    let width = hints::min_width(56, &footer);
    let inner = width.saturating_sub(2) as usize;

    let mut lines: Vec<Line> = vec![];

    match app.custom_preview(c) {
        Ok(preview) => {
            let mut segments = vec![(preview.words, false)];
            if let Some(base) = preview.base {
                segments.push((base, false));
            }
            segments.push((preview.output, true));

            let mut spans: Vec<Span> = Vec::new();
            let mut used = 0usize;
            for (i, (text, is_output)) in segments.into_iter().enumerate() {
                let style = if is_output {
                    theme.accent_style()
                } else {
                    theme.base()
                };
                let sep = if i == 0 { " " } else { " \u{2192} " };
                let needed = sep.chars().count() + text.chars().count();
                if i > 0 && used + needed > inner {
                    lines.push(Line::from(std::mem::take(&mut spans)));
                    let indent = "  \u{2192} ";
                    used = indent.chars().count() + text.chars().count();
                    spans.push(Span::styled(indent, theme.dim_style()));
                } else {
                    used += needed;
                    spans.push(Span::styled(sep, theme.dim_style()));
                }
                spans.push(Span::styled(text, style));
            }
            lines.push(Line::from(spans));
        }
        Err(reason) => {
            lines.push(Line::from(Span::styled(
                format!(" {reason}"),
                theme.dim_style(),
            )));
        }
    }

    let entry_hints = |lines: &mut Vec<Line>, buffer: &str, example: &str| {
        lines.push(Line::from(Span::styled(
            format!("    add: {buffer}_   {example}"),
            theme.dim_style(),
        )));
        lines.push(Line::from(Span::styled(
            "    (enter adds \u{b7} empty enter saves \u{b7} backspace removes)",
            theme.dim_style(),
        )));
    };

    let mut current_section = "";
    for field in c.fields() {
        if section(field) != current_section {
            current_section = section(field);
            lines.push(Line::default());
            lines.push(Line::from(Span::styled(
                format!(" {current_section}"),
                theme.dim_style(),
            )));
        }
        let selected = field == sel;

        match field {
            CustomField::Repr => {
                let value = format!("{}  ({} reg)", c.repr.label(), c.repr.register_count());
                lines.push(field_row(
                    theme,
                    "Type",
                    12,
                    edit_value(value, selected, true),
                    selected,
                ));
            }
            CustomField::WordOrder => {
                let value = match c.word_order {
                    Some(order) => format!("{order:?}"),
                    None => format!("device ({:?})", app.config.device.word_order),
                };
                lines.push(field_row(
                    theme,
                    "Word order",
                    12,
                    edit_value(value, selected, true),
                    selected,
                ));
            }
            CustomField::Next => {
                let value = if c.next.is_empty() {
                    "(contiguous)".to_string()
                } else {
                    joined(&c.next, u16::to_string)
                };
                lines.push(field_row(theme, "Next words", 12, value, selected));
                if selected {
                    entry_hints(&mut lines, &c.next_buffer, "address of word 2 (then 3, 4)");
                }
            }
            CustomField::Ops => {
                let value = joined(&c.ops, |o| o.display());
                lines.push(field_row(theme, "Operations", 12, value, selected));
                if selected {
                    entry_hints(&mut lines, &c.op_buffer, "e.g. *0.1  +5  /10  ^2");
                }
            }
            CustomField::Enum => {
                let value = joined(&c.enum_map, |e| format!("{}\u{2192}{}", e.value, e.text));
                lines.push(field_row(theme, "Enum", 12, value, selected));
                if selected {
                    entry_hints(&mut lines, &c.enum_buffer, "e.g. 3=Running");
                }
            }
            CustomField::Bits => {
                let value = joined(&c.bits, |e| format!("{}\u{2192}{}", e.bit, e.name));
                lines.push(field_row(theme, "Bits", 12, value, selected));
                if selected {
                    entry_hints(&mut lines, &c.bit_buffer, "e.g. 0=run");
                }
            }
            CustomField::Decimals => {
                let value = if selected {
                    format!("{}_", c.decimals)
                } else if c.decimals.is_empty() {
                    "auto".to_string()
                } else {
                    c.decimals.clone()
                };
                lines.push(field_row(theme, "Decimals", 12, value, selected));
                if selected {
                    lines.push(Line::from(Span::styled(
                        "    auto; 0 for none; numerical for amount",
                        theme.dim_style(),
                    )));
                }
            }
            CustomField::Prefix => {
                let value = edit_value(c.prefix.clone(), selected, false);
                lines.push(field_row(theme, "Prefix", 12, value, selected));
            }
            CustomField::Suffix => {
                let value = edit_value(c.suffix.clone(), selected, false);
                lines.push(field_row(theme, "Suffix", 12, value, selected));
            }
        }
    }

    if let Some(err) = &c.error {
        lines.push(Line::default());
        lines.push(Line::from(Span::styled(
            format!(" {err}"),
            theme.err_style(),
        )));
    }

    lines.push(Line::default());
    lines.push(hints::footer(theme, footer));

    let title = format!("Custom rule \u{b7} {:?} @ {}", c.register_type, c.address);
    super::render(frame, area, theme, &title, width, lines);
}