Files
diskonaut/src/app.rs
2025-11-01 09:46:38 -07:00

292 lines
9.8 KiB
Rust

use ::std::fs::{self, Metadata};
use ::std::mem::ManuallyDrop;
use ::std::path::PathBuf;
use ::std::sync::mpsc::{Receiver, SyncSender};
use ::tui::backend::Backend;
use crate::Event;
use crate::messages::{Instruction, handle_instructions};
use crate::state::files::{FileOrFolder, FileTree, Folder};
use crate::state::tiles::Board;
use crate::state::{FileToDelete, UiEffects};
use crate::ui::Display;
#[derive(Clone)]
pub enum UiMode {
Loading,
Normal,
ScreenTooSmall,
DeleteFile(FileToDelete),
ErrorMessage(String),
Exiting { app_loaded: bool },
WarningMessage(FileToDelete),
}
pub struct App<B>
where
B: Backend,
{
pub is_running: bool,
pub loaded: bool,
pub ui_mode: UiMode,
board: Board,
file_tree: ManuallyDrop<FileTree>,
display: Display<B>,
event_sender: SyncSender<Event>,
ui_effects: UiEffects,
delete_confirmation_disabled: bool,
}
impl<B> App<B>
where
B: Backend,
{
pub fn new(
terminal_backend: B,
path_in_filesystem: PathBuf,
event_sender: SyncSender<Event>,
show_apparent_size: bool,
disable_delete_confirmation: bool,
) -> Self {
let display = Display::new(terminal_backend);
let board = Board::new(&Folder::new(&path_in_filesystem));
let base_folder = Folder::new(&path_in_filesystem);
let file_tree = ManuallyDrop::new(FileTree::new(
base_folder,
path_in_filesystem,
show_apparent_size,
));
// we use ManuallyDrop here because otherwise the app takes forever to exit
let ui_effects = UiEffects::new();
App {
is_running: true,
loaded: false,
board,
file_tree,
display,
ui_mode: UiMode::Loading,
event_sender,
ui_effects,
delete_confirmation_disabled: disable_delete_confirmation,
}
}
pub fn start(&mut self, receiver: Receiver<Instruction>) {
handle_instructions(self, receiver);
self.display.clear();
}
pub fn render_and_update_board(&mut self) {
let current_folder = self.file_tree.get_current_folder();
self.board.change_files(&current_folder);
self.render();
}
pub fn increment_loading_progress_indicator(&mut self) {
self.ui_effects.increment_loading_progress_indicator();
}
pub fn render(&mut self) {
let full_screen_size = self.display.size();
if full_screen_size.width < 50 || full_screen_size.height < 15 {
self.ui_mode = UiMode::ScreenTooSmall;
}
self.display.render(
&mut self.file_tree,
&mut self.board,
&self.ui_mode,
&self.ui_effects,
);
}
pub fn flash_space_freed(&mut self) {
self.ui_effects.flash_space_freed = true;
}
pub fn unflash_space_freed(&mut self) {
self.ui_effects.flash_space_freed = false;
}
pub fn set_path_to_red(&mut self) {
self.ui_effects.current_path_is_red = true;
}
pub fn reset_current_path_color(&mut self) {
self.ui_effects.current_path_is_red = false;
}
pub fn start_ui(&mut self) {
self.ui_mode = UiMode::Normal;
self.loaded = true;
self.render_and_update_board();
}
pub fn add_entry_to_base_folder(&mut self, file_metadata: &Metadata, entry_path: PathBuf) {
self.file_tree.add_entry(file_metadata, &entry_path);
self.ui_effects.last_read_path = Some(entry_path);
}
pub fn reset_ui_mode(&mut self) {
match self.ui_mode {
UiMode::Loading | UiMode::Normal => {}
_ => {
self.ui_mode = {
if self.loaded {
UiMode::Normal
} else {
UiMode::Loading
}
}
}
};
}
pub fn show_warning_modal(&mut self) {
if let Some(file_to_delete) = self.get_file_to_delete() {
self.ui_mode = UiMode::WarningMessage(file_to_delete);
self.render();
}
}
pub fn prompt_exit(&mut self) {
self.ui_mode = UiMode::Exiting {
app_loaded: self.loaded,
};
self.render();
}
pub fn exit(&mut self) {
self.is_running = false;
// here we do a blocking send rather than a try_send
// because we want to make sure that if the receiver
// is active, it received this event so that the app
// would exit cleanly
let _ = self.event_sender.send(Event::AppExit);
}
pub fn handle_enter(&mut self) {
if !self.board.has_selected_index() {
self.board.move_to_largest_folder();
}
self.enter_selected();
}
pub fn move_selected_right(&mut self) {
self.board.move_selected_right();
self.render();
}
pub fn move_selected_left(&mut self) {
self.board.move_selected_left();
self.render();
}
pub fn move_selected_down(&mut self) {
self.board.move_selected_down();
self.render();
}
pub fn move_selected_up(&mut self) {
self.board.move_selected_up();
self.render();
}
pub fn enter_selected(&mut self) {
self.board.record_current_index_and_zoom_level();
if let Some(tile) = &self.board.currently_selected() {
let selected_name = &tile.name;
if let Some(file_or_folder) = self.file_tree.item_in_current_folder(&selected_name) {
match file_or_folder {
FileOrFolder::Folder(_) => {
self.file_tree.enter_folder(&selected_name);
self.board.reset_zoom_index();
self.board.reset_selected_index();
self.render_and_update_board();
}
FileOrFolder::File(_) => {} // do not enter if currently_selected is a file
}
};
}
}
pub fn go_up(&mut self) {
let succeeded = self.file_tree.leave_folder();
if let Some((index, zoom_level)) = self.board.pop_previous_index_and_zoom_level() {
if let Some(index) = index {
self.board.set_selected_index(&index);
}
self.board.set_zoom_index(zoom_level);
}
self.render_and_update_board();
if !succeeded {
let _ = self.event_sender.try_send(Event::PathError);
}
}
pub fn get_file_to_delete(&self) -> Option<FileToDelete> {
let currently_selected = self.board.currently_selected()?;
let mut path_to_file = self.file_tree.current_folder_names.clone();
path_to_file.push(currently_selected.name.clone());
let file_to_delete = FileToDelete {
path_in_filesystem: self.file_tree.path_in_filesystem.clone(),
path_to_file,
file_type: currently_selected.file_type,
num_descendants: currently_selected.descendants,
size: currently_selected.size,
};
Some(file_to_delete)
}
pub fn prompt_file_deletion(&mut self) {
if let Some(file_to_delete) = self.get_file_to_delete() {
self.ui_mode = UiMode::DeleteFile(file_to_delete.clone());
if self.delete_confirmation_disabled {
// Here we just delete the file.
// As we have set the UI mode above we will get the deletion in progress message box instead of the prompt.
self.delete_file(&file_to_delete);
} else {
// Here we will render which will display the confirmation prompt
self.render();
}
}
}
pub fn normal_mode(&mut self) {
self.ui_mode = UiMode::Normal;
self.render_and_update_board();
}
pub fn delete_file(&mut self, file_to_delete: &FileToDelete) {
self.ui_effects.deletion_in_progress = true;
self.render();
self.ui_effects.deletion_in_progress = false;
let full_path = file_to_delete.full_path();
match fs::metadata(&full_path) {
Ok(metadata) => {
let file_type = metadata.file_type();
let file_removed = if file_type.is_dir() {
fs::remove_dir_all(&full_path)
} else {
fs::remove_file(&full_path)
};
match file_removed {
Ok(_) => {
self.remove_file_from_ui(file_to_delete);
self.ui_mode = UiMode::Normal;
self.render_and_update_board();
let _ = self.event_sender.try_send(Event::FileDeleted);
}
Err(msg) => {
self.ui_mode = UiMode::ErrorMessage(format!("{}", msg));
self.render();
}
};
}
Err(msg) => {
self.ui_mode = UiMode::ErrorMessage(format!("{}", msg));
self.render();
}
}
}
pub fn increment_failed_to_read(&mut self) {
self.file_tree.failed_to_read += 1;
}
pub fn zoom_in(&mut self) {
let current_folder = self.file_tree.get_current_folder();
self.board.zoom_in(current_folder);
self.render();
}
pub fn zoom_out(&mut self) {
let current_folder = self.file_tree.get_current_folder();
self.board.zoom_out(current_folder);
self.render();
}
pub fn reset_zoom(&mut self) {
let current_folder = self.file_tree.get_current_folder();
self.board.reset_zoom(current_folder);
self.render();
}
fn remove_file_from_ui(&mut self, file_to_delete: &FileToDelete) {
self.file_tree.space_freed += file_to_delete.size;
self.file_tree.delete_file(file_to_delete);
self.board.reset_selected_index();
}
}