lingora-tui 0.4.7

Lingora is a free and open-source localization management program that analyses fluent translation files highlighting discrepancies between reference and target languages. This application provides a terminal user interface; run as `lingora-tui`.
Documentation
use std::collections::HashMap;

use crossterm::event::{Event, KeyCode, KeyEvent};
use rat_event::{HandleEvent, Outcome, Regular};
use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
use ratatui::{prelude::*, widgets::*};
use tui_tree_widget::{Tree, TreeItem, TreeState};

use crate::{
    projections::{
        FilteredLocalesHierarchy, HasSelectionPair, LocaleNode, LocaleNodeId, LocaleNodeKind,
    },
    theme::LingoraTheme,
};

#[derive(Debug)]
pub struct LocaleTreeState {
    focus_flag: FocusFlag,
    tree_state: TreeState<LocaleNodeId>,
    reference: Option<LocaleNodeId>,
    target: Option<LocaleNodeId>,
    area: Rect,
}

impl LocaleTreeState {
    pub fn new(
        reference_node_id: Option<LocaleNodeId>,
        node_ids: impl IntoIterator<Item = LocaleNodeId>,
    ) -> Self {
        let focus_flag = FocusFlag::default();

        let mut tree_state = TreeState::default();
        node_ids.into_iter().for_each(|id| {
            tree_state.open(vec![id]);
        });

        let reference = reference_node_id;
        let target = None;

        let area = Rect::default();

        Self {
            focus_flag,
            tree_state,
            reference,
            target,
            area,
        }
    }

    fn handle_key_event(&mut self, event: &KeyEvent) -> Outcome {
        match &event.code {
            KeyCode::Up => {
                self.tree_state.key_up();
                self.target = self.tree_state.selected().last().copied();
                Outcome::Unchanged
            }
            KeyCode::Down => {
                self.tree_state.key_down();
                self.target = self.tree_state.selected().last().copied();
                Outcome::Unchanged
            }
            KeyCode::Right => {
                self.tree_state.key_right();
                self.target = self.tree_state.selected().last().copied();
                Outcome::Unchanged
            }
            KeyCode::Left => {
                self.tree_state.key_left();
                self.target = self.tree_state.selected().last().copied();
                Outcome::Unchanged
            }
            KeyCode::Char(' ') => {
                self.tree_state.toggle_selected();
                self.reference = self.tree_state.selected().last().copied();
                Outcome::Unchanged
            }
            _ => Outcome::Continue,
        }
    }
}

impl HasSelectionPair for LocaleTreeState {
    type Item = LocaleNodeId;

    fn reference(&self) -> Option<&Self::Item> {
        self.reference.as_ref()
    }

    fn target(&self) -> Option<&Self::Item> {
        self.target.as_ref()
    }
}

impl HasFocus for LocaleTreeState {
    fn build(&self, builder: &mut FocusBuilder) {
        builder.leaf_widget(self);
    }

    fn focus(&self) -> FocusFlag {
        self.focus_flag.clone()
    }

    fn area(&self) -> Rect {
        self.area
    }
}

impl HandleEvent<Event, Regular, Outcome> for LocaleTreeState {
    fn handle(&mut self, event: &Event, _qualifier: Regular) -> Outcome {
        if self.focus_flag.is_focused() {
            match event {
                Event::Key(event) => self.handle_key_event(event),
                _ => Outcome::Continue,
            }
        } else {
            Outcome::Continue
        }
    }
}

pub struct LocaleTree<'a> {
    theme: &'a LingoraTheme,
    filtered_hierarchy: &'a FilteredLocalesHierarchy,
}

impl<'a> LocaleTree<'a> {
    pub fn new(theme: &'a LingoraTheme, filtered_hierarchy: &'a FilteredLocalesHierarchy) -> Self {
        Self {
            theme,
            filtered_hierarchy,
        }
    }

    fn to_tree_item(
        &self,
        id: &LocaleNodeId,
        nodes: &HashMap<LocaleNodeId, LocaleNode>,
    ) -> Option<TreeItem<'_, LocaleNodeId>> {
        let styled = |node: &LocaleNode| {
            let styled = match node.kind() {
                LocaleNodeKind::WorkspaceRoot => Span::from("workspace"),
                LocaleNodeKind::LanguageRoot { language } => {
                    self.theme.language_root_span(language)
                }
                LocaleNodeKind::Locale { locale } => self.theme.locale_span(locale),
            };

            if node.has_issues() {
                styled.patch_style(self.theme.error())
            } else {
                styled
            }
        };

        if let Some(node) = nodes.get(id) {
            if node.has_children() {
                let children = node
                    .children()
                    .filter_map(|id| self.to_tree_item(id, nodes))
                    .collect();
                TreeItem::new(*id, styled(node), children).ok()
            } else {
                Some(TreeItem::new_leaf(*id, styled(node)))
            }
        } else {
            None
        }
    }
}

impl StatefulWidget for LocaleTree<'_> {
    type State = LocaleTreeState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        state.area = area;

        let roots = self
            .filtered_hierarchy
            .roots()
            .filter_map(|id| self.to_tree_item(id, self.filtered_hierarchy.nodes()))
            .collect::<Vec<_>>();

        let tree = Tree::new(&roots)
            .expect("unique locale ids in tree")
            .block(self.theme.focus_block(&state.focus_flag))
            .experimental_scrollbar(Some(
                Scrollbar::new(ScrollbarOrientation::VerticalRight)
                    .begin_symbol(None)
                    .track_symbol(None)
                    .end_symbol(None),
            ))
            .highlight_style(self.theme.selection());

        StatefulWidget::render(tree, area, buf, &mut state.tree_state);
    }
}