use crate::evaluator::{EvalError, Value}; use std::path::PathBuf; use std::process::Command; use walkdir::WalkDir; #[tracing::instrument(level = "trace", skip_all)] pub fn read_file(args: &[Value]) -> Result { let path = get_path(args)?; let content = std::fs::read_to_string(&path)?; Ok(Value::Str(content)) } #[tracing::instrument(level = "trace", skip_all)] pub fn read_file_lines(args: &[Value]) -> Result { let path = get_path(args)?; let content = std::fs::read_to_string(&path)?; let lines: Vec = content.lines().map(|l| Value::Str(l.to_string())).collect(); Ok(Value::List(lines)) } #[tracing::instrument(level = "trace", skip_all)] pub fn write_file(args: &[Value]) -> Result { let path = get_path(args)?; let content = match args.get(1) { Some(Value::Str(s)) => s, _ => { return Err(EvalError::TypeError( "write_file requires content string".to_string(), )); } }; std::fs::write(&path, content)?; Ok(Value::Bool(true)) } #[tracing::instrument(level = "trace", skip_all)] pub fn copy_file(args: &[Value]) -> Result { let src = get_path(args)?; let dst = match args.get(1) { Some(Value::Path(p)) => p.clone(), Some(Value::Str(s)) => expand_path(s), _ => { return Err(EvalError::TypeError( "copy_file requires destination path".to_string(), )); } }; std::fs::copy(&src, &dst)?; Ok(Value::Bool(true)) } #[tracing::instrument(level = "trace", skip_all)] pub fn delete_file(args: &[Value]) -> Result { let path = get_path(args)?; std::fs::remove_file(&path)?; Ok(Value::Bool(true)) } #[tracing::instrument(level = "trace", skip_all)] pub fn file_exists(args: &[Value]) -> Result { let path = get_path(args)?; Ok(Value::Bool(path.is_file())) } #[tracing::instrument(level = "trace", skip_all)] pub fn dir_exists(args: &[Value]) -> Result { let path = get_path(args)?; Ok(Value::Bool(path.is_dir())) } #[tracing::instrument(level = "trace", skip_all)] pub fn create_dir_all(args: &[Value]) -> Result { let path = get_path(args)?; std::fs::create_dir_all(&path)?; Ok(Value::Bool(true)) } #[tracing::instrument(level = "trace", skip_all)] pub fn list_dir(args: &[Value]) -> Result { let path = get_path(args)?; let entries: Vec = std::fs::read_dir(&path)? .filter_map(|e| e.ok()) .map(|e| Value::Path(e.path())) .collect(); Ok(Value::List(entries)) } #[tracing::instrument(level = "trace", skip_all)] pub fn walk_dir(args: &[Value]) -> Result { let path = get_path(args)?; let entries: Vec = WalkDir::new(&path) .into_iter() .filter_map(|e| e.ok()) .map(|e| Value::Path(e.path().to_path_buf())) .collect(); Ok(Value::List(entries)) } #[tracing::instrument(level = "trace", skip_all)] pub fn temp_dir() -> Result { Ok(Value::Path(std::env::temp_dir())) } #[tracing::instrument(level = "trace", skip_all)] pub fn temp_file(args: &[Value]) -> Result { let prefix = match args.first() { Some(Value::Str(s)) => s.as_str(), _ => "doot", }; let suffix = match args.get(1) { Some(Value::Str(s)) => s.as_str(), _ => "", }; let path = std::env::temp_dir().join(format!("{}_{}{}", prefix, uuid_simple(), suffix)); Ok(Value::Path(path)) } #[tracing::instrument(level = "trace", skip_all)] pub fn is_symlink(args: &[Value]) -> Result { let path = get_path(args)?; Ok(Value::Bool(path.is_symlink())) } #[tracing::instrument(level = "trace", skip_all)] pub fn read_link(args: &[Value]) -> Result { let path = get_path(args)?; let target = std::fs::read_link(&path)?; Ok(Value::Path(target)) } #[tracing::instrument(level = "trace", skip_all)] pub fn path_join(args: &[Value]) -> Result { let mut result = PathBuf::new(); for arg in args { match arg { Value::Path(p) => result.push(p), Value::Str(s) => result.push(s), _ => { return Err(EvalError::TypeError( "path_join expects paths or strings".to_string(), )); } } } Ok(Value::Path(result)) } #[tracing::instrument(level = "trace", skip_all)] pub fn path_parent(args: &[Value]) -> Result { let path = get_path(args)?; Ok(Value::Path( path.parent().map(|p| p.to_path_buf()).unwrap_or_default(), )) } #[tracing::instrument(level = "trace", skip_all)] pub fn path_filename(args: &[Value]) -> Result { let path = get_path(args)?; Ok(Value::Str( path.file_name() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(), )) } #[tracing::instrument(level = "trace", skip_all)] pub fn path_extension(args: &[Value]) -> Result { let path = get_path(args)?; Ok(Value::Str( path.extension() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default(), )) } #[tracing::instrument(level = "trace", skip_all)] pub fn home_dir() -> Result { Ok(Value::Path(doot_utils::xdg::home_dir())) } #[tracing::instrument(level = "trace", skip_all)] pub fn config_dir() -> Result { Ok(Value::Path(doot_utils::xdg::config_home())) } #[tracing::instrument(level = "trace", skip_all)] pub fn data_dir() -> Result { Ok(Value::Path(doot_utils::xdg::data_home())) } #[tracing::instrument(level = "trace", skip_all)] pub fn cache_dir() -> Result { Ok(Value::Path(doot_utils::xdg::cache_home())) } #[tracing::instrument(level = "trace", skip_all)] pub fn exec(args: &[Value]) -> Result { let cmd = match args.first() { Some(Value::Str(s)) => s, _ => { return Err(EvalError::TypeError( "exec expects a command string".to_string(), )); } }; let output = Command::new("sh").arg("-c").arg(cmd).output()?; Ok(Value::Str( String::from_utf8_lossy(&output.stdout).to_string(), )) } #[tracing::instrument(level = "trace", skip_all)] pub fn exec_with_status(args: &[Value]) -> Result { let cmd = match args.first() { Some(Value::Str(s)) => s, _ => { return Err(EvalError::TypeError( "exec_with_status expects a command string".to_string(), )); } }; let status = Command::new("sh").arg("-c").arg(cmd).status()?; Ok(Value::Int(status.code().unwrap_or(-1) as i64)) } #[tracing::instrument(level = "trace", skip_all)] pub fn shell(args: &[Value]) -> Result { exec(args) } #[tracing::instrument(level = "trace", skip_all)] pub fn which(args: &[Value]) -> Result { let cmd = match args.first() { Some(Value::Str(s)) => s, _ => { return Err(EvalError::TypeError( "which expects a command name".to_string(), )); } }; let output = Command::new("which").arg(cmd).output()?; if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); Ok(Value::Path(PathBuf::from(path))) } else { Ok(Value::None) } } #[tracing::instrument(level = "trace", skip_all)] pub fn to_json(args: &[Value]) -> Result { let val = args.first().unwrap_or(&Value::None); let json = value_to_json(val); Ok(Value::Str(json.to_string())) } #[tracing::instrument(level = "trace", skip_all)] pub fn from_json(args: &[Value]) -> Result { let s = match args.first() { Some(Value::Str(s)) => s, _ => { return Err(EvalError::TypeError( "from_json expects a string".to_string(), )); } }; let json: serde_json::Value = serde_json::from_str(s) .map_err(|e| EvalError::TypeError(format!("invalid JSON: {}", e)))?; Ok(json_to_value(&json)) } #[tracing::instrument(level = "trace", skip_all)] pub fn to_toml(args: &[Value]) -> Result { let val = args.first().unwrap_or(&Value::None); let toml_val = value_to_toml(val); let s = toml::to_string(&toml_val) .map_err(|e| EvalError::TypeError(format!("TOML serialization error: {}", e)))?; Ok(Value::Str(s)) } #[tracing::instrument(level = "trace", skip_all)] pub fn from_toml(args: &[Value]) -> Result { let s = match args.first() { Some(Value::Str(s)) => s, _ => { return Err(EvalError::TypeError( "from_toml expects a string".to_string(), )); } }; let toml_val: toml::Value = toml::from_str(s).map_err(|e| EvalError::TypeError(format!("invalid TOML: {}", e)))?; Ok(toml_to_value(&toml_val)) } #[tracing::instrument(level = "trace", skip_all)] pub fn to_yaml(args: &[Value]) -> Result { let val = args.first().unwrap_or(&Value::None); let json = value_to_json(val); Ok(Value::Str( serde_json::to_string_pretty(&json).unwrap_or_default(), )) } #[tracing::instrument(level = "trace", skip_all)] pub fn from_yaml(args: &[Value]) -> Result { from_json(args) } fn get_path(args: &[Value]) -> Result { match args.first() { Some(Value::Path(p)) => Ok(p.clone()), Some(Value::Str(s)) => Ok(expand_path(s)), _ => Err(EvalError::TypeError("expected path or string".to_string())), } } fn expand_path(s: &str) -> PathBuf { if let Some(stripped) = s.strip_prefix('~') { let home = doot_utils::xdg::home_dir(); home.join(stripped.strip_prefix('/').unwrap_or(stripped)) } else { PathBuf::from(s) } } fn uuid_simple() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); format!("{:x}", nanos) } fn value_to_json(val: &Value) -> serde_json::Value { match val { Value::Int(n) => serde_json::Value::Number(serde_json::Number::from(*n)), Value::Float(n) => serde_json::Number::from_f64(*n) .map(serde_json::Value::Number) .unwrap_or(serde_json::Value::Null), Value::Str(s) => serde_json::Value::String(s.clone()), Value::Bool(b) => serde_json::Value::Bool(*b), Value::Path(p) => serde_json::Value::String(p.display().to_string()), Value::List(items) => serde_json::Value::Array(items.iter().map(value_to_json).collect()), Value::Struct(_, fields) => { let map: serde_json::Map = fields .iter() .map(|(k, v)| (k.clone(), value_to_json(v))) .collect(); serde_json::Value::Object(map) } Value::None => serde_json::Value::Null, _ => serde_json::Value::Null, } } fn json_to_value(json: &serde_json::Value) -> Value { match json { serde_json::Value::Null => Value::None, serde_json::Value::Bool(b) => Value::Bool(*b), serde_json::Value::Number(n) => { if let Some(i) = n.as_i64() { Value::Int(i) } else if let Some(f) = n.as_f64() { Value::Float(f) } else { Value::None } } serde_json::Value::String(s) => Value::Str(s.clone()), serde_json::Value::Array(arr) => Value::List(arr.iter().map(json_to_value).collect()), serde_json::Value::Object(obj) => { let fields: indexmap::IndexMap = obj .iter() .map(|(k, v)| (k.clone(), json_to_value(v))) .collect(); Value::Struct("object".to_string(), fields) } } } fn value_to_toml(val: &Value) -> toml::Value { match val { Value::Int(n) => toml::Value::Integer(*n), Value::Float(n) => toml::Value::Float(*n), Value::Str(s) => toml::Value::String(s.clone()), Value::Bool(b) => toml::Value::Boolean(*b), Value::Path(p) => toml::Value::String(p.display().to_string()), Value::List(items) => toml::Value::Array(items.iter().map(value_to_toml).collect()), Value::Struct(_, fields) => { let map: toml::map::Map = fields .iter() .map(|(k, v)| (k.clone(), value_to_toml(v))) .collect(); toml::Value::Table(map) } _ => toml::Value::String(String::new()), } } fn toml_to_value(toml: &toml::Value) -> Value { match toml { toml::Value::Boolean(b) => Value::Bool(*b), toml::Value::Integer(i) => Value::Int(*i), toml::Value::Float(f) => Value::Float(*f), toml::Value::String(s) => Value::Str(s.clone()), toml::Value::Array(arr) => Value::List(arr.iter().map(toml_to_value).collect()), toml::Value::Table(table) => { let fields: indexmap::IndexMap = table .iter() .map(|(k, v)| (k.clone(), toml_to_value(v))) .collect(); Value::Struct("table".to_string(), fields) } toml::Value::Datetime(dt) => Value::Str(dt.to_string()), } }