1use std::collections::HashSet;
2use std::ffi::OsString;
3use std::fs::File;
4use std::io::{BufRead, BufReader, Read, Seek};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::thread;
8
9use crossbeam_channel::{Receiver, Sender};
10use unicode_width::UnicodeWidthChar;
11
12use crate::file_manager::{FileEntry, browse_dir};
13use crate::formatter::Formatter;
14
15pub enum WorkerTask {
16 LoadDirectory {
17 path: PathBuf,
18 focus: Option<OsString>,
19 dirs_first: bool,
20 show_hidden: bool,
21 show_system: bool,
22 case_insensitive: bool,
23 always_show: Arc<HashSet<OsString>>,
24 pane_width: usize,
25 request_id: u64,
26 },
27 LoadPreview {
28 path: PathBuf,
29 max_lines: usize,
30 pane_width: usize,
31 request_id: u64,
32 },
33 FileOp {
34 op: FileOperation,
35 request_id: u64,
36 },
37}
38
39pub enum FileOperation {
40 Delete(Vec<PathBuf>),
41 Rename {
42 old: PathBuf,
43 new: PathBuf,
44 },
45 Copy {
46 src: Vec<PathBuf>,
47 dest: PathBuf,
48 cut: bool,
49 focus: Option<OsString>,
50 },
51 Create {
52 path: PathBuf,
53 is_dir: bool,
54 },
55}
56
57pub enum WorkerResponse {
58 DirectoryLoaded {
59 path: PathBuf,
60 entries: Vec<FileEntry>,
61 focus: Option<OsString>,
62 request_id: u64,
63 },
64 PreviewLoaded {
65 lines: Vec<String>,
66 request_id: u64,
67 },
68 OperationComplete {
69 message: String,
70 request_id: u64,
71 need_reload: bool,
72 focus: Option<OsString>,
73 },
74 Error(String),
75}
76
77pub fn start_worker(task_rx: Receiver<WorkerTask>, res_tx: Sender<WorkerResponse>) {
78 thread::spawn(move || {
79 while let Ok(task) = task_rx.recv() {
80 match task {
81 WorkerTask::LoadDirectory {
82 path,
83 focus,
84 dirs_first,
85 show_hidden,
86 show_system,
87 case_insensitive,
88 always_show,
89 pane_width,
90 request_id,
91 } => match browse_dir(&path) {
92 Ok(mut entries) => {
93 let formatter = Formatter::new(
94 dirs_first,
95 show_hidden,
96 show_system,
97 case_insensitive,
98 always_show,
99 pane_width,
100 );
101 formatter.filter_entries(&mut entries);
102 let _ = res_tx.send(WorkerResponse::DirectoryLoaded {
103 path,
104 entries,
105 focus,
106 request_id,
107 });
108 }
109 Err(e) => {
110 let _ = res_tx.send(WorkerResponse::Error(format!("I/O Error: {}", e)));
111 }
112 },
113 WorkerTask::LoadPreview {
114 path,
115 max_lines,
116 pane_width,
117 request_id,
118 } => {
119 let lines = safe_read_preview(&path, max_lines, pane_width);
120 let _ = res_tx.send(WorkerResponse::PreviewLoaded { lines, request_id });
121 }
122 WorkerTask::FileOp { op, request_id } => {
123 let mut focus_target: Option<OsString> = None;
124 let result: Result<String, String> = match op {
125 FileOperation::Delete(paths) => {
126 for p in paths {
127 let _ = if p.is_dir() {
128 std::fs::remove_dir_all(p)
129 } else {
130 std::fs::remove_file(p)
131 };
132 }
133 Ok("Items deleted".to_string())
134 }
135 FileOperation::Rename { old, new } => {
136 focus_target = new.file_name().map(|n| n.to_os_string());
137 std::fs::rename(old, new)
138 .map(|_| "Renamed".into())
139 .map_err(|e| e.to_string())
140 }
141 FileOperation::Create { path, is_dir } => {
142 focus_target = path.file_name().map(|n| n.to_os_string());
143 let res = if is_dir {
144 std::fs::create_dir_all(&path)
145 } else {
146 std::fs::File::create(&path).map(|_| ())
147 };
148 res.map(|_| "Created".into()).map_err(|e| e.to_string())
149 }
150 FileOperation::Copy {
151 src,
152 dest,
153 cut,
154 focus,
155 } => {
156 focus_target = focus;
157 for s in src {
158 if let Some(name) = s.file_name() {
159 let target = dest.join(name);
160 let _ = if cut {
161 std::fs::rename(s, target)
162 } else {
163 std::fs::copy(s, target).map(|_| ())
164 };
165 }
166 }
167 Ok("Pasted".into())
168 }
169 };
170
171 match result {
172 Ok(msg) => {
173 let _ = res_tx.send(WorkerResponse::OperationComplete {
174 message: msg,
175 request_id,
176 need_reload: true,
177 focus: focus_target, });
179 }
180 Err(e) => {
181 let _ = res_tx.send(WorkerResponse::Error(format!("Op Error: {}", e)));
182 }
183 }
184 }
185 }
186 }
187 });
188}
189
190fn sanitize_to_exact_width(line: &str, pane_width: usize) -> String {
192 let mut out = String::with_capacity(pane_width);
193 let mut current_w = 0;
194
195 for char in line.chars() {
196 if char == '\t' {
197 let space_count = 4 - (current_w % 4);
198 if current_w + space_count > pane_width {
199 break;
200 }
201 out.push_str(&" ".repeat(space_count));
202 current_w += space_count;
203 continue;
204 }
205
206 if char.is_control() {
207 continue;
208 }
209
210 let w = char.width().unwrap_or(0);
211 if current_w + w > pane_width {
212 break;
213 }
214
215 out.push(char);
216 current_w += w;
217 }
218
219 if current_w < pane_width {
221 out.push_str(&" ".repeat(pane_width - current_w));
222 }
223
224 out
225}
226
227fn preview_directory(path: &Path, max_lines: usize, pane_width: usize) -> Vec<String> {
228 match browse_dir(path) {
229 Ok(entries) => {
230 let mut lines = Vec::with_capacity(max_lines + 1);
231
232 for e in entries.iter().take(max_lines) {
234 let suffix = if e.is_dir() { "/" } else { "" };
235 let display_name = format!("{}{}", e.name().to_string_lossy(), suffix);
236
237 lines.push(sanitize_to_exact_width(&display_name, pane_width));
239 }
240
241 if lines.is_empty() {
243 lines.push(sanitize_to_exact_width("[empty directory]", pane_width));
244 }
245 else if entries.len() > max_lines {
247 lines.pop();
248 lines.push(sanitize_to_exact_width("...", pane_width));
249 }
250
251 while lines.len() < max_lines {
255 lines.push(" ".repeat(pane_width));
256 }
257
258 lines
259 }
260 Err(e) => {
261 let mut err_lines = vec![sanitize_to_exact_width(
262 &format!("[Error: {}]", e),
263 pane_width,
264 )];
265 while err_lines.len() < max_lines {
267 err_lines.push(" ".repeat(pane_width));
268 }
269 err_lines
270 }
271 }
272}
273
274fn safe_read_preview(path: &Path, max_lines: usize, pane_width: usize) -> Vec<String> {
275 let max_lines = std::cmp::max(max_lines, 3);
276
277 let Ok(meta) = std::fs::metadata(path) else {
279 return vec![sanitize_to_exact_width(
280 "[Error: Access Denied]",
281 pane_width,
282 )];
283 };
284
285 if path.is_dir() {
286 return preview_directory(path, max_lines, pane_width);
287 }
288
289 const MAX_PREVIEW_SIZE: u64 = 10 * 1024 * 1024;
291 if meta.len() > MAX_PREVIEW_SIZE {
292 return vec![sanitize_to_exact_width(
293 "[File too large for preview]",
294 pane_width,
295 )];
296 }
297
298 if !meta.is_file() {
299 return vec![sanitize_to_exact_width("[Not a regular file]", pane_width)];
300 }
301
302 match File::open(path) {
304 Ok(mut file) => {
305 let mut buffer = [0u8; 1024];
307 let n = file.read(&mut buffer).unwrap_or(0);
308 if buffer[..n].contains(&0) {
309 return vec![sanitize_to_exact_width(
310 "[Binary file - preview hidden]",
311 pane_width,
312 )];
313 }
314
315 let _ = file.rewind();
316
317 let reader = BufReader::new(file);
318 let mut preview_lines = Vec::with_capacity(max_lines);
319
320 for line_result in reader.lines().take(max_lines) {
321 match line_result {
322 Ok(line) => {
323 preview_lines.push(sanitize_to_exact_width(&line, pane_width));
324 }
325 Err(_) => break,
326 }
327 }
328
329 if preview_lines.is_empty() {
330 preview_lines.push(sanitize_to_exact_width("[Empty file]", pane_width));
331 }
332
333 preview_lines
334 }
335 Err(e) => vec![sanitize_to_exact_width(
336 &format!("[Error reading file: {}]", e),
337 pane_width,
338 )],
339 }
340}