feat: complete Rust port of claude-statusline
Port the entire 2236-line bash statusline script to Rust. Implements all 25 sections, 3-phase layout engine (render, priority drop, flex/justify), file-based caching with flock, 9-level terminal width detection, trend sparklines, and deep-merge JSON config. Release binary: 864K with LTO. Render time: <1ms warm. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
154
src/cache.rs
Normal file
154
src/cache.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
pub struct Cache {
|
||||
dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
/// Create cache with secure directory. Returns disabled cache on failure.
|
||||
pub fn new(template: &str, session_id: &str) -> Self {
|
||||
let dir_str = template.replace("{session_id}", session_id);
|
||||
let dir = PathBuf::from(&dir_str);
|
||||
|
||||
if !dir.exists() {
|
||||
if fs::create_dir_all(&dir).is_err() {
|
||||
return Self { dir: None };
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs::set_permissions(&dir, fs::Permissions::from_mode(0o700));
|
||||
}
|
||||
}
|
||||
|
||||
// Security: verify ownership, not a symlink, not world-writable
|
||||
if !verify_cache_dir(&dir) {
|
||||
return Self { dir: None };
|
||||
}
|
||||
|
||||
Self { dir: Some(dir) }
|
||||
}
|
||||
|
||||
pub fn dir(&self) -> Option<&Path> {
|
||||
self.dir.as_deref()
|
||||
}
|
||||
|
||||
/// Get cached value if fresher than TTL.
|
||||
pub fn get(&self, key: &str, ttl: Duration) -> Option<String> {
|
||||
let path = self.key_path(key)?;
|
||||
let meta = fs::metadata(&path).ok()?;
|
||||
let modified = meta.modified().ok()?;
|
||||
let age = SystemTime::now().duration_since(modified).ok()?;
|
||||
if age < ttl {
|
||||
fs::read_to_string(&path).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stale cached value (ignores TTL). Used as fallback on command failure.
|
||||
pub fn get_stale(&self, key: &str) -> Option<String> {
|
||||
let path = self.key_path(key)?;
|
||||
fs::read_to_string(&path).ok()
|
||||
}
|
||||
|
||||
/// Atomic write: write to .tmp then rename (prevents partial reads).
|
||||
/// Uses flock with LOCK_NB — skips cache on contention rather than blocking.
|
||||
pub fn set(&self, key: &str, value: &str) -> Option<()> {
|
||||
let path = self.key_path(key)?;
|
||||
let tmp = path.with_extension("tmp");
|
||||
|
||||
// Try non-blocking flock
|
||||
let lock_path = path.with_extension("lock");
|
||||
let lock_file = fs::File::create(&lock_path).ok()?;
|
||||
if !try_flock(&lock_file) {
|
||||
return None; // contention — skip cache write
|
||||
}
|
||||
|
||||
let mut f = fs::File::create(&tmp).ok()?;
|
||||
f.write_all(value.as_bytes()).ok()?;
|
||||
fs::rename(&tmp, &path).ok()?;
|
||||
|
||||
unlock(&lock_file);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn key_path(&self, key: &str) -> Option<PathBuf> {
|
||||
let dir = self.dir.as_ref()?;
|
||||
let safe_key: String = key
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Some(dir.join(safe_key))
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_cache_dir(dir: &Path) -> bool {
|
||||
let meta = match fs::symlink_metadata(dir) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if !meta.is_dir() || meta.file_type().is_symlink() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
// Must be owned by current user
|
||||
if meta.uid() != unsafe { libc::getuid() } {
|
||||
return false;
|
||||
}
|
||||
// Must not be world-writable
|
||||
if meta.mode() & 0o002 != 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn try_flock(file: &fs::File) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
||||
ret == 0
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = file;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn unlock(file: &fs::File) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
unsafe {
|
||||
libc::flock(file.as_raw_fd(), libc::LOCK_UN);
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = file;
|
||||
}
|
||||
}
|
||||
|
||||
/// Session ID: first 12 chars of MD5 hex of project_dir.
|
||||
/// Same algorithm as bash for cache compatibility during migration.
|
||||
pub fn session_id(project_dir: &str) -> String {
|
||||
use md5::{Digest, Md5};
|
||||
let hash = Md5::digest(project_dir.as_bytes());
|
||||
format!("{:x}", hash)[..12].to_string()
|
||||
}
|
||||
Reference in New Issue
Block a user