Add App core struct with event-handling and initialization logic for TUI.
This commit is contained in:
269
crates/owlen-tui/src/files.rs
Normal file
269
crates/owlen-tui/src/files.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
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(¤t_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])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user