yuru 0.1.8

A fast phonetic fuzzy finder for the shell
use anyhow::{bail, Context, Result};

use crate::{
    cli::{Args, FzfCompatArg, ZhPolyphoneArg, ZhScriptArg},
    options::has_unsupported_bindings,
};

pub(crate) fn enforce_fzf_compat(args: &Args) -> Result<()> {
    let mode = effective_fzf_compat(args)?;
    let ignored = ignored_fzf_options(args);
    if ignored.is_empty() || mode == FzfCompatArg::Ignore {
        return Ok(());
    }

    match mode {
        FzfCompatArg::Strict => {
            bail!(
                "unsupported fzf option(s): {}. Use --fzf-compat=warn or --fzf-compat=ignore to allow them",
                ignored.join(", ")
            );
        }
        FzfCompatArg::Warn => {
            for option in ignored {
                eprintln!("yuru: warning: ignoring unsupported fzf option {option}");
            }
        }
        FzfCompatArg::Ignore => {}
    }

    Ok(())
}

pub(crate) fn warn_reserved_zh_options(args: &Args) {
    if args.zh_polyphone == ZhPolyphoneArg::Phrase {
        eprintln!(
            "yuru: warning: --zh-polyphone=phrase is not implemented yet; using common polyphone expansion"
        );
    }
    if args.zh_script != ZhScriptArg::Auto {
        eprintln!("yuru: warning: --zh-script is reserved and currently has no effect");
    }
}

#[cfg(test)]
pub(crate) fn accepted_fzf_option_count(args: &Args) -> usize {
    macro_rules! count_bool {
        ($($field:ident),* $(,)?) => {
            0 $(+ usize::from(args.$field))*
        };
    }
    macro_rules! count_opt {
        ($($field:ident),* $(,)?) => {
            0 $(+ usize::from(args.$field.is_some()))*
        };
    }

    count_bool!(
        no_exact,
        extended_exact,
        no_extended,
        ignore_case,
        no_ignore_case,
        smart_case,
        no_sort,
        disabled,
        phony,
        enabled,
        no_phony,
        literal,
        no_literal,
        tac,
        no_tac,
        no_tail,
        read0,
        no_read0,
        sync,
        no_sync,
        print0,
        no_print0,
        ansi,
        no_ansi,
        print_query,
        no_print_query,
        select_1,
        no_select_1,
        exit_0,
        no_exit_0,
        no_multi,
        no_expect,
        no_preview,
        preview_auto,
        no_preview_border,
        no_height,
        no_popup,
        no_tmux,
        reverse,
        no_reverse,
        no_margin,
        no_padding,
        no_border,
        no_border_label,
        no_header,
        no_header_lines,
        header_first,
        no_header_first,
        no_header_border,
        no_header_lines_border,
        no_header_label,
        no_footer,
        no_footer_border,
        no_footer_label,
        no_color,
        no_256,
        bold,
        no_bold,
        black,
        no_black,
        cycle,
        no_cycle,
        highlight_line,
        no_highlight_line,
        no_wrap,
        wrap_word,
        no_wrap_word,
        multi_line,
        no_multi_line,
        raw,
        no_raw,
        track,
        no_track,
        no_id_nth,
        no_gap,
        no_gap_line,
        keep_right,
        no_keep_right,
        no_hscroll,
        hscroll,
        no_scrollbar,
        no_list_border,
        no_list_label,
        no_input,
        no_info_command,
        no_info,
        inline_info,
        no_inline_info,
        no_separator,
        filepath_word,
        no_filepath_word,
        no_input_border,
        no_input_label,
        no_listen,
        no_listen_unsafe,
        no_history,
        no_tty_default,
        force_tty_in,
        no_force_tty_in,
        no_winpty,
        no_mouse,
        no_unicode,
        unicode,
        ambidouble,
        no_ambidouble,
        clear,
        no_clear,
        man,
    ) + count_opt!(
        sort,
        tail,
        expect,
        toggle_sort,
        preview,
        preview_text_extensions,
        preview_window,
        preview_border,
        preview_label,
        preview_label_pos,
        preview_wrap_sign,
        height,
        min_height,
        popup,
        tmux,
        layout,
        margin,
        padding,
        border,
        border_label,
        border_label_pos,
        prompt,
        header,
        header_lines,
        header_border,
        header_lines_border,
        header_label,
        header_label_pos,
        footer,
        footer_border,
        footer_label,
        footer_label_pos,
        wrap,
        wrap_sign,
        id_nth,
        gap,
        gap_line,
        freeze_left,
        freeze_right,
        scroll_off,
        hscroll_off,
        jump_labels,
        gutter,
        gutter_raw,
        pointer,
        marker,
        marker_multi_line,
        ellipsis,
        tabstop,
        scrollbar,
        list_border,
        list_label,
        list_label_pos,
        info,
        info_command,
        separator,
        ghost,
        input_border,
        input_label,
        input_label_pos,
        with_shell,
        style,
        listen,
        listen_unsafe,
        history,
        history_size,
        tty_default,
        proxy_script,
        threads,
        bench,
        profile_cpu,
        profile_mem,
        profile_block,
        profile_mutex,
    ) + args.bind.len()
        + args.color.len()
}

fn effective_fzf_compat(args: &Args) -> Result<FzfCompatArg> {
    if let Some(mode) = args.fzf_compat {
        return Ok(mode);
    }

    match std::env::var("YURU_FZF_COMPAT") {
        Ok(value) => parse_fzf_compat_env(&value),
        Err(std::env::VarError::NotPresent) => Ok(FzfCompatArg::Warn),
        Err(error) => Err(error).context("failed to read YURU_FZF_COMPAT"),
    }
}

fn parse_fzf_compat_env(value: &str) -> Result<FzfCompatArg> {
    match value.trim() {
        "strict" => Ok(FzfCompatArg::Strict),
        "warn" => Ok(FzfCompatArg::Warn),
        "ignore" => Ok(FzfCompatArg::Ignore),
        other => bail!("unsupported YURU_FZF_COMPAT value: {other}"),
    }
}

fn ignored_fzf_options(args: &Args) -> Vec<&'static str> {
    let mut out = Vec::new();

    if has_unsupported_bindings(&args.bind) {
        out.push("--bind");
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;

    #[test]
    fn accepted_fzf_options_are_counted() {
        let args = Args::parse_from([
            "yuru",
            "--extended-exact",
            "--no-exact",
            "--scheme",
            "path",
            "--bind",
            "ctrl-j:preview-down",
            "--preview",
            "cat {}",
            "--height",
            "40%",
            "--multi=3",
        ]);

        assert!(accepted_fzf_option_count(&args) > 0);
    }
}