yofi 0.2.0

minimalistic menu for wayland
use nom::{
    error::{Error, ErrorKind},
    IResult,
};
use once_cell::sync::OnceCell;

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct InputValue<'a> {
    pub source: &'a str,
    pub search_string: &'a str,
    pub args: Option<&'a str>,
    pub env_vars: Option<&'a str>,
    pub workind_dir: Option<&'a str>,
}

impl InputValue<'static> {
    pub fn empty() -> Self {
        InputValue {
            source: "",
            search_string: "",
            args: None,
            env_vars: None,
            workind_dir: None,
        }
    }
}

enum NextValueKind {
    Args,
    EnvVars,
    WorkingDir,
}

static SEPARATOR_REGEX: OnceCell<regex::Regex> = OnceCell::new();

fn parse_command_part(input: &str) -> IResult<&str, (&str, Option<NextValueKind>)> {
    let re = SEPARATOR_REGEX
        .get_or_init(|| regex::Regex::new(r"(.*?)(!!|#|~)").unwrap())
        .clone();
    let res = match nom_regex::str::re_capture::<Error<_>>(re)(input) {
        Ok((left, matches)) => {
            let parsed = matches[1];
            let kind = match matches[2] {
                "!!" => Some(NextValueKind::Args),
                "#" => Some(NextValueKind::EnvVars),
                "~" => Some(NextValueKind::WorkingDir),
                _ => None,
            };
            (left, (parsed, kind))
        }
        Err(nom::Err::Error(Error {
            input,
            code: ErrorKind::RegexpCapture,
        })) => ("", (input, None)),
        Err(err) => return Err(err),
    };
    Ok(res)
}

pub fn parser(source: &str) -> IResult<&str, InputValue<'_>> {
    let (mut input, (search_string, mut next_kind)) = parse_command_part(source)?;
    let mut command = InputValue {
        source,
        search_string,
        args: None,
        env_vars: None,
        workind_dir: None,
    };

    while let Some(kind) = next_kind.take() {
        let (left, (cmd, new_kind)) = parse_command_part(input)?;

        match kind {
            NextValueKind::Args => command.args = Some(cmd),
            NextValueKind::EnvVars => command.env_vars = Some(cmd),
            NextValueKind::WorkingDir => command.workind_dir = Some(cmd),
        }

        input = left;
        next_kind = new_kind;
    }

    Ok((input, command))
}

#[cfg(test)]
mod tests {
    use super::{parser, InputValue};

    use quickcheck_macros::quickcheck;
    use test_case::test_case;

    #[test_case("", InputValue::empty(); "empty string")]
    #[test_case("qwdqwd asd asd", InputValue {
        search_string: "qwdqwd asd asd",
        ..InputValue::empty()
    }; "search string")]
    #[test_case("qwdqwd asd asd", InputValue {
        search_string: "qwdqwd asd asd",
        ..InputValue::empty()
    }; "search string with exact prefix")]
    #[test_case("qwdqwd!!asd#dsa", InputValue {
        search_string: "qwdqwd",
        args: Some("asd"),
        env_vars: Some("dsa"),
        ..InputValue::empty()
    }; "search string with args then env")]
    #[test_case("qwdqwd#dsa!!asd", InputValue {
        search_string: "qwdqwd",
        args: Some("asd"),
        env_vars: Some("dsa"),
        ..InputValue::empty()
    }; "search string with env then args")]
    #[test_case("qwdqwd~zx,c#qwe !!asd", InputValue {
        search_string: "qwdqwd",
        args: Some("asd"),
        env_vars: Some("qwe "),
        workind_dir: Some("zx,c"),
        ..InputValue::empty()
    }; "search string with working dir then env then args")]
    #[test_case("#qwe~zx,c!!asd", InputValue {
        search_string: "",
        args: Some("asd"),
        env_vars: Some("qwe"),
        workind_dir: Some("zx,c"),
        ..InputValue::empty()
    }; "all but search string")]
    #[test_case("ffx!!--new-instance#MOZ_ENABLE_WAYLAND=1~/run/user/1000", InputValue {
        search_string: "ffx",
        args: Some("--new-instance"),
        env_vars: Some("MOZ_ENABLE_WAYLAND=1"),
        workind_dir: Some("/run/user/1000"),
        ..InputValue::empty()
    }; "with all params")]
    fn test_parse(input: &str, input_value: InputValue) {
        assert_eq!(
            parser(input),
            Ok((
                "",
                InputValue {
                    source: input,
                    ..input_value
                }
            ))
        );
    }

    #[quickcheck]
    fn test_parse_all(input: String) {
        let (left, _) = parser(&input).unwrap();
        assert_eq!(left, "");
    }
}