use crate::app::actions::{ActionContext, ActionMode, InputMode};
use crate::app::keymap::{Action, Keymap, SystemAction};
use crate::app::{NavState, ParentState, PreviewState};
use crate::config::Config;
use crate::core::worker::{WorkerResponse, WorkerTask, Workers};
use crate::ui::overlays::{Overlay, OverlayStack};
use crossterm::event::KeyEvent;
use std::ffi::OsString;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Instant;
pub enum KeypressResult {
Continue,
Consumed,
Quit,
OpenedEditor,
}
#[derive(Debug, Clone, Copy)]
pub struct LayoutMetrics {
pub parent_width: usize,
pub main_width: usize,
pub preview_width: usize,
pub preview_height: usize,
}
impl Default for LayoutMetrics {
fn default() -> Self {
Self {
parent_width: 20,
main_width: 40,
preview_width: 40,
preview_height: 50,
}
}
}
pub struct AppState<'a> {
pub(super) config: &'a Config,
pub(super) keymap: Keymap,
pub(super) metrics: LayoutMetrics,
pub(super) nav: NavState,
pub(super) actions: ActionContext,
pub(super) preview: PreviewState,
pub(super) parent: ParentState,
pub(super) workers: Workers,
pub(super) is_loading: bool,
pub(super) notification_time: Option<Instant>,
pub(super) overlays: OverlayStack,
}
impl<'a> AppState<'a> {
pub fn new(config: &'a Config) -> std::io::Result<Self> {
let workers = Workers::spawn();
let current_dir = std::env::current_dir()?;
let mut app = Self {
config,
keymap: Keymap::from_config(config),
metrics: LayoutMetrics::default(),
nav: NavState::new(current_dir),
actions: ActionContext::default(),
preview: PreviewState::default(),
parent: ParentState::default(),
workers,
is_loading: false,
notification_time: None,
overlays: OverlayStack::new(),
};
app.request_dir_load(None);
app.request_parent_content();
Ok(app)
}
pub fn config(&self) -> &Config {
self.config
}
pub fn metrics(&self) -> &LayoutMetrics {
&self.metrics
}
pub fn metrics_mut(&mut self) -> &mut LayoutMetrics {
&mut self.metrics
}
pub fn nav(&self) -> &NavState {
&self.nav
}
pub fn actions(&self) -> &ActionContext {
&self.actions
}
pub fn preview(&self) -> &PreviewState {
&self.preview
}
pub fn parent(&self) -> &ParentState {
&self.parent
}
pub fn notification_time(&self) -> &Option<Instant> {
&self.notification_time
}
pub fn overlays(&self) -> &OverlayStack {
&self.overlays
}
pub fn overlays_mut(&mut self) -> &mut OverlayStack {
&mut self.overlays
}
pub fn set_notification_time(&mut self, t: Option<Instant>) {
self.notification_time = t;
}
pub fn visible_selected(&self) -> Option<usize> {
if self.nav.entries().is_empty() {
None
} else {
Some(self.nav.selected_idx())
}
}
pub fn has_visible_entries(&self) -> bool {
!self.nav.entries().is_empty()
}
pub fn tick(&mut self) -> bool {
let mut changed = false;
if let Some(expiry) = self.notification_time
&& Instant::now() >= expiry
{
self.notification_time = None;
self.overlays_mut()
.retain(|o| !matches!(o, Overlay::Message { .. }));
changed = true;
}
if self.preview.should_trigger() {
self.request_preview();
changed = true;
}
let current_selection_path = self
.nav
.selected_entry()
.map(|entry| self.nav.current_dir().join(entry.name()));
if let ActionMode::Input {
mode: InputMode::Find,
..
} = self.actions.mode()
&& let Some(query) = self.actions.take_query()
{
if query.is_empty() {
self.actions.clear_find_results();
} else {
self.request_find(query);
}
changed = true;
}
while let Ok(response) = self.workers.response_rx().try_recv() {
changed = true;
match response {
WorkerResponse::DirectoryLoaded {
path,
entries,
focus,
request_id,
} => {
if request_id == self.nav.request_id() && path == self.nav.current_dir() {
self.nav.update_from_worker(path, entries, focus);
self.is_loading = false;
self.request_preview();
self.request_parent_content();
self.refresh_show_info_if_open();
}
else if request_id == self.preview.request_id() {
if current_selection_path.as_ref() == Some(&path) {
self.preview.update_from_entries(entries, request_id);
let pos = current_selection_path
.as_ref()
.and_then(|p| self.nav.get_position().get(p))
.copied()
.unwrap_or(0);
self.preview.set_selected_idx(pos);
}
}
else if request_id == self.parent.request_id() {
let current_name = self
.nav
.current_dir()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
self.parent
.update_from_entries(entries, ¤t_name, request_id, &path);
}
}
WorkerResponse::PreviewLoaded { lines, request_id } => {
if request_id == self.preview.request_id() {
self.preview.update_content(lines, request_id);
}
}
WorkerResponse::OperationComplete {
message: _,
request_id: _,
need_reload,
focus,
} => {
if need_reload {
self.request_dir_load(focus);
self.request_parent_content();
}
}
WorkerResponse::FindResults {
base_dir,
results,
request_id,
} => {
if base_dir == self.nav.current_dir()
&& request_id == self.actions.find_request_id()
{
self.actions.set_find_results(results);
}
}
WorkerResponse::Error(e) => {
self.preview.set_error(e);
}
}
}
changed
}
pub fn handle_keypress(&mut self, key: KeyEvent) -> KeypressResult {
if self.actions.is_input_mode() {
return self.handle_input_mode(key);
}
if let Some(action) = self.keymap.lookup(key) {
match action {
Action::System(SystemAction::Quit) => return KeypressResult::Quit,
Action::Nav(nav_act) => return self.handle_nav_action(nav_act),
Action::File(file_act) => return self.handle_file_action(file_act),
}
}
KeypressResult::Continue
}
pub fn request_dir_load(&mut self, focus: Option<std::ffi::OsString>) {
self.is_loading = true;
let request_id = self.nav.prepare_new_request();
let _ = self.workers.io_tx().send(WorkerTask::LoadDirectory {
path: self.nav.current_dir().to_path_buf(),
focus,
dirs_first: self.config.dirs_first(),
show_hidden: self.config.show_hidden(),
show_system: self.config.show_system(),
case_insensitive: self.config.case_insensitive(),
always_show: Arc::clone(self.config.always_show()),
request_id,
});
}
pub fn request_preview(&mut self) {
if let Some(entry) = self.nav.selected_shown_entry() {
let path = self.nav.current_dir().join(entry.name());
let req_id = self.preview.prepare_new_request(path.clone());
if entry.is_dir() || entry.is_symlink() {
let _ = self.workers.io_tx().send(WorkerTask::LoadDirectory {
path,
focus: None,
dirs_first: self.config.dirs_first(),
show_hidden: self.config.show_hidden(),
show_system: self.config.show_system(),
case_insensitive: self.config.case_insensitive(),
always_show: Arc::clone(self.config.always_show()),
request_id: req_id,
});
} else {
let preview_options = self.config.display().preview_options();
let preview_method = preview_options.method().clone();
let bat_args = self
.config
.bat_args_for_preview(self.metrics.preview_width)
.into_iter()
.map(OsString::from)
.collect();
let _ = self.workers.preview_tx().send(WorkerTask::LoadPreview {
path,
max_lines: self.metrics.preview_height,
pane_width: self.metrics.preview_width,
preview_method,
args: bat_args,
request_id: req_id,
});
}
} else {
self.preview.clear();
}
}
pub fn request_parent_content(&mut self) {
if let Some(parent_path) = self.nav.current_dir().parent() {
let parent_path_buf = parent_path.to_path_buf();
if self.parent.should_request(&parent_path_buf) {
let req_id = self.parent.request_id();
let _ = self.workers.io_tx().send(WorkerTask::LoadDirectory {
path: parent_path_buf,
focus: None,
dirs_first: self.config.dirs_first(),
show_hidden: self.config.show_hidden(),
show_system: self.config.show_system(),
case_insensitive: self.config.case_insensitive(),
always_show: Arc::clone(self.config.always_show()),
request_id: req_id,
});
}
} else {
self.parent.clear();
}
}
pub fn request_find(&mut self, query: String) {
self.actions.cancel_find();
let request_id = self.actions.prepare_new_find_request();
let cancel_token = Arc::new(AtomicBool::new(false));
self.actions
.set_cancel_find_token(Arc::clone(&cancel_token));
let _ = self.workers.find_tx().send(WorkerTask::FindRecursive {
base_dir: self.nav.current_dir().to_path_buf(),
query,
max_results: self.config().max_find_results(),
request_id,
cancel: cancel_token,
});
}
}