lingora-tui 0.4.14

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::rc::Rc;

use crossterm::event::{Event, KeyCode, KeyEvent};
use lingora_core::prelude::AuditResult;
use rat_event::{ConsumedEvent, HandleEvent, Outcome, Regular};
use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus};
use rat_text::HasScreenCursor;
use ratatui::prelude::*;

use crate::{
    components::{
        Cursor, Entries, EntriesState, Identifiers, IdentifiersState, Issues, IssuesState, Locales,
        LocalesState,
    },
    projections::{
        Comparison, FilteredIssues, HasSelectionPair, LocaleNode, LocaleNodeId, LocalesHierarchy,
    },
    theme::LingoraTheme,
};

#[derive(Debug)]
pub struct TranslationsState {
    focus: Option<Focus>,
    locales_state: LocalesState,
    identifiers_state: IdentifiersState,
    reference_entries_state: EntriesState,
    target_entries_state: EntriesState,
    issues_state: IssuesState,
    comparison: Comparison,
}

impl TranslationsState {
    pub fn new(audit_result: Rc<AuditResult>) -> Self {
        let canonical_locale = audit_result.workspace().canonical_locale();

        let locales_hierachy = LocalesHierarchy::from(&*audit_result);

        let reference_node_id = locales_hierachy
            .node_id_for_locale(canonical_locale)
            .copied();
        let nodes = locales_hierachy.nodes().keys().copied();
        let locales_state = LocalesState::new(reference_node_id, nodes);
        let identifiers_state = IdentifiersState::default();

        let reference_entries_state = EntriesState::default();
        let target_entries_state = EntriesState::default();
        let issues_state = IssuesState::default();

        let comparison =
            Comparison::from_reference(reference_node_id, audit_result, locales_hierachy);

        Self {
            focus: None,
            locales_state,
            identifiers_state,
            reference_entries_state,
            target_entries_state,
            issues_state,
            comparison,
        }
    }

    pub fn rebuild_focus(&mut self) {
        let mut builder = FocusBuilder::new(self.focus.take());
        self.build(&mut builder);
        self.focus = Some(builder.build());
    }

    fn handle_key_event(&mut self, event: &KeyEvent) -> Outcome {
        match event.code {
            KeyCode::Tab => self.focus_next(),
            KeyCode::BackTab => self.focus_prev(),
            _ => Outcome::Continue,
        }
    }

    #[inline]
    fn focus_next(&mut self) -> Outcome {
        let focus = self.focus.as_mut().expect("focus");
        focus.next();
        Outcome::Unchanged
    }

    #[inline]
    fn focus_prev(&mut self) -> Outcome {
        let focus = self.focus.as_mut().expect("focus");
        focus.prev();
        Outcome::Unchanged
    }

    #[inline(always)]
    pub fn locale_filter(&self) -> &str {
        self.locales_state.filter()
    }

    #[inline(always)]
    pub fn locale_node(&self, node_id: &LocaleNodeId) -> Option<&LocaleNode> {
        self.comparison.locale_node(node_id)
    }

    #[inline(always)]
    pub fn identifier_filter(&self) -> &str {
        self.identifiers_state.filter()
    }
}

impl HasSelectionPair for TranslationsState {
    type Item = LocaleNodeId;

    #[inline(always)]
    fn reference(&self) -> Option<&Self::Item> {
        self.locales_state.reference()
    }

    #[inline(always)]
    fn target(&self) -> Option<&Self::Item> {
        self.locales_state.target()
    }
}

impl HasFocus for TranslationsState {
    fn build(&self, builder: &mut FocusBuilder) {
        builder.widget(&self.locales_state);
        builder.widget(&self.identifiers_state);
        builder.widget(&self.reference_entries_state);
        builder.widget(&self.target_entries_state);
        builder.widget(&self.issues_state);
    }

    fn focus(&self) -> FocusFlag {
        unreachable!()
    }

    fn area(&self) -> Rect {
        unreachable!()
    }
}

impl HasScreenCursor for TranslationsState {
    fn screen_cursor(&self) -> Cursor {
        self.locales_state
            .screen_cursor()
            .or_else(|| self.identifiers_state.screen_cursor())
    }
}

impl HandleEvent<Event, Regular, Outcome> for TranslationsState {
    fn handle(&mut self, event: &Event, qualifier: Regular) -> Outcome {
        self.rebuild_focus();

        match event {
            Event::Key(event) => self.handle_key_event(event),
            _ => Outcome::Continue,
        }
        .or_else(|| self.locales_state.handle(event, qualifier))
        .or_else(|| self.identifiers_state.handle(event, qualifier))
        .or_else(|| self.reference_entries_state.handle(event, qualifier))
        .or_else(|| self.target_entries_state.handle(event, qualifier))
        .or_else(|| self.issues_state.handle(event, qualifier))
    }
}

pub struct Translations<'a> {
    theme: &'a LingoraTheme,
    audit_result: &'a AuditResult,
}

impl<'a> Translations<'a> {
    pub fn new(theme: &'a LingoraTheme, audit_result: &'a AuditResult) -> Self {
        Self {
            theme,
            audit_result,
        }
    }
}

impl<'a> StatefulWidget for &Translations<'a> {
    type State = TranslationsState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
    where
        Self: Sized,
    {
        state
            .comparison
            .update_with_reference_and_target(state.reference().copied(), state.target().copied());

        let main_columns = Layout::horizontal(vec![
            Constraint::Percentage(15),
            Constraint::Percentage(20),
            Constraint::Min(0),
        ])
        .split(area);

        let comparison_outer = Layout::vertical(vec![Constraint::Min(0), Constraint::Length(10)])
            .split(main_columns[2]);

        let comparison_inner =
            Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
                .split(comparison_outer[0]);

        Locales::new(self.theme, state.comparison.locales_hierarchy()).render(
            main_columns[0],
            buf,
            &mut state.locales_state,
        );

        Identifiers::new(self.theme, state.comparison.identifiers().cloned()).render(
            main_columns[1],
            buf,
            &mut state.identifiers_state,
        );

        Entries::new(
            self.theme,
            state
                .comparison
                .reference_entries(state.identifiers_state.selected()),
        )
        .render(comparison_inner[0], buf, &mut state.reference_entries_state);

        Entries::new(
            self.theme,
            state
                .comparison
                .target_entries(state.identifiers_state.selected()),
        )
        .render(comparison_inner[1], buf, &mut state.target_entries_state);

        let filtered_issues = FilteredIssues::from_issues(self.audit_result.issues(), state);

        Issues::new(self.theme, filtered_issues.issues().to_owned()).render(
            comparison_outer[1],
            buf,
            &mut state.issues_state,
        );
    }
}