runa_tui/
worker.rs

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, // CRITICA:
178                            });
179                        }
180                        Err(e) => {
181                            let _ = res_tx.send(WorkerResponse::Error(format!("Op Error: {}", e)));
182                        }
183                    }
184                }
185            }
186        }
187    });
188}
189
190// Calculating the pane widht and clean the output to the widht of the pane
191fn 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 the string is shorter than the pane, fill it with spaces.
220    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            // Process existing entries
233            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                // Sanitize and pad to exact width
238                lines.push(sanitize_to_exact_width(&display_name, pane_width));
239            }
240
241            // Handle Empty State
242            if lines.is_empty() {
243                lines.push(sanitize_to_exact_width("[empty directory]", pane_width));
244            }
245            // Handle Overflow Indicator
246            else if entries.len() > max_lines {
247                lines.pop();
248                lines.push(sanitize_to_exact_width("...", pane_width));
249            }
250
251            // If the folder has fewer items than the height of the pane,
252            // it fills the remaining lines with empty padded strings.
253            // This physically erases old content from the bottom of the pane.
254            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            // Fill error screen with blanks too
266            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    // Metadata check
278    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    // Size Check
290    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    // File Read and binary Check
303    match File::open(path) {
304        Ok(mut file) => {
305            // First, peek for null bytes to detect binary files
306            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}