akg 0.5.2

Fast package manager for Termux
// Триггеры dpkg: interest/activate из info/*.triggers, запуск postinst triggered.

use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fs;

use crate::db::{field, Cfg, StatusDb};
use crate::install::run_script;
use crate::util::*;

#[derive(Default)]
struct Spec {
    interests: Vec<String>,
    activates: Vec<String>,
}

fn parse(text: &str) -> Spec {
    let mut s = Spec::default();
    for line in text.lines() {
        let line = line.split('#').next().unwrap_or("").trim();
        let mut it = line.split_whitespace();
        let Some(first) = it.next() else { continue };
        match (first, it.next()) {
            ("interest" | "interest-await" | "interest-noawait", Some(n)) => {
                s.interests.push(n.to_string())
            }
            ("activate" | "activate-await" | "activate-noawait", Some(n)) => {
                s.activates.push(n.to_string())
            }
            (n, None) if !n.starts_with("interest") && !n.starts_with("activate") => {
                s.interests.push(n.to_string())
            }
            _ => {}
        }
    }
    s
}

// Сработал ли файловый триггер: затронут путь внутри target (или файл ровно по target).
fn hit(touched: &[(String, bool)], target: &str) -> bool {
    let t = target.trim_end_matches('/');
    touched.iter().any(|(p, is_dir)| {
        if p.len() > t.len() {
            p.starts_with(t) && p.as_bytes()[t.len()] == b'/'
        } else {
            !*is_dir && p == t
        }
    })
}

pub fn run(cfg: &Cfg, db: &StatusDb, configured: &HashSet<String>, touched: &[(String, bool)]) {
    let Ok(rd) = fs::read_dir(cfg.info()) else {
        return;
    };
    let mut specs: BTreeMap<String, Spec> = BTreeMap::new();
    for e in rd.flatten() {
        let n = e.file_name().to_string_lossy().into_owned();
        let Some(pkg) = n.strip_suffix(".triggers") else {
            continue;
        };
        let Ok(text) = fs::read_to_string(e.path()) else {
            continue;
        };
        specs.insert(pkg.to_string(), parse(&text));
    }
    if specs.is_empty() {
        return;
    }

    // явные триггеры, которые активировали только что настроенные пакеты
    let mut named: BTreeSet<&str> = BTreeSet::new();
    for p in configured {
        if let Some(s) = specs.get(p) {
            for a in &s.activates {
                named.insert(a.as_str());
            }
        }
    }

    let mut pending: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
    for (pkg, s) in &specs {
        if configured.contains(pkg) || db.status_of(pkg).as_deref() != Some("installed") {
            continue;
        }
        for t in &s.interests {
            let fire = if t.starts_with('/') {
                hit(touched, t)
            } else {
                named.contains(t.as_str())
            };
            if fire {
                pending.entry(pkg.clone()).or_default().insert(t.clone());
            }
        }
    }

    for (pkg, names) in &pending {
        let script = cfg.info().join(format!("{pkg}.postinst"));
        if !script.exists() {
            continue;
        }
        let arch = db
            .get(pkg)
            .and_then(|s| field(s, "Architecture"))
            .unwrap_or_default();
        let list = names.iter().cloned().collect::<Vec<_>>().join(" ");
        println!("  {} {} ({})", paint("36", "trigger"), pkg, list);
        warn(run_script(
            cfg,
            pkg,
            &arch,
            &script,
            "postinst",
            &["triggered", list.as_str()],
        ));
    }
}