tui-canvas 0.8.10

Form/textarea/input for TUI
Documentation
use std::collections::HashSet;

use toml::Value;

use crate::canvas::modes::AppMode;

use super::{
    BuiltinCanvasKeybindingPreset, CanvasKeyAction, CanvasKeyBindingEntry, CanvasKeyBindings,
    CanvasKeybindingPresetError, CanvasKeybindingPresetIssue, display_binding,
};

#[derive(Clone, Debug)]
pub struct CanvasKeybindingProfile {
    preset: BuiltinCanvasKeybindingPreset,
    defaults: CanvasKeyBindings,
    current: CanvasKeyBindings,
}

struct OverrideSection {
    mode: AppMode,
    bindings: Vec<(CanvasKeyAction, Vec<String>)>,
}

impl CanvasKeybindingProfile {
    pub fn new(preset: BuiltinCanvasKeybindingPreset) -> Self {
        let defaults = CanvasKeyBindings::from_builtin_preset(preset);
        Self {
            preset,
            current: defaults.clone(),
            defaults,
        }
    }

    pub fn with_overrides_toml(
        preset: BuiltinCanvasKeybindingPreset,
        source: &str,
    ) -> Result<Self, CanvasKeybindingPresetError> {
        let mut profile = Self::new(preset);
        profile.apply_overrides_toml(source)?;
        Ok(profile)
    }

    pub fn preset(&self) -> BuiltinCanvasKeybindingPreset {
        self.preset
    }

    pub fn defaults(&self) -> &CanvasKeyBindings {
        &self.defaults
    }

    pub fn current(&self) -> &CanvasKeyBindings {
        &self.current
    }

    pub fn current_mut(&mut self) -> &mut CanvasKeyBindings {
        &mut self.current
    }

    pub fn entries(&self) -> Vec<CanvasKeyBindingEntry> {
        self.current.entries()
    }

    pub fn remap_action(
        &mut self,
        mode: AppMode,
        action: CanvasKeyAction,
        bindings: Vec<String>,
    ) -> Result<(), CanvasKeybindingPresetError> {
        self.current.remap_action(mode, action, bindings)
    }

    pub fn apply_overrides_toml(
        &mut self,
        source: &str,
    ) -> Result<(), CanvasKeybindingPresetError> {
        for section in parse_override_toml(source)? {
            for (action, bindings) in section.bindings {
                self.current.remap_action(section.mode, action, bindings)?;
            }
        }
        Ok(())
    }

    pub fn overrides_toml(&self) -> String {
        let mut out = String::new();
        for mode in [AppMode::Nor, AppMode::Ins, AppMode::Sel] {
            let mut lines = Vec::new();
            let mut actions = self.actions_for_mode(mode);
            actions.sort_by(|left, right| left.as_str().cmp(right.as_str()));
            actions.dedup();

            for action in actions {
                let default = binding_set(self.defaults.bindings_for(mode, &action));
                let current = binding_set(self.current.bindings_for(mode, &action));
                if default == current {
                    continue;
                }

                let mut display = current
                    .iter()
                    .map(|sequence| display_binding(sequence))
                    .collect::<Vec<_>>();
                display.sort();
                let value = if display.is_empty() {
                    "[]".to_string()
                } else {
                    format!(
                        "[{}]",
                        display
                            .into_iter()
                            .map(|binding| format!("{:?}", binding))
                            .collect::<Vec<_>>()
                            .join(", ")
                    )
                };
                lines.push(format!("{} = {}", action.as_str(), value));
            }

            if lines.is_empty() {
                continue;
            }
            if !out.is_empty() {
                out.push('\n');
            }
            out.push_str(&format!("[{}]\n", mode_section_name(mode)));
            for line in lines {
                out.push_str(&line);
                out.push('\n');
            }
        }
        out
    }

    fn actions_for_mode(&self, mode: AppMode) -> Vec<CanvasKeyAction> {
        self.defaults
            .entries()
            .into_iter()
            .chain(self.current.entries())
            .filter(|entry| entry.mode == mode)
            .map(|entry| entry.action)
            .collect()
    }
}

fn parse_override_toml(source: &str) -> Result<Vec<OverrideSection>, CanvasKeybindingPresetError> {
    if source.trim().is_empty() {
        return Ok(Vec::new());
    }

    let value = toml::from_str::<Value>(source).map_err(CanvasKeybindingPresetError::Toml)?;
    let Some(table) = value.as_table() else {
        return Err(CanvasKeybindingPresetError::Issues(vec![
            CanvasKeybindingPresetIssue::RootNotTable,
        ]));
    };

    let mut sections = Vec::new();
    let mut issues = Vec::new();
    for (section_name, section_value) in table {
        let Some(section) = section_value.as_table() else {
            issues.push(CanvasKeybindingPresetIssue::SectionNotTable {
                section: section_name.clone(),
            });
            continue;
        };

        let mode_name = match section.get("mode") {
            Some(value) => value.as_str().unwrap_or_else(|| {
                issues.push(CanvasKeybindingPresetIssue::ModeNotString {
                    section: section_name.clone(),
                });
                section_name.as_str()
            }),
            None => section_name.as_str(),
        };

        let Ok(mode) = mode_name.parse::<AppMode>() else {
            issues.push(CanvasKeybindingPresetIssue::UnknownMode {
                section: section_name.clone(),
                mode: mode_name.to_string(),
            });
            continue;
        };

        let mut bindings = Vec::new();
        for (action_name, bindings_value) in section {
            if action_name == "mode" {
                continue;
            }
            let action = CanvasKeyAction::from_name(action_name);
            if matches!(action, CanvasKeyAction::Unknown(_)) {
                issues.push(CanvasKeybindingPresetIssue::UnknownAction {
                    section: section_name.clone(),
                    action: action_name.clone(),
                });
                continue;
            }
            let Some(keys) =
                parse_override_string_list(section_name, action_name, bindings_value, &mut issues)
            else {
                continue;
            };
            bindings.push((action, keys));
        }

        sections.push(OverrideSection { mode, bindings });
    }

    if issues.is_empty() {
        Ok(sections)
    } else {
        Err(CanvasKeybindingPresetError::Issues(issues))
    }
}

fn parse_override_string_list(
    section: &str,
    action: &str,
    value: &Value,
    issues: &mut Vec<CanvasKeybindingPresetIssue>,
) -> Option<Vec<String>> {
    if let Some(text) = value.as_str() {
        return Some(vec![text.to_string()]);
    }
    let Some(values) = value.as_array() else {
        issues.push(CanvasKeybindingPresetIssue::BindingsNotStringList {
            section: section.to_string(),
            action: action.to_string(),
        });
        return None;
    };

    let mut out = Vec::new();
    for value in values {
        let Some(text) = value.as_str() else {
            issues.push(CanvasKeybindingPresetIssue::BindingsNotStringList {
                section: section.to_string(),
                action: action.to_string(),
            });
            return None;
        };
        out.push(text.to_string());
    }
    Some(out)
}

fn binding_set(bindings: Vec<Vec<super::KeyStroke>>) -> HashSet<Vec<super::KeyStroke>> {
    bindings.into_iter().collect()
}

fn mode_section_name(mode: AppMode) -> &'static str {
    mode.as_str()
}