use std::path::PathBuf;
use std::sync::mpsc::{self};
#[cfg(not(test))]
use std::time::Duration;
use crate::app::App;
const MIN_INTERVAL_SECS: u64 = 30;
const MAX_INTERVAL_SECS: u64 = 3600;
const DEFAULT_INTERVAL_SECS: u64 = 300;
const FRESHNESS_MULTIPLIER: u32 = 3;
fn clamp_interval(v: Option<u64>) -> u64 {
v.unwrap_or(DEFAULT_INTERVAL_SECS)
.clamp(MIN_INTERVAL_SECS, MAX_INTERVAL_SECS)
}
pub fn prefetch_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(|h| {
PathBuf::from(h)
.join(".cache")
.join("mnml")
.join("prefetch")
})
}
pub fn prefetch_cache_path(integration_id: &str, prefetch_id: &str) -> Option<PathBuf> {
prefetch_dir().map(|d| d.join(format!("{integration_id}-{prefetch_id}.json")))
}
#[allow(dead_code)]
struct PrefetchJob {
integration_id: String,
prefetch_id: String,
command: String,
interval_secs: u64,
stagger_secs: u64,
}
impl App {
pub fn start_prefetch_workers(&mut self) {
let mut jobs: Vec<PrefetchJob> = Vec::new();
for m in &self.integration_manifests {
if !self.integration_chip_enabled(&m.id) {
continue;
}
for p in &m.prefetch {
if !crate::app::statusline_segments::binary_from_command_on_path(&p.command) {
continue;
}
jobs.push(PrefetchJob {
integration_id: m.id.clone(),
prefetch_id: p.id.clone(),
command: p.command.clone(),
interval_secs: clamp_interval(p.poll_interval_secs),
stagger_secs: 0, });
}
}
self.prefetch_worker_shutdowns.clear();
if jobs.is_empty() {
return;
}
if let Some(dir) = prefetch_dir() {
let _ = std::fs::create_dir_all(&dir);
}
for (index, mut job) in jobs.into_iter().enumerate() {
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
self.prefetch_worker_shutdowns.push(shutdown_tx);
job.stagger_secs = (index as u64 * 2).min(30);
spawn_prefetch_worker(job, shutdown_rx);
}
}
pub fn prefetch_cache_for_launch(
&self,
integration_id: &str,
args: &[String],
) -> Option<PathBuf> {
let pane_kind = extract_only_kind(args);
let manifest = self
.integration_manifests
.iter()
.find(|m| m.id == integration_id)?;
for p in &manifest.prefetch {
let matches = match (p.for_pane_kind.as_deref(), pane_kind.as_deref()) {
(None, _) => true,
(Some(want), Some(got)) => want == got,
(Some(_), None) => false,
};
if !matches {
continue;
}
let path = prefetch_cache_path(integration_id, &p.id)?;
let meta = std::fs::metadata(&path).ok()?;
let mtime = meta.modified().ok()?;
let age = std::time::SystemTime::now().duration_since(mtime).ok()?;
let stale_after = clamp_interval(p.poll_interval_secs) * FRESHNESS_MULTIPLIER as u64;
if age.as_secs() > stale_after {
continue;
}
return Some(path);
}
None
}
}
fn extract_only_kind(args: &[String]) -> Option<String> {
let mut it = args.iter();
while let Some(a) = it.next() {
if let Some(rest) = a.strip_prefix("--only=") {
return Some(rest.to_string());
}
if a == "--only" {
return it.next().cloned();
}
}
None
}
#[allow(dead_code)]
struct PrefetchUpdate {
integration_id: String,
prefetch_id: String,
result: Result<(), String>,
}
#[cfg(not(test))]
fn spawn_prefetch_worker(job: PrefetchJob, shutdown_rx: std::sync::mpsc::Receiver<()>) {
let name = format!("mnml-prefetch-{}-{}", job.integration_id, job.prefetch_id);
std::thread::Builder::new()
.name(name)
.spawn(move || run_prefetch_worker(job, shutdown_rx))
.ok();
}
#[cfg(test)]
fn spawn_prefetch_worker(_job: PrefetchJob, _shutdown_rx: std::sync::mpsc::Receiver<()>) {
}
#[cfg(not(test))]
fn run_prefetch_worker(job: PrefetchJob, shutdown_rx: std::sync::mpsc::Receiver<()>) {
if job.stagger_secs > 0 {
match shutdown_rx.recv_timeout(Duration::from_secs(job.stagger_secs)) {
Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
}
}
loop {
let _ = poll_and_write(&job);
match shutdown_rx.recv_timeout(Duration::from_secs(job.interval_secs)) {
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
}
}
}
#[cfg(not(test))]
fn poll_and_write(job: &PrefetchJob) -> Result<(), String> {
let mut parts = job.command.split_whitespace();
let bin = parts.next().ok_or_else(|| "empty command".to_string())?;
let args: Vec<&str> = parts.collect();
let out = std::process::Command::new(bin)
.args(&args)
.output()
.map_err(|e| format!("spawn: {e}"))?;
if !out.status.success() {
return Err(format!("exit {}", out.status.code().unwrap_or(-1)));
}
let path = prefetch_cache_path(&job.integration_id, &job.prefetch_id)
.ok_or_else(|| "no HOME".to_string())?;
let mut tmp = path.clone();
tmp.set_extension("json.tmp");
std::fs::write(&tmp, &out.stdout).map_err(|e| format!("write tmp: {e}"))?;
std::fs::rename(&tmp, &path).map_err(|e| format!("rename: {e}"))?;
Ok(())
}
#[allow(dead_code)]
fn _touch_update_shape(u: PrefetchUpdate) -> (String, String, Result<(), String>) {
(u.integration_id, u.prefetch_id, u.result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_only_kind_finds_space_form() {
let args = vec![
"--flag".to_string(),
"--only".to_string(),
"work".to_string(),
];
assert_eq!(extract_only_kind(&args), Some("work".to_string()));
}
#[test]
fn extract_only_kind_finds_equals_form() {
let args = vec!["--only=boards".to_string()];
assert_eq!(extract_only_kind(&args), Some("boards".to_string()));
}
#[test]
fn extract_only_kind_absent_returns_none() {
let args = vec!["--values".to_string()];
assert_eq!(extract_only_kind(&args), None);
}
#[test]
fn clamp_interval_floors_at_min() {
assert_eq!(clamp_interval(Some(5)), MIN_INTERVAL_SECS);
}
}