Files
owlen/crates/owlen-tui/src/files.rs

269 lines
8.4 KiB
Rust

use anyhow::{Result, Context};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::config::Config;
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: PathBuf,
pub name: String,
pub size: u64,
pub modified: SystemTime,
pub is_readable: bool,
pub is_writable: bool,
}
pub struct FileManager {
config: Config,
}
impl FileManager {
pub fn new(config: Config) -> Self {
Self { config }
}
/// Read a file and return its contents
pub fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<String> {
let path = path.as_ref();
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to get metadata for {}", path.display()))?;
// Check file size limit
let size_mb = metadata.len() / (1024 * 1024);
if size_mb > self.config.files.max_file_size_mb {
return Err(anyhow::anyhow!(
"File {} is too large ({} MB > {} MB limit)",
path.display(),
size_mb,
self.config.files.max_file_size_mb
));
}
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read file {}", path.display()))?;
Ok(content)
}
/// Write content to a file
pub fn write_file<P: AsRef<Path>>(&self, path: P, content: &str) -> Result<()> {
let path = path.as_ref();
// Create backup if enabled
if self.config.files.backup_files && path.exists() {
self.create_backup(path)?;
}
// Ensure parent directory exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory {}", parent.display()))?;
}
fs::write(path, content)
.with_context(|| format!("Failed to write file {}", path.display()))?;
Ok(())
}
/// Create a backup of the file
fn create_backup<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let backup_path = path.with_extension(format!("{}.backup",
path.extension().and_then(|s| s.to_str()).unwrap_or("txt")));
fs::copy(path, &backup_path)
.with_context(|| format!("Failed to create backup at {}", backup_path.display()))?;
Ok(())
}
/// List files in a directory
pub fn list_files<P: AsRef<Path>>(&self, dir: P) -> Result<Vec<FileInfo>> {
let dir = dir.as_ref();
let entries = fs::read_dir(dir)
.with_context(|| format!("Failed to read directory {}", dir.display()))?;
let mut files = Vec::new();
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let metadata = entry.metadata()?;
let name = entry.file_name().to_string_lossy().to_string();
files.push(FileInfo {
path: path.clone(),
name,
size: metadata.len(),
modified: metadata.modified()?,
is_readable: path.exists() && fs::File::open(&path).is_ok(),
is_writable: !metadata.permissions().readonly(),
});
}
}
// Sort by name
files.sort_by(|a, b| a.name.cmp(&b.name));
Ok(files)
}
/// Check if a file exists
pub fn file_exists<P: AsRef<Path>>(&self, path: P) -> bool {
path.as_ref().exists()
}
/// Get file info
pub fn get_file_info<P: AsRef<Path>>(&self, path: P) -> Result<FileInfo> {
let path = path.as_ref();
let metadata = fs::metadata(path)
.with_context(|| format!("Failed to get metadata for {}", path.display()))?;
Ok(FileInfo {
path: path.to_path_buf(),
name: path.file_name().unwrap_or_default().to_string_lossy().to_string(),
size: metadata.len(),
modified: metadata.modified()?,
is_readable: path.exists() && fs::File::open(&path).is_ok(),
is_writable: !metadata.permissions().readonly(),
})
}
/// Append content to a file
pub fn append_file<P: AsRef<Path>>(&self, path: P, content: &str) -> Result<()> {
let path = path.as_ref();
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("Failed to open file for appending {}", path.display()))?;
file.write_all(content.as_bytes())
.with_context(|| format!("Failed to append to file {}", path.display()))?;
Ok(())
}
/// Load project context file (OWLEN.md)
pub fn load_project_context(&self) -> Result<Option<String>> {
let context_file = &self.config.general.project_context_file;
if self.file_exists(context_file) {
match self.read_file(context_file) {
Ok(content) => Ok(Some(content)),
Err(_) => Ok(None), // File exists but can't read, return None instead of error
}
} else {
Ok(None)
}
}
/// Create a default project context file
pub fn create_default_project_context(&self) -> Result<()> {
let context_file = &self.config.general.project_context_file;
if !self.file_exists(context_file) {
let default_content = r#"# Project Context - OWLlama
This file provides context about your project to the AI assistant.
## Project Description
Describe your project here.
## Key Files and Structure
List important files, directories, and their purposes.
## Technologies Used
- Programming languages
- Frameworks
- Tools and dependencies
## Development Guidelines
- Coding standards
- Best practices
- Testing approach
## Current Focus
What you're currently working on or need help with.
---
*This file is automatically loaded as context for AI conversations.*
"#;
self.write_file(context_file, default_content)?;
}
Ok(())
}
}
/// Utility functions for common file operations
pub mod utils {
use super::*;
/// Get the current working directory
pub fn get_current_dir() -> Result<PathBuf> {
std::env::current_dir()
.context("Failed to get current directory")
}
/// Expand tilde in path
pub fn expand_path<P: AsRef<Path>>(path: P) -> PathBuf {
let path_str = path.as_ref().to_string_lossy();
let expanded = shellexpand::tilde(&path_str);
PathBuf::from(expanded.as_ref())
}
/// Get relative path from current directory
pub fn get_relative_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let current_dir = get_current_dir()?;
let absolute_path = path.as_ref().canonicalize()
.context("Failed to canonicalize path")?;
absolute_path.strip_prefix(&current_dir)
.map(|p| p.to_path_buf())
.or_else(|_| Ok(absolute_path))
}
/// Check if path is a text file based on extension
pub fn is_text_file<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
matches!(ext.to_lowercase().as_str(),
"txt" | "md" | "rs" | "py" | "js" | "ts" | "html" | "css" | "json" |
"toml" | "yaml" | "yml" | "xml" | "csv" | "log" | "sh" | "bash" |
"c" | "cpp" | "h" | "hpp" | "java" | "go" | "php" | "rb" | "swift" |
"kt" | "scala" | "r" | "sql" | "dockerfile" | "makefile"
)
} else {
// Files without extensions might be text (like Makefile, README, etc.)
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.chars().all(|c| c.is_ascii()))
.unwrap_or(false)
}
}
/// Format file size in human readable format
pub fn format_file_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = size as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size as u64, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
}