use std::io::Write;
use std::path::{Path, PathBuf};
static BUNDLE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zsh_docs.zst"));
include!(concat!(env!("OUT_DIR"), "/zsh_docs_id.rs"));
const STAMP: &str = ".zshrs-docs-version";
fn stamp_value() -> String {
format!("{}-{}", env!("CARGO_PKG_VERSION"), DOCS_ID)
}
fn base_dir() -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
Some(PathBuf::from(home).join(".zshrs"))
}
pub fn man_dir() -> Option<PathBuf> {
Some(base_dir()?.join("man"))
}
pub fn info_dir() -> Option<PathBuf> {
Some(base_dir()?.join("info"))
}
pub fn help_dir() -> Option<PathBuf> {
Some(base_dir()?.join("help"))
}
fn needs_write(base: &Path) -> bool {
match std::fs::read_to_string(base.join(STAMP)) {
Ok(s) => s.trim() != stamp_value(),
Err(_) => true,
}
}
pub fn ensure_installed() -> Option<usize> {
let base = base_dir()?;
if !needs_write(&base) {
return Some(0);
}
let raw = zstd::decode_all(BUNDLE).ok()?;
let mut n = 0usize;
let mut i = 0usize;
while i + 4 <= raw.len() {
let nl = u32::from_le_bytes(raw[i..i + 4].try_into().ok()?) as usize;
i += 4;
if i + nl + 4 > raw.len() {
break;
}
let name = String::from_utf8_lossy(&raw[i..i + nl]).into_owned();
i += nl;
let bl = u32::from_le_bytes(raw[i..i + 4].try_into().ok()?) as usize;
i += 4;
if i + bl > raw.len() {
break;
}
let body = &raw[i..i + bl];
i += bl;
if name.is_empty() || name.starts_with('/') || name.contains("..") {
continue;
}
let dest = base.join(&name);
let Some(parent) = dest.parent() else { continue };
if std::fs::create_dir_all(parent).is_err() {
continue;
}
if std::fs::write(&dest, body).is_ok() {
n += 1;
}
}
if let Ok(mut f) = std::fs::File::create(base.join(STAMP)) {
let _ = f.write_all(stamp_value().as_bytes());
}
tracing::info!(target: "bundled_docs", written = n, dir = %base.display(),
"materialised bundled zsh man/info pages");
Some(n)
}
fn prepend_search_path(var: &str, dir: &Path) {
let dir = dir.to_string_lossy().into_owned();
let next = match std::env::var(var) {
Ok(cur) if !cur.is_empty() => {
if cur.split(':').any(|e| e == dir) {
return;
}
format!("{dir}:{cur}")
}
_ => format!("{dir}:"),
};
unsafe { std::env::set_var(var, next) };
}
fn published_dirs() -> Vec<(&'static str, PathBuf)> {
let mut out = Vec::new();
if let Some(d) = man_dir() {
if d.is_dir() {
out.push(("MANPATH", d));
}
}
if let Some(d) = info_dir() {
if d.is_dir() {
out.push(("INFOPATH", d));
}
}
out
}
fn helpdir_value() -> Option<String> {
let d = help_dir()?;
d.is_dir().then(|| d.to_string_lossy().into_owned())
}
pub fn install_and_publish() {
let _ = ensure_installed();
for (var, dir) in published_dirs() {
prepend_search_path(var, &dir);
}
if let Some(v) = helpdir_value() {
if std::env::var_os("HELPDIR").is_none() {
unsafe { std::env::set_var("HELPDIR", v) };
}
}
}
pub fn publish_into(env: &mut Vec<(String, String)>) {
let _ = ensure_installed();
for (var, dir) in published_dirs() {
let dir = dir.to_string_lossy().into_owned();
match env.iter_mut().find(|(k, _)| k == var) {
Some((_, v)) if !v.is_empty() => {
if !v.split(':').any(|e| e == dir) {
*v = format!("{dir}:{v}");
}
}
Some((_, v)) => *v = format!("{dir}:"),
None => env.push((var.to_string(), format!("{dir}:"))),
}
let val = env
.iter()
.find(|(k, _)| k == var)
.map(|(_, v)| v.clone())
.unwrap_or_default();
unsafe { std::env::set_var(var, val) };
}
if let Some(v) = helpdir_value() {
if !env.iter().any(|(k, val)| k == "HELPDIR" && !val.is_empty()) {
env.retain(|(k, _)| k != "HELPDIR");
env.push(("HELPDIR".to_string(), v.clone()));
unsafe { std::env::set_var("HELPDIR", v) };
}
}
}