use std::error::Error;
use std::path::PathBuf;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::Frame;
use crate::ui::terminal::views::{FileExplorer, FileItem, PreviewView, QueueView};
use crate::ui::terminal::{AppMode, Event, KeyResult, Tui};
use crate::ui::{Theme, UiAction, UserInterface, TransformAction};
use crate::transformers::transform;
use crate::{sort, unsort};
pub struct OperationQueue {
operations: Vec<FileOperation>,
selected_index: usize,
}
impl OperationQueue {
pub fn new() -> Self {
Self {
operations: Vec::new(),
selected_index: 0,
}
}
pub fn add(&mut self, operation: FileOperation) {
self.operations.push(operation);
}
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
pub fn operations(&self) -> &[FileOperation] {
&self.operations
}
pub fn selected_index(&self) -> usize {
self.selected_index
}
pub fn select_next(&mut self) {
if !self.operations.is_empty() {
self.selected_index = (self.selected_index + 1) % self.operations.len();
}
}
pub fn select_prev(&mut self) {
if !self.operations.is_empty() {
self.selected_index = self
.selected_index
.checked_sub(1)
.unwrap_or(self.operations.len() - 1);
}
}
pub fn remove_selected(&mut self) {
if !self.operations.is_empty() {
self.operations.remove(self.selected_index);
if self.selected_index >= self.operations.len() && !self.operations.is_empty() {
self.selected_index = self.operations.len() - 1;
}
}
}
pub fn clear(&mut self) {
self.operations.clear();
self.selected_index = 0;
}
}
#[derive(Clone, Debug)]
pub struct FileOperation {
pub source: PathBuf,
pub destination: PathBuf,
pub operation_type: OperationType,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OperationType {
Move,
Transform(crate::transformers::TransformType),
}
pub struct App {
tui: Tui,
mode: AppMode,
current_dir: PathBuf,
explorer: FileExplorer,
queue: OperationQueue,
queue_view: QueueView,
preview: PreviewView,
theme: Theme,
should_exit: bool,
status_message: String,
}
impl App {
pub fn new() -> anyhow::Result<Self> {
let tui = Tui::new()?;
Tui::init_panic_hook();
let current_dir = std::env::current_dir()?;
let mut explorer = FileExplorer::new(current_dir.clone());
eprintln!("DEBUG: Loaded {} files in {}", explorer.files.len(), current_dir.display());
if explorer.files.is_empty() {
eprintln!("DEBUG: No files found, adding placeholder");
use crate::ui::terminal::views::FileItem;
explorer.files.push(FileItem {
name: "No files found".to_string(),
path: current_dir.clone(),
is_dir: false,
is_symlink: false,
size: 0,
});
}
Ok(Self {
tui,
mode: AppMode::Normal,
current_dir: current_dir.clone(),
explorer,
queue: OperationQueue::new(),
queue_view: QueueView::new(),
preview: PreviewView::new(),
theme: Theme::default(),
should_exit: false,
status_message: String::from("Press ? for help. j/k to navigate, Ctrl+Q to quit"),
})
}
fn handle_key_event(&mut self, key: KeyEvent) -> anyhow::Result<()> {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
self.should_exit = true;
return Ok(());
}
(KeyCode::Char('?'), KeyModifiers::NONE) => {
self.status_message = String::from("Help mode - press ESC to exit");
return Ok(());
}
(KeyCode::Esc, KeyModifiers::NONE) => {
self.mode = AppMode::Normal;
self.status_message = String::from("Normal mode");
return Ok(());
}
_ => {}
}
match self.mode {
AppMode::Normal => self.handle_normal_mode_key(key)?,
AppMode::Visual => self.handle_visual_mode_key(key)?,
AppMode::Command => self.handle_command_mode_key(key)?,
AppMode::Insert => self.handle_insert_mode_key(key)?,
}
Ok(())
}
fn handle_normal_mode_key(&mut self, key: KeyEvent) -> anyhow::Result<()> {
match self.explorer.handle_key(key, &self.mode) {
KeyResult::Handled(action) => {
if let Some(action) = action {
self.handle_ui_action(action)?;
}
return Ok(());
}
KeyResult::NotHandled => {}
}
match self.queue_view.handle_key(key, &self.mode, &mut self.queue) {
KeyResult::Handled(action) => {
if let Some(action) = action {
self.handle_ui_action(action)?;
}
return Ok(());
}
KeyResult::NotHandled => {}
}
match (key.code, key.modifiers) {
(KeyCode::Char('v'), KeyModifiers::NONE) => {
self.mode = AppMode::Visual;
self.status_message = String::from("Visual mode");
}
(KeyCode::Char(':'), KeyModifiers::NONE) => {
self.mode = AppMode::Command;
self.status_message = String::from(":");
}
(KeyCode::Char('x'), KeyModifiers::NONE) => {
self.handle_ui_action(UiAction::ExecuteQueue)?;
}
(KeyCode::Char('q'), KeyModifiers::NONE) => {
self.queue.clear();
self.status_message = String::from("Queue cleared");
}
_ => {}
}
Ok(())
}
fn handle_visual_mode_key(&mut self, key: KeyEvent) -> anyhow::Result<()> {
match self.explorer.handle_key(key, &self.mode) {
KeyResult::Handled(action) => {
if let Some(action) = action {
self.handle_ui_action(action)?;
}
return Ok(());
}
KeyResult::NotHandled => {}
}
Ok(())
}
fn handle_command_mode_key(&mut self, key: KeyEvent) -> anyhow::Result<()> {
match key.code {
KeyCode::Enter => {
self.mode = AppMode::Normal;
self.status_message = String::from("Command executed");
}
_ => {}
}
Ok(())
}
fn handle_insert_mode_key(&mut self, key: KeyEvent) -> anyhow::Result<()> {
match key.code {
KeyCode::Enter => {
self.mode = AppMode::Normal;
}
_ => {}
}
Ok(())
}
fn handle_ui_action(&mut self, action: UiAction) -> anyhow::Result<()> {
match action {
UiAction::Exit => {
self.should_exit = true;
}
UiAction::ExecuteQueue => {
self.execute_queue()?;
}
UiAction::ShowHelp => {
self.status_message = String::from("Help view (not implemented)");
}
UiAction::AddToQueue => {
if let Some(file) = self.explorer.selected() {
if !file.is_dir {
let operation = FileOperation {
source: file.path.clone(),
destination: file.path.clone(), operation_type: OperationType::Move,
};
self.queue.add(operation);
self.status_message = format!("Added {} to queue", file.name);
}
}
}
UiAction::Transform(transform_action) => {
if let Some(file) = self.explorer.selected().cloned() {
if !file.is_dir {
self.add_transform_to_queue(&file, transform_action)?;
}
}
}
UiAction::GroupFiles => {
if let Some(dir) = self.explorer.selected().cloned() {
if dir.is_dir {
self.group_files_in_directory(&dir.path)?;
}
}
}
UiAction::FlattenDirectory => {
if let Some(dir) = self.explorer.selected().cloned() {
if dir.is_dir {
self.flatten_directory(&dir.path)?;
}
}
}
UiAction::Continue => {}
}
Ok(())
}
fn add_transform_to_queue(&mut self, file: &FileItem, transform_action: TransformAction) -> anyhow::Result<()> {
let transform_type = match transform_action {
TransformAction::Snake => crate::transformers::TransformType::Snake,
TransformAction::Kebab => crate::transformers::TransformType::Kebab,
TransformAction::Clean => crate::transformers::TransformType::Clean,
TransformAction::Title => crate::transformers::TransformType::Title,
TransformAction::Camel => crate::transformers::TransformType::Camel,
TransformAction::Pascal => crate::transformers::TransformType::Pascal,
TransformAction::Lower => crate::transformers::TransformType::Lower,
TransformAction::Upper => crate::transformers::TransformType::Upper,
};
let filename = file.path.file_name()
.ok_or_else(|| anyhow::anyhow!("Invalid filename"))?
.to_string_lossy();
let new_filename = transform(&filename, transform_type);
let new_path = file.path.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid parent directory"))?
.join(&new_filename);
let operation = FileOperation {
source: file.path.clone(),
destination: new_path,
operation_type: OperationType::Transform(transform_type),
};
self.queue.add(operation);
self.status_message = format!("Added {} transformation for {}", transform_action.as_str(), file.name);
Ok(())
}
fn execute_queue(&mut self) -> anyhow::Result<()> {
if self.queue.is_empty() {
self.status_message = String::from("Queue is empty");
return Ok(());
}
let operations = self.queue.operations().to_vec();
let mut success_count = 0;
let mut error_count = 0;
for operation in operations {
match std::fs::rename(&operation.source, &operation.destination) {
Ok(_) => {
success_count += 1;
}
Err(_e) => {
error_count += 1;
}
}
}
self.queue.clear();
self.status_message = format!("Executed: {} success, {} errors", success_count, error_count);
let _ = self.explorer.reload_files();
Ok(())
}
fn group_files_in_directory(&mut self, dir_path: &PathBuf) -> anyhow::Result<()> {
match sort::group_by_basename(&dir_path.to_string_lossy(), false) {
Ok(_) => {
self.status_message = format!("Grouped files in {}", dir_path.display());
let _ = self.explorer.reload_files();
}
Err(e) => {
self.status_message = format!("Error grouping files: {}", e);
}
}
Ok(())
}
fn flatten_directory(&mut self, dir_path: &PathBuf) -> anyhow::Result<()> {
match unsort::flatten_directory(&dir_path.to_string_lossy(), false) {
Ok(_) => {
let _ = unsort::remove_empty_dirs(&dir_path.to_string_lossy(), false);
self.status_message = format!("Flattened directory {}", dir_path.display());
let _ = self.explorer.reload_files();
}
Err(e) => {
self.status_message = format!("Error flattening directory: {}", e);
}
}
Ok(())
}
fn render(&mut self) -> anyhow::Result<()> {
let current_dir = self.current_dir.display().to_string();
let status_message = self.status_message.clone();
let mode = format!("{:?}", self.mode);
let queue_len = self.queue.operations().len();
let files_data: Vec<(String, bool)> = self.explorer.files.iter()
.map(|file| (file.name.clone(), file.is_dir))
.collect();
self.tui.draw(|frame| {
use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
};
let size = frame.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(size);
let header = Paragraph::new(format!("SMV Terminal UI - {}", current_dir))
.block(Block::default().borders(Borders::ALL).title("Smart Move"))
.style(Style::default().fg(Color::Cyan));
frame.render_widget(header, chunks[0]);
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(70), Constraint::Percentage(30), ])
.split(chunks[1]);
let explorer_content: Vec<ListItem> = files_data.iter()
.map(|(name, is_dir)| {
let icon = if *is_dir { "📁" } else { "📄" };
ListItem::new(format!("{} {}", icon, name))
})
.collect();
let explorer = List::new(explorer_content)
.block(Block::default().borders(Borders::ALL).title("Files"))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::Blue));
frame.render_widget(explorer, main_chunks[0]);
let queue_content = if queue_len > 0 {
vec![ListItem::new(format!("Operations: {}", queue_len))]
} else {
vec![ListItem::new("No operations queued")]
};
let queue = List::new(queue_content)
.block(Block::default().borders(Borders::ALL).title("Queue"))
.style(Style::default().fg(Color::White));
frame.render_widget(queue, main_chunks[1]);
let status_text = format!("Mode: {} | {} | Ctrl+Q: Quit, ?: Help", mode, status_message);
let status = Paragraph::new(status_text)
.block(Block::default().borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.wrap(Wrap { trim: true });
frame.render_widget(status, chunks[2]);
})?;
Ok(())
}
fn render_app(&self, _frame: &mut Frame) -> anyhow::Result<()> {
Ok(())
}
}
impl UserInterface for App {
fn run(&mut self) -> Result<(), Box<dyn Error>> {
self.render().map_err(|e| format!("Initial render failed: {}", e))?;
while !self.should_exit {
match self.tui.next_event() {
Ok(Event::Key(key)) => {
self.handle_key_event(key).map_err(|e| format!("Key event handling failed: {}", e))?;
}
Ok(Event::Resize(_, _)) => {
}
Ok(Event::Tick) => {
}
Err(e) => {
eprintln!("Event error: {}", e);
}
}
self.render().map_err(|e| format!("Render failed: {}", e))?;
}
self.tui.exit()?;
Ok(())
}
fn open_directory(&mut self, path: PathBuf) -> Result<(), Box<dyn Error>> {
self.current_dir = path.clone();
self.explorer.change_directory(path)?;
Ok(())
}
}