use crate::app::keymap::KeyPrefix;
use crate::app::nav::NavState;
use crate::core::proc::FindResult;
use crate::core::worker::{FileOperation, WorkerTask};
use crossbeam_channel::Sender;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use std::time::Instant;
#[derive(Clone, PartialEq)]
pub(crate) enum ActionMode {
Normal,
Input { mode: InputMode, prompt: String },
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum InputMode {
Rename,
NewFile,
NewFolder,
Filter,
ConfirmDelete { is_trash: bool },
Find,
MoveFile,
GoToPath,
}
pub(crate) struct ActionContext {
mode: ActionMode,
input_buffer: String,
input_cursor_pos: usize,
clipboard: Option<HashSet<PathBuf>>,
autocomplete: AutoCompleteState,
prefix_recognizer: KeyPrefix,
is_cut: bool,
find: FindState,
}
impl ActionContext {
#[inline]
pub(crate) fn mode(&self) -> &ActionMode {
&self.mode
}
#[inline]
pub(crate) fn input_buffer(&self) -> &str {
&self.input_buffer
}
#[inline]
pub(crate) fn input_cursor_pos(&self) -> usize {
self.input_cursor_pos
}
pub(crate) fn prefix_recognizer_mut(&mut self) -> &mut KeyPrefix {
&mut self.prefix_recognizer
}
#[inline]
pub(crate) fn clipboard(&self) -> &Option<HashSet<PathBuf>> {
&self.clipboard
}
pub(crate) fn clipboard_mut(&mut self) -> &mut Option<HashSet<PathBuf>> {
&mut self.clipboard
}
pub(crate) fn autocomplete_mut(&mut self) -> &mut AutoCompleteState {
&mut self.autocomplete
}
pub(crate) fn find_state_mut(&mut self) -> &mut FindState {
&mut self.find
}
#[inline]
pub(crate) fn find_results(&self) -> &[FindResult] {
self.find.results()
}
#[inline]
pub(crate) fn find_selected(&self) -> usize {
self.find.selected()
}
pub(crate) fn set_find_results(&mut self, results: Vec<FindResult>) {
self.find.set_results(results)
}
pub(crate) fn clear_find_results(&mut self) {
self.find.clear_results()
}
#[inline]
pub(crate) fn find_request_id(&self) -> u64 {
self.find.request_id()
}
pub(crate) fn prepare_new_find_request(&mut self) -> u64 {
self.find.prepare_new_request()
}
pub(crate) fn take_query(&mut self) -> Option<String> {
self.find.take_query(&self.input_buffer)
}
pub(crate) fn find_debounce(&mut self, delay: Duration) {
self.find.set_debounce(delay);
}
pub(crate) fn cancel_find(&mut self) {
self.find.cancel_current();
}
pub(crate) fn set_cancel_find_token(&mut self, token: Arc<AtomicBool>) {
self.find.set_cancel(token);
}
pub(crate) fn set_input_buffer(&mut self, new_buf: String) {
self.input_cursor_pos = new_buf.len();
self.input_buffer = new_buf;
}
#[inline]
pub(crate) fn is_input_mode(&self) -> bool {
matches!(self.mode, ActionMode::Input { .. })
}
pub(crate) fn enter_mode(&mut self, mode: ActionMode, initial_value: String) {
self.mode = mode;
self.input_buffer = initial_value;
self.input_cursor_pos = self.input_buffer.len();
}
pub(crate) fn exit_mode(&mut self) {
self.mode = ActionMode::Normal;
self.input_buffer.clear();
self.find.reset();
self.autocomplete.reset();
}
pub(crate) fn action_delete(
&mut self,
nav: &mut NavState,
worker_tx: &Sender<WorkerTask>,
move_to_trash: bool,
) {
let targets = nav.get_action_targets();
if targets.is_empty() {
return;
}
let _ = worker_tx.send(WorkerTask::FileOp {
op: FileOperation::Delete(targets.into_iter().collect(), move_to_trash),
});
nav.clear_markers();
}
pub(crate) fn action_copy(&mut self, nav: &mut NavState, is_cut: bool) {
let mut set = HashSet::new();
if !nav.markers().is_empty() {
for path in nav.markers() {
set.insert(path.clone());
}
nav.clear_markers();
} else if let Some(entry) = nav.selected_entry() {
set.insert(nav.current_dir().join(entry.name()));
}
if !set.is_empty() {
self.clipboard = Some(set);
self.is_cut = is_cut;
}
}
pub(crate) fn action_paste(&mut self, nav: &mut NavState, worker_tx: &Sender<WorkerTask>) {
if let Some(source) = &self.clipboard {
let first_file_name = source
.iter()
.min()
.and_then(|p| p.file_name())
.map(|n| n.to_os_string());
let _ = worker_tx.send(WorkerTask::FileOp {
op: FileOperation::Copy {
src: source.iter().cloned().collect(),
dest: nav.current_dir().to_path_buf(),
cut: self.is_cut,
focus: first_file_name,
},
});
if self.is_cut {
self.clipboard = None;
}
nav.clear_markers();
}
}
pub(crate) fn action_filter(&mut self, nav: &mut NavState) {
nav.set_filter(self.input_buffer.clone());
}
pub(crate) fn action_rename(&mut self, nav: &mut NavState, worker_tx: &Sender<WorkerTask>) {
if self.input_buffer.is_empty() {
return;
}
if let Some(entry) = nav.selected_entry() {
let old_path = nav.current_dir().join(entry.name());
let new_path = old_path.with_file_name(&self.input_buffer);
let _ = worker_tx.send(WorkerTask::FileOp {
op: FileOperation::Rename {
old: old_path,
new: new_path,
},
});
}
self.exit_mode();
}
pub(crate) fn action_create(
&mut self,
nav: &mut NavState,
is_dir: bool,
worker_tx: &Sender<WorkerTask>,
) {
if self.input_buffer.is_empty() {
return;
}
let path = nav.current_dir().join(&self.input_buffer);
let _ = worker_tx.send(WorkerTask::FileOp {
op: FileOperation::Create { path, is_dir },
});
self.exit_mode();
}
pub(crate) fn actions_move(
&mut self,
nav: &mut NavState,
destination: PathBuf,
worker_tx: &Sender<WorkerTask>,
) {
let targets = nav.get_action_targets();
if targets.is_empty() {
return;
}
let _ = worker_tx.send(WorkerTask::FileOp {
op: FileOperation::Copy {
src: targets.into_iter().collect(),
dest: destination,
cut: true,
focus: None,
},
});
nav.clear_markers();
}
pub(crate) fn action_clear_clipboard(&mut self) {
if self.clipboard.is_some() {
self.clipboard = None;
self.is_cut = false;
}
}
pub(crate) fn action_move_cursor_left(&mut self) {
if self.input_cursor_pos > 0 {
self.input_cursor_pos -= 1;
}
}
pub(crate) fn action_move_cursor_right(&mut self) {
if self.input_cursor_pos < self.input_buffer.len() {
self.input_cursor_pos += 1;
}
}
pub(crate) fn action_insert_at_cursor(&mut self, ch: char) {
self.input_buffer.insert(self.input_cursor_pos, ch);
self.input_cursor_pos += ch.len_utf8();
}
pub(crate) fn action_backspace_at_cursor(&mut self) {
if self.input_cursor_pos > 0
&& let Some((previous, _)) = self.input_buffer[..self.input_cursor_pos]
.char_indices()
.next_back()
{
self.input_buffer.remove(previous);
self.input_cursor_pos = previous;
}
}
pub(crate) fn action_cursor_home(&mut self) {
self.input_cursor_pos = 0;
}
pub(crate) fn action_cursor_end(&mut self) {
self.input_cursor_pos = self.input_buffer.len();
}
}
impl Default for ActionContext {
fn default() -> Self {
Self {
mode: ActionMode::Normal,
input_buffer: String::new(),
input_cursor_pos: 0,
clipboard: None,
autocomplete: AutoCompleteState::default(),
prefix_recognizer: KeyPrefix::new(Duration::from_secs(4)),
is_cut: false,
find: FindState::default(),
}
}
}
#[derive(Default)]
pub(crate) struct FindState {
cache: Vec<FindResult>,
request_id: u64,
debounce: Option<Instant>,
last_query: String,
selected: usize,
cancel: Option<Arc<AtomicBool>>,
}
impl FindState {
#[inline]
fn results(&self) -> &[FindResult] {
&self.cache
}
#[inline]
fn request_id(&self) -> u64 {
self.request_id
}
#[inline]
fn selected(&self) -> usize {
self.selected
}
fn cancel_current(&mut self) {
if let Some(token) = self.cancel.take() {
token.store(true, Ordering::Relaxed);
}
}
fn set_results(&mut self, results: Vec<FindResult>) {
self.cache = results;
self.selected = 0;
}
fn set_cancel(&mut self, token: Arc<AtomicBool>) {
self.cancel = Some(token);
}
fn clear_results(&mut self) {
self.cache.clear();
}
fn prepare_new_request(&mut self) -> u64 {
self.request_id = self.request_id.wrapping_add(1);
self.request_id
}
fn set_debounce(&mut self, delay: Duration) {
self.debounce = Some(Instant::now() + delay);
}
fn take_query(&mut self, current_query: &str) -> Option<String> {
let until = self.debounce?;
if Instant::now() < until {
return None;
}
self.debounce = None;
if current_query == self.last_query {
self.last_query.clear();
return None;
}
self.last_query.clear();
self.last_query.push_str(current_query);
Some(current_query.to_string())
}
fn reset(&mut self) {
self.cancel_current();
self.cache.clear();
self.debounce = None;
self.last_query.clear();
}
pub(crate) fn select_next(&mut self) {
if self.selected + 1 < self.cache.len() {
self.selected += 1;
}
}
pub(crate) fn select_prev(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
}
#[derive(Default)]
pub(crate) struct AutoCompleteState {
suggestions: Vec<String>,
index: usize,
last_input: String,
}
impl AutoCompleteState {
#[inline]
pub(crate) fn suggestions(&self) -> &Vec<String> {
&self.suggestions
}
#[inline]
pub(crate) fn last_input(&self) -> &str {
&self.last_input
}
pub(crate) fn reset(&mut self) {
self.suggestions.clear();
self.index = 0;
self.last_input.clear();
}
pub(crate) fn update(&mut self, suggestions: Vec<String>, input: &str) {
self.suggestions = suggestions;
self.index = 0;
self.last_input = input.to_string();
}
pub(crate) fn advance(&mut self) {
if !self.suggestions.is_empty() {
self.index = (self.index + 1) % self.suggestions.len();
}
}
pub(crate) fn current(&self) -> Option<&String> {
self.suggestions.get(self.index)
}
}