yofi 0.2.1

minimalistic menu for wayland
Documentation
use std::cmp::Reverse;
use std::collections::HashMap;
use std::ffi::CString;
use std::os::unix::fs::PermissionsExt;

use anyhow::{Context, Result};

use super::{Entry, EvalInfo};
use crate::usage_cache::Usage;

const CACHE_PATH: &str = concat!(crate::prog_name!(), ".bincache");

#[derive(PartialEq, Eq, Hash)]
struct Binary {
    path: String,
    fname: String,
}

pub struct BinsMode {
    bins: Vec<Binary>,
    entry_name_cache: HashMap<String, String>,
    term: Vec<CString>,
    usage: Usage,
}

impl BinsMode {
    pub fn new(term: Vec<CString>) -> Self {
        let usage = Usage::from_path(CACHE_PATH);

        let paths = std::env::var("PATH")
            .map(|paths| paths.split(':').map(|s| s.to_owned()).collect())
            .unwrap_or_else(|_| vec!["/usr/bin".into()]);
        let mut bins: Vec<_> = paths
            .into_iter()
            .flat_map(|p| std::fs::read_dir(&p).map_err(|e| log::warn!("failed to read {p}: {e}")))
            .flatten()
            .flatten()
            .filter_map(|f| {
                let meta = f.metadata().ok()?;
                if f.path().is_file() && meta.permissions().mode() & 0o001 > 0 {
                    let p = f.path();
                    Some(Binary {
                        path: p.to_str()?.to_owned(),
                        fname: p.file_name()?.to_str()?.to_owned(),
                    })
                } else {
                    None
                }
            })
            .collect();

        bins.sort_by(|x, y| {
            let x_usage_count = usage.entry_count(&x.path);
            let y_usage_count = usage.entry_count(&y.path);

            Reverse(x_usage_count)
                .cmp(&Reverse(y_usage_count))
                .then_with(|| x.path.cmp(&y.path))
        });
        bins.dedup();

        let mut fname_counts = HashMap::<_, u8>::new();

        for b in &bins {
            let count = fname_counts.entry(b.fname.clone()).or_default();
            *count = count.saturating_add(1);
        }

        let mut entry_name_cache = HashMap::new();

        for bin in &bins {
            if fname_counts
                .get(&bin.fname)
                .filter(|&&cnt| cnt > 1)
                .is_none()
            {
                log::warn!("file name {} found multiple times, skipping", bin.fname);
                continue;
            }

            entry_name_cache.insert(bin.path.clone(), format!("{} ({})", bin.fname, bin.path));
        }

        Self {
            bins,
            entry_name_cache,
            term,
            usage,
        }
    }

    pub fn eval(&mut self, info: EvalInfo<'_>) -> Result<std::convert::Infallible> {
        let binary = if let Some(idx) = info.index {
            self.bins[idx].path.as_str()
        } else {
            info.search_string
        };

        self.usage.increment_entry_usage(binary.to_string());
        self.usage.try_update_cache(CACHE_PATH);

        crate::exec::exec(
            Some(std::mem::take(&mut self.term)),
            std::iter::once(CString::new(binary).context("invalid binary name")?),
            info.input_value,
        )
    }

    pub fn entries_len(&self) -> usize {
        self.bins.len()
    }

    pub fn subentries_len(&self, _: usize) -> usize {
        0
    }

    pub fn entry(&self, idx: usize, _: usize) -> Entry<'_> {
        let bin = &self.bins[idx];

        let name = if let Some(name) = self.entry_name_cache.get(&bin.path) {
            name.as_str()
        } else {
            bin.fname.as_str()
        };

        Entry {
            name,
            subname: None,
            icon: None,
        }
    }

    pub fn text_entries(&self) -> impl super::ExactSizeIterator<Item = &str> {
        self.bins.iter().map(|e| e.fname.as_str())
    }
}