use std::collections::HashSet;
use std::ffi::OsString;
use std::fs::File;
use std::io::{BufRead, BufReader, ErrorKind, Read, Seek};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use crossbeam_channel::{Receiver, Sender};
use unicode_width::UnicodeWidthChar;
use crate::file_manager::{FileEntry, browse_dir};
use crate::formatter::Formatter;
use crate::utils::get_unused_path;
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>>,
pane_width: usize,
request_id: u64,
},
LoadPreview {
path: PathBuf,
max_lines: usize,
pane_width: usize,
request_id: u64,
},
FileOp {
op: FileOperation,
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,
},
}
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>,
},
Error(String),
}
pub fn start_worker(task_rx: Receiver<WorkerTask>, res_tx: Sender<WorkerResponse>) {
thread::spawn(move || {
while let Ok(task) = task_rx.recv() {
match task {
WorkerTask::LoadDirectory {
path,
focus,
dirs_first,
show_hidden,
show_system,
case_insensitive,
always_show,
pane_width,
request_id,
} => match browse_dir(&path) {
Ok(mut entries) => {
let formatter = Formatter::new(
dirs_first,
show_hidden,
show_system,
case_insensitive,
always_show,
pane_width,
);
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)));
}
},
WorkerTask::LoadPreview {
path,
max_lines,
pane_width,
request_id,
} => {
let lines = safe_read_preview(&path, max_lines, pane_width);
let _ = res_tx.send(WorkerResponse::PreviewLoaded { lines, request_id });
}
WorkerTask::FileOp { op, request_id } => {
let mut focus_target: Option<OsString> = None;
let result: Result<String, String> = match op {
FileOperation::Delete(paths) => {
for p in paths {
let _ = if p.is_dir() {
std::fs::remove_dir_all(p)
} else {
std::fs::remove_file(p)
};
}
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 {
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)));
}
}
}
}
}
});
}
fn sanitize_to_exact_width(line: &str, pane_width: usize) -> String {
let mut out = String::with_capacity(pane_width);
let mut current_w = 0;
for char in line.chars() {
if char == '\t' {
let space_count = 4 - (current_w % 4);
if current_w + space_count > pane_width {
break;
}
out.push_str(&" ".repeat(space_count));
current_w += space_count;
continue;
}
if char.is_control() {
continue;
}
let w = char.width().unwrap_or(0);
if current_w + w > pane_width {
break;
}
out.push(char);
current_w += w;
}
if current_w < pane_width {
out.push_str(&" ".repeat(pane_width - current_w));
}
out
}
fn preview_directory(path: &Path, max_lines: usize, pane_width: usize) -> Vec<String> {
match browse_dir(path) {
Ok(entries) => {
let mut lines = Vec::with_capacity(max_lines + 1);
for e in entries.iter().take(max_lines) {
let suffix = if e.is_dir() { "/" } else { "" };
let display_name = format!("{}{}", e.name().to_string_lossy(), suffix);
lines.push(sanitize_to_exact_width(&display_name, pane_width));
}
if lines.is_empty() {
lines.push(sanitize_to_exact_width("[empty directory]", pane_width));
}
else if entries.len() > max_lines {
lines.pop();
lines.push(sanitize_to_exact_width("...", pane_width));
}
while lines.len() < max_lines {
lines.push(" ".repeat(pane_width));
}
lines
}
Err(e) => {
let mut err_lines = vec![sanitize_to_exact_width(
&format!("[Error: {}]", e),
pane_width,
)];
while err_lines.len() < max_lines {
err_lines.push(" ".repeat(pane_width));
}
err_lines
}
}
}
fn safe_read_preview(path: &Path, max_lines: usize, pane_width: usize) -> Vec<String> {
const MIN_PREVIEW_LINES: usize = 3;
const MAX_PREVIEW_SIZE: u64 = 10 * 1024 * 1024;
const HEADER_PEEK_BYTES: usize = 8;
const BINARY_PEEK_BYTES: usize = 1024;
let max_lines = std::cmp::max(max_lines, MIN_PREVIEW_LINES);
let Ok(meta) = std::fs::metadata(path) else {
return vec![sanitize_to_exact_width(
"[Error: Access Denied]",
pane_width,
)];
};
if path.is_dir() {
return preview_directory(path, max_lines, pane_width);
}
if meta.len() > MAX_PREVIEW_SIZE {
return vec![sanitize_to_exact_width(
"[File too large for preview]",
pane_width,
)];
}
if !meta.is_file() {
return vec![sanitize_to_exact_width("[Not a regular file]", pane_width)];
}
match File::open(path) {
Ok(mut file) => {
let mut header = [0u8; HEADER_PEEK_BYTES];
let read_bytes = file.read(&mut header).unwrap_or(0);
if read_bytes >= 5 && &header[..5] == b"%PDF-" {
return vec![sanitize_to_exact_width(
"[Binary file - preview hidden]",
pane_width,
)];
}
let mut buffer = [0u8; BINARY_PEEK_BYTES];
let n = file.read(&mut buffer).unwrap_or(0);
if buffer[..n].contains(&0) {
return vec![sanitize_to_exact_width(
"[Binary file - preview hidden]",
pane_width,
)];
}
let _ = file.rewind();
let reader = BufReader::new(file);
let mut preview_lines = Vec::with_capacity(max_lines);
for line_result in reader.lines().take(max_lines) {
match line_result {
Ok(line) => {
preview_lines.push(sanitize_to_exact_width(&line, pane_width));
}
Err(_) => break,
}
}
if preview_lines.is_empty() {
preview_lines.push(sanitize_to_exact_width("[Empty file]", pane_width));
}
preview_lines
}
Err(e) => {
let msg = match e.kind() {
ErrorKind::PermissionDenied => "[Error: Permission Denied]",
ErrorKind::NotFound => "[Error: File Not Found]",
_ => {
return vec![sanitize_to_exact_width(
&format!("[Error reading file: {}]", e),
pane_width,
)];
}
};
vec![sanitize_to_exact_width(msg, pane_width)]
}
}
}