clankerdiff-ratatui 0.1.2

Embeddable Ratatui diff review widget
Documentation
use super::{MarkdownReviewEvent, MarkdownReviewState, MarkdownReviewWidget};
use crate::{
    InputOutcome, KeyBinding, KeyEvent, MarkdownReviewCommand, NavigationPane, ReviewInput,
    ReviewOptions, ThemeChoice, default_markdown_keybindings, keybindings::markdown_command_label,
};
use clankerdiff_core::ReviewCapabilities;
use clankerdiff_markdown::{MarkdownDocument, MarkdownReviewError};
use clankerdiff_theme::ReviewTheme;
#[cfg(feature = "crossterm-backend")]
use crossterm::event::Event;
use ratatui::{Frame, layout::Rect};
use std::sync::Arc;

#[derive(Debug)]
pub struct MarkdownReview {
    state: MarkdownReviewState,
    widget: MarkdownReviewWidget,
}

impl MarkdownReview {
    #[must_use]
    pub fn builder() -> MarkdownReviewBuilder {
        MarkdownReviewBuilder::default()
    }

    pub fn render(&mut self, frame: &mut Frame<'_>, area: Rect) {
        frame.render_stateful_widget(self.widget.clone(), area, &mut self.state);
        if let Some(position) = self.state.cursor_position() {
            frame.set_cursor_position(position);
        }
    }

    pub fn handle_input(
        &mut self,
        input: impl Into<ReviewInput>,
    ) -> Result<InputOutcome<MarkdownReviewEvent>, MarkdownReviewError> {
        self.state.handle_input(input.into())
    }

    pub fn handle_command(
        &mut self,
        command: impl Into<MarkdownReviewCommand>,
    ) -> Result<InputOutcome<MarkdownReviewEvent>, MarkdownReviewError> {
        self.state.handle_command(command)
    }

    #[cfg(feature = "crossterm-backend")]
    pub fn handle_crossterm_event(
        &mut self,
        event: &Event,
    ) -> Result<InputOutcome<MarkdownReviewEvent>, MarkdownReviewError> {
        super::handle_crossterm_event(&mut self.state, event.clone())
    }

    #[must_use]
    pub const fn state(&self) -> &MarkdownReviewState {
        &self.state
    }

    pub fn state_mut(&mut self) -> &mut MarkdownReviewState {
        self.state.mark_dirty();
        &mut self.state
    }

    #[must_use]
    pub const fn is_dirty(&self) -> bool {
        self.state.is_dirty()
    }

    pub fn mark_dirty(&mut self) {
        self.state.mark_dirty();
    }

    pub fn set_document(&mut self, document: impl Into<Arc<MarkdownDocument>>) {
        self.state.set_document(document.into());
    }

    pub fn set_theme(&mut self, theme: ReviewTheme) {
        self.state.set_theme(theme);
    }
}

#[derive(Debug)]
pub struct MarkdownReviewBuilder<T = ()> {
    document: T,
    theme: ReviewTheme,
    theme_choices: Arc<[ThemeChoice]>,
    options: ReviewOptions,
    capabilities: ReviewCapabilities,
    keybindings: Vec<KeyBinding<MarkdownReviewCommand>>,
    widget: MarkdownReviewWidget,
}

impl Default for MarkdownReviewBuilder {
    fn default() -> Self {
        Self {
            document: (),
            theme: ReviewTheme::default(),
            theme_choices: Arc::from([]),
            options: ReviewOptions::default(),
            capabilities: ReviewCapabilities::default(),
            keybindings: default_markdown_keybindings(),
            widget: MarkdownReviewWidget::new(),
        }
    }
}

impl<T> MarkdownReviewBuilder<T> {
    #[must_use]
    pub fn markdown(self, source: impl AsRef<str>) -> MarkdownReviewBuilder<Arc<MarkdownDocument>> {
        self.document(MarkdownDocument::parse(source.as_ref()))
    }

    #[must_use]
    pub fn document(
        self,
        document: impl Into<Arc<MarkdownDocument>>,
    ) -> MarkdownReviewBuilder<Arc<MarkdownDocument>> {
        MarkdownReviewBuilder {
            document: document.into(),
            theme: self.theme,
            theme_choices: self.theme_choices,
            options: self.options,
            capabilities: self.capabilities,
            keybindings: self.keybindings,
            widget: self.widget,
        }
    }

    #[must_use]
    pub fn embedded(self) -> Self {
        self.borders(false)
            .footer(false)
            .navigation(NavigationPane::Hidden)
    }

    #[must_use]
    pub fn theme(mut self, theme: ReviewTheme) -> Self {
        self.theme = theme;
        self
    }

    #[must_use]
    pub fn theme_choices(mut self, themes: impl Into<Arc<[ThemeChoice]>>) -> Self {
        self.theme_choices = themes.into();
        self
    }

    #[must_use]
    pub fn options(mut self, options: ReviewOptions) -> Self {
        self.options = options;
        self
    }

    #[must_use]
    pub fn footer(mut self, visible: bool) -> Self {
        self.options.footer = visible;
        self
    }

    #[must_use]
    pub fn navigation(mut self, navigation: NavigationPane) -> Self {
        self.options.navigation = navigation;
        self
    }

    #[must_use]
    pub fn borders(mut self, visible: bool) -> Self {
        self.widget = self.widget.borders(visible);
        self
    }

    #[must_use]
    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.widget = self.widget.title(title);
        self
    }

    #[must_use]
    pub fn capabilities(mut self, capabilities: ReviewCapabilities) -> Self {
        self.capabilities = capabilities;
        self
    }

    #[must_use]
    pub fn bind(self, key: impl Into<KeyEvent>, command: impl Into<MarkdownReviewCommand>) -> Self {
        let command = command.into();
        self.binding(KeyBinding::new(
            key.into(),
            command,
            markdown_command_label(command),
        ))
    }

    #[must_use]
    pub fn binding(mut self, binding: KeyBinding<MarkdownReviewCommand>) -> Self {
        self.keybindings.push(binding);
        self
    }

    #[must_use]
    pub fn keybindings(
        mut self,
        bindings: impl Into<Vec<KeyBinding<MarkdownReviewCommand>>>,
    ) -> Self {
        self.keybindings = bindings.into();
        self
    }
}

impl MarkdownReviewBuilder<Arc<MarkdownDocument>> {
    #[must_use]
    pub fn build(self) -> MarkdownReview {
        let mut state = MarkdownReviewState::with_theme(self.document, self.theme);
        state.set_options(self.options);
        state.set_capabilities(self.capabilities);
        state.set_keybindings(self.keybindings);
        state.set_theme_choices(self.theme_choices);
        MarkdownReview {
            state,
            widget: self.widget,
        }
    }
}