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:
Taylor Eernisse
2026-02-06 14:21:57 -05:00
commit b55d1aefd1
65 changed files with 12439 additions and 0 deletions

154
src/cache.rs Normal file
View 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()
}