From 71968f7f64480341d6a279a703bf153ad8eb014b Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 28 Aug 2023 00:49:12 +0200 Subject: [PATCH] updated color scheme, added threaded modes, added a way to set permission bits (chmod) --- README.md | 35 +++ src/main.rs | 61 +++-- src/run.rs | 639 +++++++++++++++++++++++++++++++------------------ src/tasks.rs | 123 ++++++---- src/updates.rs | 26 ++ 5 files changed, 592 insertions(+), 292 deletions(-) diff --git a/README.md b/README.md index 738c233..f1cb154 100755 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ TuiFile can - create new directories - copy, move and delete - quickly open your `$TERM` and `$EDITOR` +- build the file list on a background thread to avoid blocking - add more features (open an issue with ideas if you have any) ## Demo @@ -23,6 +24,7 @@ https://github.com/Dummi26/tuifile/assets/67615357/0b0553c9-72e5-4d38-8537-f6cc3 ### Global +Ctrl+C/D -> quit Ctrl+Up/K -> previous Ctrl+Down/J -> next Ctrl+Left/H -> close @@ -37,9 +39,12 @@ Ctrl+Right/L -> duplicate - S -> Select or toggle current - D -> Deselect all - F -> focus Find/Filter bar +- M -> set Mode bysed on Find/Filter bar - N -> New directory (name taken from find/filter bar text) - C -> Copy selected to this directory - R -> remove selected files and directories (not recursive: also requires selecting the directories content) +- P -> set Permissions (mode taken as base-8 number from find/filter bar text) +- O -> set Owner (and group - TODO!) - 1-9 or 0 -> set recursive depth limit (0 = infinite) - T -> open terminal here - E -> open this file in your editor @@ -50,3 +55,33 @@ Ctrl+Right/L -> duplicate - Enter -> back & filter - Backspace -> delete - type to enter search regex + +## File List Modes + +### Blocking + +This is the simplest mode. If listing all the files takes a long time, the program will be unresponsive. + +To enable, type `b` into the filter bar, go back to files mode, and press `m`. + +### Threaded + +To avoid blocking, this mode performs all filesystem operations in the background. +Can cause flickering and isn't as responsive on fast disks. + +To enable, type `t` into the filter bar, go back to files mode, and press `m`. + +### Timeout + +Like blocking, but after the timeout is reached, tuifile will stop adding more files to the list. +This means that file lists may be incomplete. + +To enable, type `b` into the filter bar, go back to files mode, and press `m`. +Replace `` with a number like `1` or `0.3`. + +### TimeoutThenThreaded + +Like blocking, but after the timeout is reached, tuifile will cancel the operation and restart it in threaded mode. + +To enable, type `t` into the filter bar, go back to files mode, and press `m`. +Replace `` with a number like `1` or `0.3`. diff --git a/src/main.rs b/src/main.rs index d31a3e8..42b3e8f 100755 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,9 @@ mod tasks; mod updates; use std::{ - fs::{self, DirEntry, Metadata}, + fs::{self, Metadata}, io::{self, StdoutLock}, path::PathBuf, - rc::Rc, sync::{Arc, Mutex}, thread::JoinHandle, }; @@ -46,6 +45,7 @@ fn main() -> io::Result<()> { terminal_command: std::env::var("TERM").unwrap_or("alacritty".to_string()), editor_command: std::env::var("EDITOR").unwrap_or("nano".to_string()), live_search: !args.no_live_search, + info_what: vec![0, 1], }; if args.check { eprintln!("Terminal: {}", share.terminal_command); @@ -111,11 +111,7 @@ fn main() -> io::Result<()> { .filter(|e| e.selected) .filter_map(|e| { Some(( - e.entry - .path() - .strip_prefix(&v.current_dir) - .ok()? - .to_owned(), + e.path.strip_prefix(&v.current_dir).ok()?.to_owned(), e.rel_depth == v.scan_files_max_depth, )) }) @@ -193,30 +189,39 @@ struct Share { live_search: bool, terminal_command: String, editor_command: String, + /// 0: size + /// 1: mode (permissions) + info_what: Vec, } impl Share { - fn check_bgtasks(&mut self) -> bool { + /// returns Some if any task has finished. + /// returns Some(true) if at least one of these tasks may have altered files. + /// (this should trigger a rescan) + fn check_bgtasks(&mut self) -> Option { for (i, task) in self.tasks.iter_mut().enumerate() { if task.thread.is_finished() { - self.tasks.remove(i); - return true; + return Some(self.tasks.remove(i).rescan_after); } } - false + None } } struct BackgroundTask { status: Arc>, thread: JoinHandle>, + rescan_after: bool, } impl BackgroundTask { pub fn new( + text: String, func: impl FnOnce(Arc>) -> Result<(), String> + Send + 'static, + rescan_after: bool, ) -> Self { - let status = Arc::new(Mutex::new(String::new())); + let status = Arc::new(Mutex::new(text)); Self { status: Arc::clone(&status), thread: std::thread::spawn(move || func(status)), + rescan_after, } } } @@ -226,6 +231,7 @@ struct TuiFile { current_dir: PathBuf, dir_content: Vec, dir_content_len: usize, + dir_content_builder_task: Option, String>>>>>, scroll: usize, current_index: usize, focus: Focus, @@ -238,15 +244,39 @@ struct TuiFile { last_drawn_files_count: usize, last_files_max_scroll: usize, after_rescanning_files: Vec>, + scan_files_mode: ScanFilesMode, +} +#[derive(Clone)] +enum ScanFilesMode { + /// file-scanning blocks the main thread. + /// prevents flickering. + Blocking, + /// file-scanning doesn't block the main thread. + /// leads to flickering as the file list appears empty until the thread finishes. + Threaded, + /// file-scanning blocks the main thread for up to _ seconds. + /// after the timeout is reached, file scanning is stopped. + /// can lead to incomplete file lists. + Timeout(f32), + /// file-scanning blocks the main thread for up to _ seconds. + /// after the timeout is reached, file-scanning will restart on a thread. + /// prevents flickering but will scan the first files twice if the timeout is reached. + TimeoutThenThreaded(f32), +} +impl Default for ScanFilesMode { + fn default() -> Self { + Self::Blocking + } } #[derive(Clone)] struct DirContent { - entry: Rc, + path: PathBuf, name: String, name_charlen: usize, rel_depth: usize, passes_filter: bool, selected: bool, + info: String, more: DirContentType, } #[derive(Clone)] @@ -257,7 +287,6 @@ enum DirContentType { metadata: Metadata, }, File { - size: String, metadata: Metadata, }, Symlink { @@ -286,6 +315,7 @@ impl TuiFile { current_dir: self.current_dir.clone(), dir_content: self.dir_content.clone(), dir_content_len: self.dir_content_len, + dir_content_builder_task: None, scroll: self.scroll, current_index: self.current_index, focus: self.focus.clone(), @@ -298,6 +328,7 @@ impl TuiFile { last_drawn_files_count: self.last_drawn_files_count, last_files_max_scroll: self.last_files_max_scroll, after_rescanning_files: vec![], + scan_files_mode: ScanFilesMode::default(), } } pub fn new(current_dir: PathBuf) -> io::Result { @@ -310,6 +341,7 @@ impl TuiFile { current_dir, dir_content: vec![], dir_content_len: 0, + dir_content_builder_task: None, scroll: 0, current_index: 0, focus: Focus::Files, @@ -322,6 +354,7 @@ impl TuiFile { last_drawn_files_count: 0, last_files_max_scroll: 0, after_rescanning_files: vec![], + scan_files_mode: ScanFilesMode::default(), }) } fn set_current_index(&mut self, mut i: usize) { diff --git a/src/run.rs b/src/run.rs index a72608b..142aac7 100755 --- a/src/run.rs +++ b/src/run.rs @@ -4,12 +4,16 @@ use crossterm::{cursor, queue, style, terminal, ExecutableCommand}; use regex::RegexBuilder; use crate::updates::Updates; -use crate::{tasks, AppCmd, DirContent, DirContentType, Focus, Share}; +use crate::{ + tasks, AppCmd, BackgroundTask, DirContent, DirContentType, Focus, ScanFilesMode, Share, +}; use std::io::Write; +use std::os::unix::prelude::PermissionsExt; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::rc::Rc; -use std::time::Duration; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; use std::{fs, io}; use crate::TuiFile; @@ -33,87 +37,250 @@ impl TuiFile { } pub fn run(&mut self, share: &mut Share) -> io::Result { loop { - if share.check_bgtasks() { - return Ok(AppCmd::TaskFinished); + if let Some(rescan) = share.check_bgtasks() { + if let Some(task) = &self.dir_content_builder_task { + if let Some(v) = { + let mut temp = task.lock().unwrap(); + temp.take() + } { + self.dir_content_builder_task = None; + after_rescanning_files(self, v); + } + } + if rescan { + return Ok(AppCmd::TaskFinished); + } } // rescan files if necessary + fn after_rescanning_files(s: &mut TuiFile, v: Result, String>) { + s.updates.request_rescanning_files_complete(); + s.updates.request_filter_files(); + match v { + Ok(v) => s.dir_content = v, + Err(err) => { + s.files_status_is_special = true; + s.files_status = err; + } + } + } if self.updates.rescan_files() { self.updates.dont_rescan_files(); - self.updates.request_filter_files(); - self.files_status_is_special = false; - self.dir_content.clear(); - get_files(self, self.current_dir.clone(), 0); - fn get_files(s: &mut TuiFile, dir: PathBuf, depth: usize) { - match fs::read_dir(&dir) { - Err(e) => { - if depth == 0 { - s.dir_content = vec![]; - s.files_status = format!("{e}"); - s.files_status_is_special = true; + if self.dir_content_builder_task.is_none() { + self.dir_content.clear(); + self.files_status_is_special = false; + let (scan_dir_blocking, mut scan_dir_threaded, timeout) = + match self.scan_files_mode { + ScanFilesMode::Blocking => (true, false, None), + ScanFilesMode::Threaded => (false, true, None), + ScanFilesMode::Timeout(t) => (true, false, Some(t)), + ScanFilesMode::TimeoutThenThreaded(t) => (true, true, Some(t)), + }; + if scan_dir_blocking { + let v = get_files( + self.current_dir.clone(), + self.scan_files_max_depth, + &share.info_what, + timeout, + ); + if v.as_ref().is_ok_and(|v| v.1) { + // completed, no need for the threaded fallback. + scan_dir_threaded = false; + } + if !scan_dir_threaded { + after_rescanning_files(self, v.map(|v| v.0)); + } + } + if scan_dir_threaded { + let dir = self.current_dir.clone(); + let max_depth = self.scan_files_max_depth; + let info_what = share.info_what.clone(); + let arc = Arc::new(Mutex::new(None)); + self.dir_content_builder_task = Some(Arc::clone(&arc)); + self.updates.request_redraw_filelist(); + self.updates.request_redraw_infobar(); + share.tasks.push(BackgroundTask::new( + "listing files...".to_string(), + move |_status| { + let v = get_files(dir, max_depth, &info_what, None).map(|v| v.0); + *arc.lock().unwrap() = Some(v); + Ok(()) + }, + false, + )); + } + fn get_files( + dir: PathBuf, + max_depth: usize, + info_what: &Vec, + timeout: Option, + ) -> Result<(Vec, bool), String> { + let mut o = vec![]; + let completed = get_files( + &mut o, + dir, + 0, + max_depth, + info_what, + timeout.map(|v| (Instant::now(), v)), + )?; + // table-style + let mut lengths = vec![]; + for e in o.iter() { + for (i, line) in e.info.lines().enumerate() { + if i >= lengths.len() { + lengths.push(0); + } + if line.len() > lengths[i] { + lengths[i] = line.len(); + } } } - Ok(files) => { - for entry in files { - if let Ok(entry) = entry { - let mut name = entry.file_name().to_string_lossy().into_owned(); - let metadata = entry.metadata(); - let p = entry.path(); - let more = match metadata { - Err(e) => DirContentType::Err(e.to_string()), - Ok(metadata) => { - if metadata.is_symlink() { - DirContentType::Symlink { metadata } - } else if metadata.is_file() { - DirContentType::File { - size: { - let mut bytes = metadata.len(); - let mut i = 0; - loop { - if bytes < 1024 - || i + 1 >= BYTE_UNITS.len() - { - break format!( - "{bytes}{}", - BYTE_UNITS[i] - ); - } else { - i += 1; - // divide by 1024 but cooler - bytes >>= 10; - } - } - }, - metadata, - } - } else if metadata.is_dir() { - DirContentType::Dir { metadata } - } else { - DirContentType::Err(format!( - "not a file, dir or symlink" - )) - } - } - }; - if let DirContentType::Dir { .. } = more { - name.push('/'); + for e in o.iter_mut() { + let src = std::mem::replace(&mut e.info, String::new()); + for (i, line) in src.lines().enumerate() { + let rem = lengths[i] - line.len(); + if line.starts_with('<') { + e.info.push_str(&line[1..]); + for _ in 0..rem { + e.info.push(' '); } - s.dir_content.push(DirContent { - entry: Rc::new(entry), - name_charlen: name.chars().count(), - name, - rel_depth: depth, - passes_filter: true, - selected: false, - more, - }); - if depth < s.scan_files_max_depth { - get_files(s, p, depth + 1); + } else if line.starts_with('>') { + for _ in 0..rem { + e.info.push(' '); + } + e.info.push_str(&line[1..]); + } else { + let r = rem / 2; + for _ in 0..r { + e.info.push(' '); + } + e.info.push_str(&line[1..]); + for _ in 0..(rem - r) { + e.info.push(' '); } } } } + fn get_files( + dir_content: &mut Vec, + dir: PathBuf, + depth: usize, + max_depth: usize, + info_what: &Vec, + time_limit: Option<(Instant, f32)>, + ) -> Result { + match fs::read_dir(&dir) { + Err(e) => { + if depth == 0 { + return Err(format!("{e}")); + } + } + Ok(files) => { + for entry in files { + if let Ok(entry) = entry { + let mut name = + entry.file_name().to_string_lossy().into_owned(); + let metadata = entry.metadata(); + let p = entry.path(); + let info = if let Ok(metadata) = &metadata { + // in each line: + // first char: + // < left-aligned + // > right-aligned + // anything else -> centered + // sep. line: "< | " + let mut info = String::new(); + for info_what in info_what { + match info_what { + 0 => { + let mut bytes = metadata.len(); + let mut i = 0; + loop { + if bytes < 1024 + || i + 1 >= BYTE_UNITS.len() + { + info.push_str(&format!( + "< | \n>{bytes}\n>{}\n", + BYTE_UNITS[i] + )); + break; + } else { + i += 1; + // divide by 1024 but cooler + bytes >>= 10; + } + } + } + 1 => { + info.push_str(&format!( + "< | \n>{:03o}\n", + metadata.permissions().mode() + & 0o777, + )); + } + _ => {} + } + } + info + } else { + String::new() + }; + let more = match metadata { + Err(e) => DirContentType::Err(e.to_string()), + Ok(metadata) => { + if metadata.is_symlink() { + DirContentType::Symlink { metadata } + } else if metadata.is_file() { + DirContentType::File { metadata } + } else if metadata.is_dir() { + DirContentType::Dir { metadata } + } else { + DirContentType::Err(format!( + "not a file, dir or symlink" + )) + } + } + }; + if let DirContentType::Dir { .. } = more { + name.push('/'); + } + dir_content.push(DirContent { + path: entry.path(), + name_charlen: name.chars().count(), + name, + rel_depth: depth, + passes_filter: true, + selected: false, + info, + more, + }); + if let Some((since, max)) = time_limit { + if since.elapsed().as_secs_f32() > max { + return Ok(false); + } + } + if depth < max_depth { + get_files( + dir_content, + p, + depth + 1, + max_depth, + info_what, + time_limit, + ); + } + } + } + } + } + Ok(true) + } + Ok((o, completed)) } } + } + if self.updates.rescanning_files_complete() { + self.updates.dont_rescanning_files_complete(); if self.current_index >= self.dir_content.len() { self.current_index = self.dir_content.len().saturating_sub(1); } @@ -129,6 +296,7 @@ impl TuiFile { self.search_text.clear(); self.search_regex = None; self.updates.request_redraw_searchbar(); + self.updates.request_filter_files(); } } if self.updates.filter_files() { @@ -199,7 +367,7 @@ impl TuiFile { pathstring.push_str(task.status.lock().unwrap().as_str()); } } - pathstring.push_str(" - "); + pathstring.push_str(" - "); if share.size.0 as usize > pathstring.len() { let mut pathchars = Vec::with_capacity(self.current_dir.as_os_str().len()); let mut maxlen = share.size.0 as usize - pathstring.len(); @@ -226,17 +394,26 @@ impl TuiFile { cursor::MoveTo(0, 0), style::PrintStyledContent( pathstring - .with(Color::Cyan) + .green() + .underlined() + .bold() .attribute(Attribute::Underlined) ) )?; } } - if self.updates.redraw_filelist() { - self.updates.dont_redraw_filelist(); + if self.updates.redraw_filebar() || self.updates.redraw_filelist() { + self.updates.request_redraw_filebar(); + self.updates.dont_redraw_filebar(); self.updates.request_move_cursor(); self.last_drawn_files_height = share.size.1.saturating_sub(3) as _; - let mut status = format!(" {}", self.files_status); + let mut status = match self.scan_files_mode { + ScanFilesMode::Blocking => " ".to_string(), + ScanFilesMode::Threaded => " (t) ".to_string(), + ScanFilesMode::Timeout(secs) => format!(" ({secs}s) "), + ScanFilesMode::TimeoutThenThreaded(secs) => format!(" ({secs}s -> t) "), + }; + status.push_str(&self.files_status); while status.len() < share.size.0 as usize { status.push(' '); } @@ -245,169 +422,126 @@ impl TuiFile { cursor::MoveTo(0, 1), style::PrintStyledContent(status.attribute(Attribute::Italic)), )?; - self.last_files_max_scroll = self - .dir_content_len - .saturating_sub(self.last_drawn_files_height); - let scrollbar_where = if self.last_files_max_scroll > 0 { - Some( - self.last_drawn_files_height.saturating_sub(1) * self.scroll - / self.last_files_max_scroll, - ) - } else { - None - }; - let mut drawn_files = 0; - for (line, entry) in self - .dir_content - .iter() - .skip(self.scroll) - .filter(|e| e.passes_filter) - .take(self.last_drawn_files_height) - .enumerate() - { - drawn_files += 1; - let (mut text, mut text_charlen) = ("- ".to_string(), 2); - for _ in 0..entry.rel_depth { - text.push_str(" | "); - } - text_charlen += entry.rel_depth * 4; - let endchar = if let Some(sb_where) = scrollbar_where { - if line == sb_where { - '#' - } else { - '|' - } + if self.updates.redraw_filelist() { + self.updates.dont_redraw_filelist(); + self.last_files_max_scroll = self + .dir_content_len + .saturating_sub(self.last_drawn_files_height); + let scrollbar_where = if self.last_files_max_scroll > 0 { + Some( + self.last_drawn_files_height.saturating_sub(1) * self.scroll + / self.last_files_max_scroll, + ) } else { - ' ' + None }; - let styled = match &entry.more { - DirContentType::Err(e) => { - text.push_str(&entry.name); - text_charlen += entry.name_charlen; - while text_charlen + 9 > share.size.0 as usize { - text.pop(); - text_charlen -= 1; + let mut drawn_files = 0; + for (line, entry) in self + .dir_content + .iter() + .skip(self.scroll) + .filter(|e| e.passes_filter) + .take(self.last_drawn_files_height) + .enumerate() + { + drawn_files += 1; + let (mut text, mut text_charlen) = ("- ".to_string(), 2); + for _ in 0..entry.rel_depth { + text.push_str(" | "); + } + text_charlen += entry.rel_depth * 4; + let endchar = if let Some(sb_where) = scrollbar_where { + if line == sb_where { + '#' + } else { + '|' } - text.push_str(" - Err: "); - text_charlen += 8; - for ch in e.chars() { - if ch == '\n' || ch == '\r' { - continue; + } else { + ' ' + }; + let styled = match &entry.more { + DirContentType::Err(e) => { + text.push_str(&entry.name); + text_charlen += entry.name_charlen; + while text_charlen + 9 > share.size.0 as usize { + text.pop(); + text_charlen -= 1; } - if text_charlen >= share.size.0 as usize { - break; + text.push_str(" - Err: "); + text_charlen += 8; + for ch in e.chars() { + if ch == '\n' || ch == '\r' { + continue; + } + if text_charlen >= share.size.0 as usize { + break; + } + text_charlen += 1; + text.push(ch); } + // make text_charlen 1 too large (for the endchar) text_charlen += 1; - text.push(ch); + while text_charlen < share.size.0 as _ { + text.push(' '); + text_charlen += 1; + } + text.push(endchar); + vec![text.red()] } - // make text_charlen 1 too large (for the endchar) - text_charlen += 1; - while text_charlen < share.size.0 as _ { + DirContentType::File { metadata } + | DirContentType::Dir { metadata } + | DirContentType::Symlink { metadata } => { + let filenamelen = + share.size.0 as usize - 2 - text_charlen - entry.info.len(); + if entry.name_charlen < filenamelen { + text.push_str(&entry.name); + for _ in 0..(filenamelen - entry.name_charlen) { + text.push(' '); + } + } else if entry.name_charlen == filenamelen { + text.push_str(&entry.name); + } else { + // the new length is the old length minus the combined length of the characters we want to cut off + let i = entry.name.len() + - entry + .name + .chars() + .rev() + .take(entry.name_charlen - filenamelen) + .map(|char| char.len_utf8()) + .sum::(); + text.push_str(&entry.name[0..i.saturating_sub(3)]); + text.push_str("..."); + } + text.push_str(&entry.info); text.push(' '); - text_charlen += 1; + text.push(endchar); + vec![match entry.more { + DirContentType::File { .. } => text.blue(), + DirContentType::Dir { .. } => text.yellow(), + DirContentType::Symlink { .. } => text.grey(), + DirContentType::Err { .. } => text.red(), + }] } - text.push(endchar); - vec![text.red()] - } - DirContentType::Dir { metadata: _ } => { - let filenamelen = share.size.0 as usize - 2 - text_charlen; - if entry.name_charlen < filenamelen { - text.push_str(&entry.name); - for _ in 0..(filenamelen - entry.name_charlen) { - text.push(' '); - } - } else if entry.name_charlen == filenamelen { - text.push_str(&entry.name); - } else { - // the new length is the old length minus the combined length of the characters we want to cut off - let i = entry.name.len() - - entry - .name - .chars() - .rev() - .take(entry.name_charlen - filenamelen) - .map(|char| char.len_utf8()) - .sum::(); - text.push_str(&entry.name[0..i.saturating_sub(3)]); - text.push_str("..."); + }; + queue!(share.stdout, cursor::MoveToNextLine(1))?; + for mut s in styled { + if entry.selected { + s = s.bold(); } - text.push(' '); - text.push(endchar); - vec![text.stylize()] + queue!(share.stdout, style::PrintStyledContent(s))?; } - DirContentType::File { size, metadata: _ } => { - let filenamelen = - share.size.0 as usize - 3 - text_charlen - size.chars().count(); - if entry.name_charlen < filenamelen { - text.push_str(&entry.name); - for _ in 0..(filenamelen - entry.name_charlen) { - text.push(' '); - } - } else if entry.name_charlen == filenamelen { - text.push_str(&entry.name); - } else { - // the new length is the old length minus the combined length of the characters we want to cut off - let i = entry.name.len() - - entry - .name - .chars() - .rev() - .take(entry.name_charlen - filenamelen) - .map(|char| char.len_utf8()) - .sum::(); - text.push_str(&entry.name[0..i.saturating_sub(3)]); - text.push_str("..."); - } - text.push(' '); - text.push_str(&size); - text.push(' '); - text.push(endchar); - vec![text.stylize()] - } - DirContentType::Symlink { metadata: _ } => { - let filenamelen = share.size.0 as usize - 2 - text_charlen; - if entry.name_charlen < filenamelen { - text.push_str(&entry.name); - for _ in 0..(filenamelen - entry.name_charlen) { - text.push(' '); - } - } else if entry.name_charlen == filenamelen { - text.push_str(&entry.name); - } else { - // the new length is the old length minus the combined length of the characters we want to cut off - let i = entry.name.len() - - entry - .name - .chars() - .rev() - .take(entry.name_charlen - filenamelen) - .map(|char| char.len_utf8()) - .sum::(); - text.push_str(&entry.name[0..i.saturating_sub(3)]); - text.push_str("..."); - } - text.push(' '); - text.push(endchar); - vec![text.italic()] - } - }; - queue!(share.stdout, cursor::MoveToNextLine(1))?; - for mut s in styled { - if entry.selected { - s = s.bold(); - } - queue!(share.stdout, style::PrintStyledContent(s))?; } - } - let empty_lines = self.last_drawn_files_count.saturating_sub(drawn_files); - self.last_drawn_files_count = drawn_files; - let empty_line = " ".repeat(share.size.0 as _); - for _ in 0..empty_lines { - queue!( - share.stdout, - cursor::MoveToNextLine(1), - style::PrintStyledContent(empty_line.as_str().stylize()) - )?; + let empty_lines = self.last_drawn_files_count.saturating_sub(drawn_files); + self.last_drawn_files_count = drawn_files; + let empty_line = " ".repeat(share.size.0 as _); + for _ in 0..empty_lines { + queue!( + share.stdout, + cursor::MoveToNextLine(1), + style::PrintStyledContent(empty_line.as_str().stylize()) + )?; + } } } if self.updates.redraw_searchbar() { @@ -426,7 +560,7 @@ impl TuiFile { share.stdout, cursor::MoveTo(0, share.size.1 - 1), style::PrintStyledContent(text.underlined()) - ); + )?; } if self.updates.move_cursor() { self.updates.dont_move_cursor(); @@ -474,6 +608,10 @@ impl TuiFile { }, Event::Key(e) => match (&self.focus, e.code) { // - - - Global - - - + // Ctrl+C/D -> Quit + (_, KeyCode::Char('c' | 'd')) if e.modifiers == KeyModifiers::CONTROL => { + return Ok(AppCmd::Quit); + } // Ctrl+Left/H -> Close (_, KeyCode::Left | KeyCode::Char('h')) if e.modifiers == KeyModifiers::CONTROL => @@ -526,7 +664,7 @@ impl TuiFile { (Focus::Files, KeyCode::Right | KeyCode::Char('l')) => { // descend into directory if let Some(entry) = self.dir_content.get(self.current_index) { - self.current_dir = entry.entry.path(); + self.current_dir = entry.path.clone(); self.updates = u32::MAX; } } @@ -559,6 +697,25 @@ impl TuiFile { self.focus = Focus::SearchBar; self.updates.request_move_cursor(); } + // M -> toggle threaded mode based on searchbar + (_, KeyCode::Char('m')) => { + self.updates.request_reset_search(); + self.updates.request_redraw_filebar(); + if self.search_text == "b" { + self.scan_files_mode = ScanFilesMode::Blocking; + } else if self.search_text == "t" { + self.scan_files_mode = ScanFilesMode::Threaded; + } else if self.search_text.starts_with("b") { + if let Ok(timeout) = self.search_text[1..].parse() { + self.scan_files_mode = ScanFilesMode::Timeout(timeout); + } + } else if self.search_text.starts_with("t") { + if let Ok(timeout) = self.search_text[1..].parse() { + self.scan_files_mode = + ScanFilesMode::TimeoutThenThreaded(timeout); + } + } + } // N -> New Directory (Focus::Files, KeyCode::Char('n')) => { let dir = self.current_dir.join(&self.search_text); @@ -573,7 +730,7 @@ impl TuiFile { (Focus::Files, KeyCode::Char('c')) => { if let Some(e) = self.dir_content.get(self.current_index) { if let DirContentType::Dir { .. } = e.more { - return Ok(AppCmd::CopyTo(e.entry.path())); + return Ok(AppCmd::CopyTo(e.path.clone())); } } } @@ -584,10 +741,30 @@ impl TuiFile { .iter() .rev() .filter(|e| e.selected) - .map(|e| e.entry.path()) + .map(|e| e.path.clone()) .collect(); tasks::task_del(paths, share); } + // P -> Permissions + (Focus::Files, KeyCode::Char('p')) => { + self.updates.request_reset_search(); + if let Ok(mode) = u32::from_str_radix(&self.search_text, 8) { + let paths = self + .dir_content + .iter() + .rev() + .filter(|e| e.selected) + .map(|e| e.path.clone()) + .collect(); + self.updates.request_redraw_infobar(); + tasks::task_chmod(paths, mode, share); + } + } + // O -> Owner (and group) + (Focus::Files, KeyCode::Char('o')) => { + self.updates.request_reset_search(); + // TODO! + } // T -> Open Terminal (Focus::Files, KeyCode::Char('t')) => 'term: { Command::new(&share.terminal_command) @@ -600,7 +777,7 @@ impl TuiFile { (Focus::Files, KeyCode::Char('e')) => { Self::term_reset(share)?; if let Some(entry) = self.dir_content.get(self.current_index) { - let entry_path = entry.entry.path(); + let entry_path = entry.path.clone(); Command::new(&share.editor_command) .arg(&entry_path) .current_dir(&self.current_dir) diff --git a/src/tasks.rs b/src/tasks.rs index f039fba..38faa51 100755 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,7 +1,9 @@ use std::{ collections::HashSet, fs, io, + os::unix::prelude::PermissionsExt, path::{Path, PathBuf}, + time::Duration, }; use crate::{BackgroundTask, Share}; @@ -11,45 +13,49 @@ pub(crate) fn task_copy( target: PathBuf, share: &mut Share, ) { - share.tasks.push(BackgroundTask::new(move |status| { - let mut total: usize = src.iter().map(|v| v.1.len()).sum(); - for (parent, rel_paths) in src { - let mut created: HashSet = HashSet::new(); - for (rel_path, copy_recursive) in rel_paths { - total = total.saturating_sub(1); - { - let s = format!("cp {total}"); - *status.lock().unwrap() = s; - } - let file_from = parent.join(&rel_path); - let file_to = target.join(&rel_path); - let is_dir = file_from.is_dir(); - let parent_created = if let Some(parent) = rel_path.parent() { - parent.as_os_str().is_empty() || created.contains(parent) - } else { - true - }; - if parent_created { - if is_dir { - copy_dir(file_from, file_to, copy_recursive); - created.insert(rel_path); - } else { - fs::copy(&file_from, &file_to); + share.tasks.push(BackgroundTask::new( + "cp".to_string(), + move |status| { + let mut total: usize = src.iter().map(|v| v.1.len()).sum(); + for (parent, rel_paths) in src { + let mut created: HashSet = HashSet::new(); + for (rel_path, copy_recursive) in rel_paths { + total = total.saturating_sub(1); + { + let s = format!("cp {total}"); + *status.lock().unwrap() = s; } - } else { - let rel_path = rel_path.file_name().unwrap(); + let file_from = parent.join(&rel_path); let file_to = target.join(&rel_path); - if is_dir { - copy_dir(file_from, file_to, copy_recursive); - created.insert(rel_path.into()); + let is_dir = file_from.is_dir(); + let parent_created = if let Some(parent) = rel_path.parent() { + parent.as_os_str().is_empty() || created.contains(parent) } else { - fs::copy(&file_from, &file_to); + true + }; + if parent_created { + if is_dir { + copy_dir(file_from, file_to, copy_recursive); + created.insert(rel_path); + } else { + fs::copy(&file_from, &file_to); + } + } else { + let rel_path = rel_path.file_name().unwrap(); + let file_to = target.join(&rel_path); + if is_dir { + copy_dir(file_from, file_to, copy_recursive); + created.insert(rel_path.into()); + } else { + fs::copy(&file_from, &file_to); + } } } } - } - Ok(()) - })); + Ok(()) + }, + true, + )); } fn copy_dir( file_from: impl AsRef, @@ -76,19 +82,42 @@ fn copy_dir( } pub(crate) fn task_del(paths: Vec, share: &mut Share) { - share.tasks.push(BackgroundTask::new(move |status| { - let total: usize = paths.len(); - for path in paths { - { - let s = format!("rm {total}"); - *status.lock().unwrap() = s; + let mut total: usize = paths.len(); + share.tasks.push(BackgroundTask::new( + format!("rm {total}"), + move |status| { + for path in paths { + { + total -= 1; + let s = format!("rm {total}"); + *status.lock().unwrap() = s; + } + if path.is_dir() { + fs::remove_dir(path); + } else { + fs::remove_file(path); + } } - if path.is_dir() { - fs::remove_dir(path); - } else { - fs::remove_file(path); - } - } - Ok(()) - })); + Ok(()) + }, + true, + )); +} +pub(crate) fn task_chmod(paths: Vec, mode: u32, share: &mut Share) { + let mut total = paths.len(); + share.tasks.push(BackgroundTask::new( + format!("chmod {total}"), + move |status| { + for path in paths { + { + total -= 1; + let s = format!("chmod {total}"); + *status.lock().unwrap() = s; + } + fs::set_permissions(path, fs::Permissions::from_mode(mode)); + } + Ok(()) + }, + true, + )); } diff --git a/src/updates.rs b/src/updates.rs index b030099..7419988 100755 --- a/src/updates.rs +++ b/src/updates.rs @@ -40,6 +40,14 @@ pub(crate) trait Updates { fn reset_search(&self) -> bool; fn dont_reset_search(&mut self); fn request_reset_search(&mut self); + + fn rescanning_files_complete(&self) -> bool; + fn dont_rescanning_files_complete(&mut self); + fn request_rescanning_files_complete(&mut self); + + fn redraw_filebar(&self) -> bool; + fn dont_redraw_filebar(&mut self); + fn request_redraw_filebar(&mut self); } impl Updates for u32 { fn rescan_files(&self) -> bool { @@ -123,4 +131,22 @@ impl Updates for u32 { fn request_reset_search(&mut self) { *self |= 0b100000000; } + fn rescanning_files_complete(&self) -> bool { + 0 != self & 0b1000000000 + } + fn dont_rescanning_files_complete(&mut self) { + *self ^= 0b1000000000; + } + fn request_rescanning_files_complete(&mut self) { + *self |= 0b1000000000; + } + fn redraw_filebar(&self) -> bool { + 0 != self & 0b10000000000 + } + fn dont_redraw_filebar(&mut self) { + *self ^= 0b10000000000; + } + fn request_redraw_filebar(&mut self) { + *self |= 0b10000000000; + } }