yofi 0.2.1

minimalistic menu for wayland
Documentation
use once_cell::sync::OnceCell;
use regex::Regex;

#[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 working_dir: Option<&'a str>,
}

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

enum NextValueKind {
    Args,
    EnvVars,
    WorkingDir,
}

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

fn parse_command_part(input: &str) -> (&str, (&str, Option<NextValueKind>)) {
    let re = SEPARATOR_REGEX
        .get_or_init(|| Regex::new(r"(!!|#|~)").unwrap())
        .clone();
    let Some(cap) = re.captures(input) else {
        return ("", (input, None));
    };
    let m = cap.get(0).unwrap();
    let bang = cap.get(1).unwrap().as_str();
    let kind = match bang {
        "!!" => Some(NextValueKind::Args),
        "#" => Some(NextValueKind::EnvVars),
        "~" => Some(NextValueKind::WorkingDir),
        s => panic!("regex bug: unexpected bang match {}", s),
    };

    (&input[m.end()..], (&input[..m.start()], kind))
}

pub fn parse(source: &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,
        working_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.working_dir = Some(cmd),
        }

        input = left;
        next_kind = new_kind;
    }

    debug_assert!(input.is_empty(), "trailing input: {}", input);

    command
}

#[cfg(test)]
mod tests {
    use super::{parse, 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 "),
        working_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"),
        working_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"),
        working_dir: Some("/run/user/1000"),
        ..InputValue::empty()
    }; "with all params")]
    fn test_parse(input: &str, input_value: InputValue) {
        assert_eq!(
            parse(input),
            InputValue {
                source: input,
                ..input_value
            }
        );
    }

    #[quickcheck]
    fn test_parse_all(input: String) {
        parse(&input);
    }
}