mtui 0.9.6

An extensive Modbus client for your terminal.
Documentation
use super::App;

use crate::num_ops::cycle;
use crate::register::RegisterType;
use crate::state::{ReadPanel, SettingsField, SettingsParams, State, StatusMessage};
use crate::tui::theme::{self, Theme};
use ratatui::style::Color;

fn theme_field(theme: &mut Theme, field: SettingsField) -> Option<&mut Color> {
    Some(match field {
        SettingsField::ThemeBg => &mut theme.bg,
        SettingsField::ThemeBorder => &mut theme.border,
        SettingsField::ThemeAccent => &mut theme.accent,
        SettingsField::ThemeText => &mut theme.text,
        SettingsField::ThemeDim => &mut theme.dim,
        SettingsField::ThemeChanged => &mut theme.changed,
        SettingsField::ThemeZebra => &mut theme.zebra,
        SettingsField::ThemeOk => &mut theme.ok,
        SettingsField::ThemeWarn => &mut theme.warn,
        SettingsField::ThemeErr => &mut theme.err,
        SettingsField::ThemeSelectedFg => &mut theme.selected_fg,
        SettingsField::ThemeSelectedBg => &mut theme.selected_bg,
        _ => return None,
    })
}

impl App {
    pub fn settings(&self) -> Option<&SettingsParams> {
        match &self.state {
            State::Settings(s) => Some(s),
            _ => None,
        }
    }

    pub fn settings_mut(&mut self) -> Option<&mut SettingsParams> {
        match &mut self.state {
            State::Settings(s) => Some(s),
            _ => None,
        }
    }

    pub fn open_settings(&mut self) {
        let previous = std::mem::take(self.read_mut());
        self.state = State::Settings(SettingsParams {
            previous,
            ..Default::default()
        });
    }

    pub fn close_settings(&mut self) {
        let mut previous = match &mut self.state {
            State::Settings(s) => std::mem::take(&mut s.previous),
            _ => return,
        };
        previous.loading = false;
        self.state = State::Read(previous);
    }

    pub(super) fn set_settings_status(&mut self, message: StatusMessage) {
        if let Some(s) = self.settings_mut() {
            s.status = Some(message);
        }
    }

    fn numeric_spec(field: SettingsField) -> Option<(i64, i64, i64)> {
        match field {
            SettingsField::RegistersBatch
            | SettingsField::HistoryCap
            | SettingsField::MatrixCols => Some((1, u16::MAX as i64, 1)),
            SettingsField::StartupAddress => Some((0, u16::MAX as i64, 1)),
            SettingsField::AutoUpdate | SettingsField::ChangedExpiry => {
                Some((0, u32::MAX as i64, 100))
            }
            SettingsField::ApiPort => Some((-1, u16::MAX as i64, 1)),
            _ => None,
        }
    }

    fn numeric_get(&self, field: SettingsField) -> i64 {
        match field {
            SettingsField::RegistersBatch => self.config.registers_batch as i64,
            SettingsField::AutoUpdate => self.config.update_interval_ms.map_or(0, |n| n as i64),
            SettingsField::ChangedExpiry => self.config.changed_expiry_ms.map_or(0, |n| n as i64),
            SettingsField::HistoryCap => self.config.graph_history_cap as i64,
            SettingsField::MatrixCols => self.config.matrix_cols as i64,
            SettingsField::StartupAddress => self.config.startup.address as i64,
            SettingsField::ApiPort => self.config.port.map_or(-1, |p| p as i64),
            _ => 0,
        }
    }

    fn numeric_set(&mut self, field: SettingsField, value: i64) {
        match field {
            SettingsField::RegistersBatch => self.config.registers_batch = value as u16,
            SettingsField::AutoUpdate => {
                self.config.update_interval_ms = (value > 0).then_some(value as u64)
            }
            SettingsField::ChangedExpiry => {
                self.config.changed_expiry_ms = (value > 0).then_some(value as u64)
            }
            SettingsField::HistoryCap => self.config.graph_history_cap = value as u16,
            SettingsField::MatrixCols => self.config.matrix_cols = value as u16,
            SettingsField::StartupAddress => self.config.startup.address = value as u16,
            SettingsField::ApiPort => self.config.port = (value >= 0).then_some(value as u16),
            _ => {}
        }
    }

