use crate::config::display::PreviewMethod;
use crate::core::{
FileEntry, FindResult, Formatter, browse_dir, find, preview_bat, safe_read_preview,
};
use crate::utils::{copy_recursive, get_unused_path};
use crossbeam_channel::{Receiver, Sender, bounded, unbounded};
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
pub struct Workers {
io_tx: Sender<WorkerTask>,
find_tx: Sender<WorkerTask>,
preview_tx: Sender<WorkerTask>,
fileop_tx: Sender<WorkerTask>,
response_rx: Receiver<WorkerResponse>,
}
impl Workers {
pub fn spawn() -> Self {
let (io_tx, io_rx) = unbounded::<WorkerTask>();
let (preview_tx, preview_rx) = unbounded::<WorkerTask>();
let (find_tx, find_rx) = bounded::<WorkerTask>(1);
let (fileop_tx, fileop_rx) = unbounded::<WorkerTask>();
let (res_tx, response_rx) = unbounded::<WorkerResponse>();
start_io_worker(io_rx, res_tx.clone());
start_preview_worker(preview_rx, res_tx.clone());
start_find_worker(find_rx, res_tx.clone());
start_fileop_worker(fileop_rx, res_tx.clone());
Self {
io_tx,
preview_tx,
find_tx,
fileop_tx,
response_rx,
}
}
pub fn io_tx(&self) -> &Sender<WorkerTask> {
&self.io_tx
}
pub fn preview_tx(&self) -> &Sender<WorkerTask> {
&self.preview_tx
}
pub fn find_tx(&self) -> &Sender<WorkerTask> {
&self.find_tx
}
pub fn fileop_tx(&self) -> &Sender<WorkerTask> {
&self.fileop_tx
}
pub fn response_rx(&self) -> &Receiver<WorkerResponse> {
&self.response_rx
}
}
pub enum WorkerTask {
LoadDirectory {
path: PathBuf,
focus: Option<OsString>,
dirs_first: bool,
show_hidden: bool,
show_system: bool,
case_insensitive: bool,
always_show: Arc<HashSet<OsString>>,
request_id: u64,
},
LoadPreview {
path: PathBuf,
max_lines: usize,
pane_width: usize,
preview_method: PreviewMethod,
args: Vec<OsString>,
request_id: u64,
},
FileOp {
op: FileOperation,
request_id: u64,
},
FindRecursive {
base_dir: PathBuf,
query: String,
max_results: usize,
cancel: Arc<AtomicBool>,
request_id: u64,
},
}
pub enum FileOperation {
Delete(Vec<PathBuf>),
Rename {
old: PathBuf,
new: PathBuf,
},
Copy {
src: Vec<PathBuf>,
dest: PathBuf,
cut: bool,
focus: Option<OsString>,
},
Create {
path: PathBuf,
is_dir: bool,
},
}
#[derive(Debug)]
pub enum WorkerResponse {
DirectoryLoaded {
path: PathBuf,
entries: Vec<FileEntry>,
focus: Option<OsString>,
request_id: u64,
},
PreviewLoaded {
lines: Vec<String>,
request_id: u64,
},
OperationComplete {
message: String,
request_id: u64,
need_reload: bool,
focus: Option<OsString>,
},
FindResults {
base_dir: PathBuf,
results: Vec<FindResult>,
request_id: u64,
},
Error(String),
}
fn start_io_worker(task_rx: Receiver<WorkerTask>, res_tx: Sender<WorkerResponse>) {
thread::spawn(move || {
while let Ok(task) = task_rx.recv() {
let WorkerTask::LoadDirectory {
path,
focus,
dirs_first,
show_hidden,
show_system,
case_insensitive,
always_show,
request_id,
} = task
else {
continue;
};
match browse_dir(&path) {
Ok(mut entries) => {
let formatter = Formatter::new(
dirs_first,
show_hidden,
show_system,
case_insensitive,
always_show,
);
formatter.filter_entries(&mut entries);
let _ = res_tx.send(WorkerResponse::DirectoryLoaded {
path,
entries,
focus,
request_id,
});
}
Err(e) => {
let _ = res_tx.send(WorkerResponse::Error(format!("I/O Error: {}", e)));
}
}
}
});
}
fn start_preview_worker(task_rx: Receiver<WorkerTask>, res_tx: Sender<WorkerResponse>) {
thread::spawn(move || {
while let Ok(task) = task_rx.recv() {
let WorkerTask::LoadPreview {
mut path,
mut max_lines,
mut pane_width,
mut preview_method,
mut args,
mut request_id,
} = task
else {
continue;
};
while let Ok(next) = task_rx.try_recv() {
if let WorkerTask::LoadPreview {
path: p,
max_lines: m,
pane_width: w,
preview_method: pm,
args: a,
request_id: id,
} = next
{
path = p;
max_lines = m;
pane_width = w;
preview_method = pm;
args = a;
request_id = id;
}
}
let lines = match preview_method {
PreviewMethod::Internal => safe_read_preview(&path, max_lines, pane_width),
PreviewMethod::Bat => match preview_bat(&path, max_lines, args.as_slice()) {
Ok(lines) => lines,
Err(_) => safe_read_preview(&path, max_lines, pane_width),
},
};
let _ = res_tx.send(WorkerResponse::PreviewLoaded { lines, request_id });
}
});
}
fn start_find_worker(task_rx: Receiver<WorkerTask>, res_tx: Sender<WorkerResponse>) {
thread::spawn(move || {
while let Ok(task) = task_rx.recv() {
let WorkerTask::FindRecursive {
mut base_dir,
mut query,
mut max_results,
mut request_id,
mut cancel,
} = task
else {
continue;
};
while let Ok(next) = task_rx.try_recv() {
if let WorkerTask::FindRecursive {
base_dir: base,
query: q,
max_results: max,
request_id: id,
cancel: c,
} = next
{
base_dir = base;
query = q;
max_results = max;
request_id = id;
cancel = c;
}
}
let mut results = Vec::new();
let _ = find(
&base_dir,
&query,
&mut results,
Arc::clone(&cancel),
max_results,
);
if results.len() > max_results {
results.truncate(max_results);
}
if cancel.load(Ordering::Acquire) {
continue;
}
let _ = res_tx.send(WorkerResponse::FindResults {
base_dir,
results,
request_id,
});
}
});
}
fn start_fileop_worker(task_rx: Receiver<WorkerTask>, res_tx: Sender<WorkerResponse>) {
thread::spawn(move || {
while let Ok(task) = task_rx.recv() {
let WorkerTask::FileOp { op, request_id } = task else {
continue;
};
let mut focus_target: Option<OsString> = None;
let result: Result<String, String> = match op {
FileOperation::Delete(paths) => {
for p in paths {
let res = if p.is_dir() {
std::fs::remove_dir_all(&p)
} else {
std::fs::remove_file(&p)
};
if let Err(e) = res {
eprintln!("Failed to delete {}: {}", p.display(), e);
}
}
Ok("Items deleted".to_string())
}
FileOperation::Rename { old, new } => {
let target = new;
if target.exists() {
Err(format!(
"Rename failed: '{}' already exists",
target.file_name().unwrap_or_default().to_string_lossy()
))
} else {
focus_target = target.file_name().map(|n| n.to_os_string());
std::fs::rename(old, &target)
.map(|_| "Renamed".into())
.map_err(|e| e.to_string())
}
}
FileOperation::Create { path, is_dir } => {
let target = get_unused_path(&path);
focus_target = target.file_name().map(|n| n.to_os_string());
let res = if is_dir {
std::fs::create_dir_all(&target)
} else {
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&target)
.map(|_| ())
};
res.map(|_| "Created".into()).map_err(|e| e.to_string())
}
FileOperation::Copy {
src,
dest,
cut,
focus,
} => {
focus_target = focus;
for s in src {
if let Some(name) = s.file_name() {
let target = get_unused_path(&dest.join(name));
if let Some(ref ft) = focus_target
&& ft == name
{
focus_target = target.file_name().map(|n| n.to_os_string());
}
let _ = if cut {
std::fs::rename(s, &target)
} else if s.is_dir() {
copy_recursive(&s, &target)
} else {
std::fs::copy(s, &target).map(|_| ())
};
}
}
Ok("Pasted".into())
}
};
match result {
Ok(msg) => {
let _ = res_tx.send(WorkerResponse::OperationComplete {
message: msg,
request_id,
need_reload: true,
focus: focus_target,
});
}
Err(e) => {
let _ = res_tx.send(WorkerResponse::Error(format!("Op Error: {}", e)));
}
}
}
});
}