kickoff 0.6.0

Fast and minimal program launcher
Documentation
use crate::history::History;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use log::*;
use nom::{
    branch::alt,
    bytes::complete::is_not,
    character::complete::char,
    combinator::opt,
    sequence::pair,
    sequence::{delimited, preceded},
    Finish, IResult,
};
use std::fs::File;
use std::{
    cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd},
    io::{BufRead, BufReader},
    path::PathBuf,
};
use std::{env, os::unix::fs::PermissionsExt};
use tokio::{
    io::{self, AsyncBufReadExt},
    task::{spawn, spawn_blocking},
};

#[derive(Eq, PartialEq, Debug, Clone)]
pub struct Element {
    pub name: String,
    pub value: String,
    pub base_score: usize,
}

impl Ord for Element {
    fn cmp(&self, other: &Element) -> Ordering {
        match other.base_score.cmp(&self.base_score) {
            Ordering::Equal => self.name.cmp(&other.name),
            e => e,
        }
    }
}

impl PartialOrd for Element {
    fn partial_cmp(&self, other: &Element) -> Option<Ordering> {
        match other.base_score.cmp(&self.base_score) {
            Ordering::Equal => Some(self.name.cmp(&other.name)),
            e => Some(e),
        }
    }
}

#[derive(Debug, Default)]
pub struct ElementList {
    inner: Vec<Element>,
}

impl ElementList {
    pub fn merge_history(&mut self, history: &History) {
        for entry in history.as_vec().iter() {
            if let Some(elem) = self.inner.iter_mut().find(|x| x.name == entry.name) {
                elem.base_score = entry.num_used;
            } else {
                self.inner.push(Element {
                    name: entry.name.to_owned(),
                    value: entry.value.to_owned(),
                    base_score: entry.num_used,
                })
            }
        }
    }

    pub fn sort_score(&mut self) {
        self.inner.sort_by(|a, b| b.base_score.cmp(&a.base_score))
    }

    pub fn search(&self, pattern: &str) -> Vec<&Element> {
        let matcher = SkimMatcherV2::default();
        let mut executables = self
            .inner
            .iter()
            .map(|x| {
                (
                    matcher
                        .fuzzy_match(&x.name, pattern)
                        .map(|score| score + x.base_score as i64),
                    x,
                )
            })
            .filter(|x| x.0.is_some())
            .collect::<Vec<(Option<i64>, &Element)>>();
        executables.sort_by(|a, b| b.0.unwrap_or(0).cmp(&a.0.unwrap_or(0)));
        executables.into_iter().map(|x| x.1).collect()
    }

    pub fn as_ref_vec(&self) -> Vec<&Element> {
        self.inner.iter().collect()
    }
}

#[derive(Debug, Default)]
pub struct ElementListBuilder {
    from_path: bool,
    from_stdin: bool,
    from_file: Vec<PathBuf>,
}

impl ElementListBuilder {
    pub fn new() -> ElementListBuilder {
        ElementListBuilder::default()
    }

    pub fn add_path(&mut self) {
        self.from_path = true;
    }
    pub fn add_files(&mut self, files: &[PathBuf]) {
        self.from_file = files.to_vec();
    }
    pub fn add_stdin(&mut self) {
        self.from_stdin = true;
    }

    pub async fn build(&self) -> Result<ElementList, Box<dyn std::error::Error>> {
        let mut fut = Vec::new();
        if self.from_stdin {
            fut.push(spawn(ElementListBuilder::build_stdin()))
        }
        if !self.from_file.is_empty() {
            let files = self.from_file.clone();
            fut.push(spawn_blocking(move || {
                ElementListBuilder::build_files(&files)
            }))
        }
        if self.from_path {
            fut.push(spawn_blocking(ElementListBuilder::build_path))
        }

        let finished = futures::future::join_all(fut).await;

        let mut res = Vec::new();
        for elements in finished {
            let mut elements = elements??;
            res.append(&mut elements);
        }

        Ok(ElementList { inner: res })
    }

    fn build_files(files: &[PathBuf]) -> Result<Vec<Element>, std::io::Error> {
        let mut res = Vec::new();
        for file in files {
            let mut reader = BufReader::new(File::open(file)?);
            let mut buf = String::new();
            while reader.read_line(&mut buf)? > 0 {
                let kv_pair = match parse_line(&buf) {
                    Ok(None) => continue,
                    Ok(Some(res)) => res,
                    Err(e) => {
                        error!("Failed parsing {}", e);
                        continue;
                    }
                };
                match kv_pair {
                    (key, Some(value)) => res.push(Element {
                        name: key.to_string(),
                        value: value.to_string(),
                        base_score: 0,
                    }),
                    ("", None) => {} // Empty Line
                    (key, None) => res.push(Element {
                        name: key.to_string(),
                        value: key.to_string(),
                        base_score: 0,
                    }),
                }

                buf.clear();
            }
        }

        Ok(res)
    }

    fn build_path() -> Result<Vec<Element>, std::io::Error> {
        let var = env::var("PATH").unwrap();

        let mut res: Vec<Element> = Vec::new();

        let paths_iter = env::split_paths(&var);
        let dirs_iter = paths_iter.filter_map(|path| std::fs::read_dir(path).ok());

        for dir in dirs_iter {
            dir.filter_map(|file| file.ok()).for_each(|file| {
                if let Ok(metadata) = file.metadata() {
                    if !metadata.is_dir() && metadata.permissions().mode() & 0o111 != 0 {
                        let name = file.file_name().to_str().unwrap().to_string();
                        res.push(Element {
                            value: name.clone(),
                            name,
                            base_score: 0,
                        });
                    }
                }
            });
        }

        res.sort();
        res.dedup_by(|a, b| a.name == b.name);

        Ok(res)
    }

    async fn build_stdin() -> Result<Vec<Element>, std::io::Error> {
        let stdin = io::stdin();
        let reader = io::BufReader::new(stdin);
        let mut lines = reader.lines();
        let mut res = Vec::new();

        while let Some(line) = lines.next_line().await? {
            let kv_pair = match parse_line(&line) {
                Ok(None) => continue,
                Ok(Some(res)) => res,
                Err(e) => {
                    error!("Failed parsing {}", e);
                    continue;
                }
            };
            match kv_pair {
                (key, Some(value)) => res.push(Element {
                    name: key.to_string(),
                    value: value.to_string(),
                    base_score: 0,
                }),
                ("", None) => {} // Empty Line
                (key, None) => res.push(Element {
                    name: key.to_string(),
                    value: key.to_string(),
                    base_score: 0,
                }),
            }
        }

        Ok(res)
    }
}

#[allow(clippy::type_complexity)]
fn parse_line<'a>(
    input: &'a str,
) -> Result<Option<(&str, Option<&str>)>, Box<dyn std::error::Error + 'a>> {
    let input = input.trim_end();
    match pair(
        alt((is_not("\"="), quoted_string)),
        opt(preceded(char('='), alt((is_not("\""), quoted_string)))),
    )(input)
    .finish()
    {
        Ok(("", ("", None))) => Ok(None),
        Ok(("", res)) => Ok(Some(res)),
        Ok((unparsed, _res)) => {
            warn!("Input was not fully consumed: {unparsed}");
            Ok(None)
        }
        Err(e) => Err(Box::new(e)),
    }
}

fn quoted_string(input: &str) -> IResult<&str, &str> {
    delimited(char('"'), is_not("\""), char('"'))(input)
}