    pub fn settings_adjust(&mut self, field: SettingsField, delta: i64) {
        match field {
            SettingsField::IgnoreDirty => self.config.ignore_dirty = !self.config.ignore_dirty,
            SettingsField::ShowMock => self.config.show_mock = !self.config.show_mock,
            SettingsField::ReadOnly => self.config.read_only = !self.config.read_only,
            SettingsField::ApiSlaveOverride => {
                self.config.allow_api_slave_id = !self.config.allow_api_slave_id
            }
            SettingsField::LogWrites => self.config.log_writes = !self.config.log_writes,
            SettingsField::ShowContinuation => {
                self.config.custom_rules.show_continuation =
                    !self.config.custom_rules.show_continuation
            }
            SettingsField::ShowClock => self.config.show_clock = !self.config.show_clock,
            SettingsField::ShowFrameTime => {
                self.config.show_frame_time = !self.config.show_frame_time
            }
            SettingsField::ShowRam => self.config.show_ram = !self.config.show_ram,
            SettingsField::ShowAscii => self.config.show_ascii = !self.config.show_ascii,
            SettingsField::ShowInactiveTabs => {
                self.config.show_inactive_tabs = !self.config.show_inactive_tabs
            }
            SettingsField::ShowReadWindow => {
                self.config.show_read_window = !self.config.show_read_window
            }
            SettingsField::GraphTimeAxis => {
                self.config.graph_time_axis = !self.config.graph_time_axis
            }
            SettingsField::StartupPanel => {
                self.config.startup.panel =
                    cycle(&ReadPanel::ALL, self.config.startup.panel, delta > 0);
            }
            SettingsField::StartupType => {
                self.config.startup.register_type = cycle(
                    &RegisterType::ALL,
                    self.config.startup.register_type,
                    delta > 0,
                );
            }
            SettingsField::CycleHoldings
            | SettingsField::CycleInputs
            | SettingsField::CycleCoils
            | SettingsField::CycleDiscretes => {
                let rt = field.cycle_register_type().expect("cycle field");
                self.config.cycle_types.toggle(rt);
            }
            SettingsField::ThemePreset => {
                let presets = Theme::PRESETS;
                let index = match presets.iter().position(|&(_, t)| t == self.config.theme) {
                    Some(i) if delta > 0 => (i + 1) % presets.len(),
                    Some(i) => (i + presets.len() - 1) % presets.len(),
                    None if delta > 0 => 0,
                    None => presets.len() - 1,
                };
                self.config.theme = presets[index].1;
            }
            f if f.is_theme_color() => {
                if let Some(slot) = theme_field(&mut self.config.theme, f) {
                    *slot = cycle(theme::PALETTE, *slot, delta > 0);
                }
            }
            _ => {
                let Some((min, max, step)) = Self::numeric_spec(field) else {
                    return;
                };
                let value = (self.numeric_get(field) + delta * step).clamp(min, max);
                self.numeric_set(field, value);
            }
        }
        self.refresh_writes_log_state();
        self.sync_api_read_only();
        self.sync_api_allow_slave_id();
        self.refresh_dirty();
    }

    pub fn settings_digit(&mut self, field: SettingsField, digit: u8) {
        if field.is_theme_color() {
            if let Some(slot) = theme_field(&mut self.config.theme, field) {
                let current = match *slot {
                    Color::Indexed(n) => n as i64,
                    _ => 0,
                };
                let value = (current * 10 + digit as i64).clamp(0, 255) as u8;
                *slot = Color::Indexed(value);
                self.refresh_dirty();
            }
            return;
        }
        let Some((min, max, _)) = Self::numeric_spec(field) else {
            return;
        };
        let value = (self.numeric_get(field).max(0) * 10 + digit as i64).clamp(min, max);
        self.numeric_set(field, value);
        self.refresh_dirty();
    }

    pub fn settings_text_input(&mut self, field: SettingsField, c: char) {
        match field {
            SettingsField::LoadConfig => {
                if let Some(s) = self.settings_mut() {
                    s.load_path.push(c);
                }
            }
            SettingsField::Name => {
                self.config.name.push(c);
                self.refresh_dirty();
            }
            SettingsField::NextConfig => {
                self.config.next_config.push(c);
                self.refresh_dirty();
            }
            _ => {}
        }
    }

    pub fn settings_backspace(&mut self, field: SettingsField) {
        if field == SettingsField::LoadConfig {
            if let Some(s) = self.settings_mut() {
                s.load_path.pop();
            }
            return;
        }
        if field == SettingsField::Name {
            self.config.name.pop();
            self.refresh_dirty();
            return;
        }
        if field == SettingsField::NextConfig {
            self.config.next_config.pop();
            self.refresh_dirty();
            return;
        }
        if field.is_theme_color() {
            let default = theme_field(&mut Theme::default(), field).copied();
            if let Some(slot) = theme_field(&mut self.config.theme, field) {
                match *slot {
                    Color::Indexed(n) if n > 0 => *slot = Color::Indexed(n / 10),
                    _ => {
                        if let Some(def) = default {
                            *slot = def;
                        }
                    }
                }
                self.refresh_dirty();
            }
            return;
        }
        let Some((min, _, _)) = Self::numeric_spec(field) else {
            return;
        };
        let value = self.numeric_get(field);
        self.numeric_set(field, if value >= 10 { value / 10 } else { min });
        self.refresh_dirty();
    }

    pub fn settings_save(&mut self) {
        let result = self.persist_config();
        match &result {
            Ok(_) => log::info!("Configuration saved"),
            Err(error) => log::error!("Save failed \u{b7} {error}"),
        }
        self.set_settings_status(result.into());
    }

    pub fn settings_load(&mut self) {
        let Some(path) = self.settings().map(|s| s.load_path.trim().to_string()) else {
            return;
        };
        if !self.free_background_slot() {
            self.set_settings_status(StatusMessage::info("Device is busy."));
            return;
        }
        match self.start_config_load(path) {
            Ok(()) => self.set_settings_status(StatusMessage::info("Loading\u{2026}")),
            Err(error) => {
                log::error!("{error}");
                self.set_settings_status(StatusMessage::err(error));
            }
        }
    }
}