jnv 0.7.1

JSON navigator and interactive filter leveraging jq
use arboard::Clipboard;
use promkit_widgets::{
    core::{render::SharedRenderer, Widget},
    status::{self, Severity},
};
use tokio::{sync::mpsc, task::JoinHandle};

use crate::context::{Index, SharedContext};

/// Represent a message to be shown in the guide.
/// This is used to decouple the logic of generating messages from the logic of rendering them.
pub enum GuideMessage {
    CopiedToClipboard,
    FailedToCopyToClipboard(String),
    FailedToSetupClipboard(String),
    FailedToCopyWhileRenderingInProgress,
    FailedToSwitchModeWhileRenderingInProgress,
    LoadedAllSuggestions(usize),
    LoadedPartiallySuggestions(usize),
    NoSuggestionFound(String),
    JqReturnedNull(String),
    JqFailed(String),
}

/// Represent an action to be performed on the guide.
pub enum GuideAction {
    Clear,
    Show(GuideMessage),
}

fn message_to_state(message: GuideMessage) -> status::State {
    match message {
        GuideMessage::CopiedToClipboard => {
            status::State::new("Copied to clipboard", Severity::Success)
        }
        GuideMessage::FailedToCopyToClipboard(e) => {
            status::State::new(format!("Failed to copy to clipboard: {e}"), Severity::Error)
        }
        GuideMessage::FailedToSetupClipboard(e) => {
            status::State::new(format!("Failed to setup clipboard: {e}"), Severity::Error)
        }
        GuideMessage::FailedToCopyWhileRenderingInProgress => status::State::new(
            "Failed to copy while rendering is in progress.",
            Severity::Warning,
        ),
        GuideMessage::FailedToSwitchModeWhileRenderingInProgress => status::State::new(
            "Failed to switch mode while rendering is in progress.",
            Severity::Warning,
        ),
        GuideMessage::LoadedAllSuggestions(count) => status::State::new(
            format!("Loaded all ({count}) suggestions"),
            Severity::Success,
        ),
        GuideMessage::LoadedPartiallySuggestions(count) => status::State::new(
            format!("Loaded partially ({count}) suggestions"),
            Severity::Success,
        ),
        GuideMessage::NoSuggestionFound(prefix) => status::State::new(
            format!("No suggestion found for '{prefix}'"),
            Severity::Warning,
        ),
        GuideMessage::JqReturnedNull(input) => status::State::new(
            format!("jq returned 'null', which may indicate a typo or incorrect filter: `{input}`"),
            Severity::Warning,
        ),
        GuideMessage::JqFailed(e) => {
            status::State::new(format!("jq failed: `{e}`"), Severity::Error)
        }
    }
}

/// Copy the given content to the clipboard and return a message indicating the result.
pub fn copy_to_clipboard_message(content: &str) -> GuideMessage {
    match Clipboard::new() {
        Ok(mut clipboard) => match clipboard.set_text(content) {
            Ok(_) => GuideMessage::CopiedToClipboard,
            Err(e) => GuideMessage::FailedToCopyToClipboard(e.to_string()),
        },
        Err(e) => GuideMessage::FailedToSetupClipboard(e.to_string()),
    }
}

/// Spawn a task that listens for guide actions and updates the guide view accordingly.
pub fn start_guide_task(
    mut action_rx: mpsc::Receiver<GuideAction>,
    shared_renderer: SharedRenderer<Index>,
    shared_ctx: SharedContext,
    no_hint: bool,
) -> JoinHandle<anyhow::Result<()>> {
    tokio::spawn(async move {
        loop {
            tokio::select! {
                Some(action) = action_rx.recv() => {
                    let area = shared_ctx.area().await;
                    let view = if no_hint {
                        Default::default()
                    } else {
                        match action {
                            GuideAction::Clear => status::State::default().create_graphemes(area.0, area.1),
                            GuideAction::Show(message) => message_to_state(message).create_graphemes(area.0, area.1),
                        }
                    };
                    shared_renderer.update([(Index::Guide, view)]).render().await?;
                }
                else => break,
            }
        }
        Ok(())
    })
}