use std::path::{Path, PathBuf};
const CWD_TAG: &str = "% cwd: ";
fn dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share/plgr/sessions"))
}
pub fn filename(cwd: &Path) -> String {
let mut out = String::new();
for &b in cwd.to_string_lossy().as_bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.') {
out.push(b as char);
} else {
out.push('%');
out.push_str(&format!("{b:02X}"));
}
}
out
}
pub fn path_for(cwd: &Path) -> Option<PathBuf> {
dir().map(|d| d.join(format!("{}.pl", filename(cwd))))
}
fn strip_cwd_tag(raw: &str) -> String {
raw.lines()
.skip_while(|l| l.trim_start().starts_with(CWD_TAG))
.collect::<Vec<_>>()
.join("\n")
}
pub fn load(cwd: &Path) -> Option<String> {
let raw = std::fs::read_to_string(path_for(cwd)?).ok()?;
Some(strip_cwd_tag(&raw))
}
pub fn save(cwd: &Path, source: &str) {
let Some(path) = path_for(cwd) else {
return;
};
if source.trim().is_empty() {
let _ = std::fs::remove_file(&path);
return;
}
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let body = format!("{CWD_TAG}{}\n{}", cwd.display(), source);
let _ = std::fs::write(&path, body);
}
pub fn remove(cwd: &Path) {
if let Some(path) = path_for(cwd) {
let _ = std::fs::remove_file(&path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filename_is_collision_free_vs_underscore_paths() {
let slash = filename(Path::new("/a/b"));
let under = filename(Path::new("/a_b"));
assert_ne!(slash, under, "distinct paths must not collide");
}
#[test]
fn filename_encodes_separators_and_spaces() {
assert_eq!(
filename(Path::new("/Users/navicore/patch-prolog")),
"%2FUsers%2Fnavicore%2Fpatch-prolog"
);
assert_eq!(filename(Path::new("/a b")), "%2Fa%20b");
}
#[test]
fn filename_is_injective_over_near_neighbours() {
let p = Path::new("/Users/navicore/git/navicore/patch-prolog");
let q = Path::new("/Users/navicore/git/navicore/patch-prolog ");
assert_ne!(filename(p), filename(q));
}
#[test]
fn strip_cwd_tag_removes_only_leading_metadata() {
let raw = "% cwd: /abs/dir\nfoo(1).\nbar(X) :- foo(X).\n";
assert_eq!(strip_cwd_tag(raw), "foo(1).\nbar(X) :- foo(X).");
}
#[test]
fn strip_cwd_tag_passes_through_untagged_source() {
assert_eq!(strip_cwd_tag("foo(1).\n"), "foo(1).");
assert_eq!(strip_cwd_tag(""), "");
}
#[test]
fn save_load_roundtrips_through_the_filesystem() {
let tmp = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("HOME", tmp.path()) };
let cwd = Path::new("/fake/project");
assert!(load(cwd).is_none(), "nothing saved yet");
save(cwd, "foo(1).\nbar(X) :- foo(X).\n");
let loaded = load(cwd).expect("saved session loads");
assert_eq!(loaded, "foo(1).\nbar(X) :- foo(X).");
let raw = std::fs::read_to_string(path_for(cwd).unwrap()).unwrap();
assert!(raw.starts_with("% cwd: /fake/project\n"));
save(cwd, "");
assert!(!path_for(cwd).unwrap().exists());
assert!(load(cwd).is_none());
}
}