updated color scheme, added threaded modes, added a way to set permission bits (chmod)

This commit is contained in:
Mark 2023-08-28 00:49:12 +02:00
parent 37557aa358
commit 71968f7f64
5 changed files with 592 additions and 292 deletions

View File

@ -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<seconds>` into the filter bar, go back to files mode, and press `m`.
Replace `<seconds>` 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<seconds>` into the filter bar, go back to files mode, and press `m`.
Replace `<seconds>` with a number like `1` or `0.3`.

View File

@ -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<u32>,
}
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<bool> {
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<Mutex<String>>,
thread: JoinHandle<Result<(), String>>,
rescan_after: bool,
}
impl BackgroundTask {
pub fn new(
text: String,
func: impl FnOnce(Arc<Mutex<String>>) -> 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<DirContent>,
dir_content_len: usize,
dir_content_builder_task: Option<Arc<Mutex<Option<Result<Vec<DirContent>, 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<Box<dyn FnOnce(&mut Self)>>,
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<DirEntry>,
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<Self> {
@ -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) {

View File

@ -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,58 +37,201 @@ impl TuiFile {
}
pub fn run(&mut self, share: &mut Share) -> io::Result<AppCmd> {
loop {
if share.check_bgtasks() {
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<Vec<DirContent>, 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;
if self.dir_content_builder_task.is_none() {
self.dir_content.clear();
get_files(self, self.current_dir.clone(), 0);
fn get_files(s: &mut TuiFile, dir: PathBuf, depth: usize) {
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<u32>,
timeout: Option<f32>,
) -> Result<(Vec<DirContent>, 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();
}
}
}
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(' ');
}
} 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<DirContent>,
dir: PathBuf,
depth: usize,
max_depth: usize,
info_what: &Vec<u32>,
time_limit: Option<(Instant, f32)>,
) -> Result<bool, String> {
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;
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 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 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()
{
break format!(
"{bytes}{}",
info.push_str(&format!(
"< | \n>{bytes}\n>{}\n",
BYTE_UNITS[i]
);
));
break;
} else {
i += 1;
// divide by 1024 but cooler
bytes >>= 10;
}
}
},
metadata,
}
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 {
@ -97,23 +244,43 @@ impl TuiFile {
if let DirContentType::Dir { .. } = more {
name.push('/');
}
s.dir_content.push(DirContent {
entry: Rc::new(entry),
dir_content.push(DirContent {
path: entry.path(),
name_charlen: name.chars().count(),
name,
rel_depth: depth,
passes_filter: true,
selected: false,
info,
more,
});
if depth < s.scan_files_max_depth {
get_files(s, p, depth + 1);
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() {
@ -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,6 +422,8 @@ impl TuiFile {
cursor::MoveTo(0, 1),
style::PrintStyledContent(status.attribute(Attribute::Italic)),
)?;
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);
@ -309,35 +488,11 @@ impl TuiFile {
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::<usize>();
text.push_str(&entry.name[0..i.saturating_sub(3)]);
text.push_str("...");
}
text.push(' ');
text.push(endchar);
vec![text.stylize()]
}
DirContentType::File { size, metadata: _ } => {
DirContentType::File { metadata }
| DirContentType::Dir { metadata }
| DirContentType::Symlink { metadata } => {
let filenamelen =
share.size.0 as usize - 3 - text_charlen - size.chars().count();
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) {
@ -358,37 +513,15 @@ impl TuiFile {
text.push_str(&entry.name[0..i.saturating_sub(3)]);
text.push_str("...");
}
text.push(' ');
text.push_str(&size);
text.push_str(&entry.info);
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::<usize>();
text.push_str(&entry.name[0..i.saturating_sub(3)]);
text.push_str("...");
}
text.push(' ');
text.push(endchar);
vec![text.italic()]
vec![match entry.more {
DirContentType::File { .. } => text.blue(),
DirContentType::Dir { .. } => text.yellow(),
DirContentType::Symlink { .. } => text.grey(),
DirContentType::Err { .. } => text.red(),
}]
}
};
queue!(share.stdout, cursor::MoveToNextLine(1))?;
@ -410,6 +543,7 @@ impl TuiFile {
)?;
}
}
}
if self.updates.redraw_searchbar() {
self.updates.dont_redraw_searchbar();
self.updates.request_move_cursor();
@ -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)

View File

@ -1,7 +1,9 @@
use std::{
collections::HashSet,
fs, io,
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
time::Duration,
};
use crate::{BackgroundTask, Share};
@ -11,7 +13,9 @@ pub(crate) fn task_copy(
target: PathBuf,
share: &mut Share,
) {
share.tasks.push(BackgroundTask::new(move |status| {
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<PathBuf> = HashSet::new();
@ -49,7 +53,9 @@ pub(crate) fn task_copy(
}
}
Ok(())
}));
},
true,
));
}
fn copy_dir(
file_from: impl AsRef<Path>,
@ -76,10 +82,13 @@ fn copy_dir(
}
pub(crate) fn task_del(paths: Vec<PathBuf>, share: &mut Share) {
share.tasks.push(BackgroundTask::new(move |status| {
let total: usize = paths.len();
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;
}
@ -90,5 +99,25 @@ pub(crate) fn task_del(paths: Vec<PathBuf>, share: &mut Share) {
}
}
Ok(())
}));
},
true,
));
}
pub(crate) fn task_chmod(paths: Vec<PathBuf>, 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,
));
}

View File

@ -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;
}
}