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 - create new directories
- copy, move and delete - copy, move and delete
- quickly open your `$TERM` and `$EDITOR` - 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) - add more features (open an issue with ideas if you have any)
## Demo ## Demo
@ -23,6 +24,7 @@ https://github.com/Dummi26/tuifile/assets/67615357/0b0553c9-72e5-4d38-8537-f6cc3
### Global ### Global
Ctrl+C/D -> quit
Ctrl+Up/K -> previous Ctrl+Up/K -> previous
Ctrl+Down/J -> next Ctrl+Down/J -> next
Ctrl+Left/H -> close Ctrl+Left/H -> close
@ -37,9 +39,12 @@ Ctrl+Right/L -> duplicate
- S -> Select or toggle current - S -> Select or toggle current
- D -> Deselect all - D -> Deselect all
- F -> focus Find/Filter bar - F -> focus Find/Filter bar
- M -> set Mode bysed on Find/Filter bar
- N -> New directory (name taken from find/filter bar text) - N -> New directory (name taken from find/filter bar text)
- C -> Copy selected to this directory - C -> Copy selected to this directory
- R -> remove selected files and directories (not recursive: also requires selecting the directories content) - 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) - 1-9 or 0 -> set recursive depth limit (0 = infinite)
- T -> open terminal here - T -> open terminal here
- E -> open this file in your editor - E -> open this file in your editor
@ -50,3 +55,33 @@ Ctrl+Right/L -> duplicate
- Enter -> back & filter - Enter -> back & filter
- Backspace -> delete - Backspace -> delete
- type to enter search regex - 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; mod updates;
use std::{ use std::{
fs::{self, DirEntry, Metadata}, fs::{self, Metadata},
io::{self, StdoutLock}, io::{self, StdoutLock},
path::PathBuf, path::PathBuf,
rc::Rc,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::JoinHandle, thread::JoinHandle,
}; };
@ -46,6 +45,7 @@ fn main() -> io::Result<()> {
terminal_command: std::env::var("TERM").unwrap_or("alacritty".to_string()), terminal_command: std::env::var("TERM").unwrap_or("alacritty".to_string()),
editor_command: std::env::var("EDITOR").unwrap_or("nano".to_string()), editor_command: std::env::var("EDITOR").unwrap_or("nano".to_string()),
live_search: !args.no_live_search, live_search: !args.no_live_search,
info_what: vec![0, 1],
}; };
if args.check { if args.check {
eprintln!("Terminal: {}", share.terminal_command); eprintln!("Terminal: {}", share.terminal_command);
@ -111,11 +111,7 @@ fn main() -> io::Result<()> {
.filter(|e| e.selected) .filter(|e| e.selected)
.filter_map(|e| { .filter_map(|e| {
Some(( Some((
e.entry e.path.strip_prefix(&v.current_dir).ok()?.to_owned(),
.path()
.strip_prefix(&v.current_dir)
.ok()?
.to_owned(),
e.rel_depth == v.scan_files_max_depth, e.rel_depth == v.scan_files_max_depth,
)) ))
}) })
@ -193,30 +189,39 @@ struct Share {
live_search: bool, live_search: bool,
terminal_command: String, terminal_command: String,
editor_command: String, editor_command: String,
/// 0: size
/// 1: mode (permissions)
info_what: Vec<u32>,
} }
impl Share { 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() { for (i, task) in self.tasks.iter_mut().enumerate() {
if task.thread.is_finished() { if task.thread.is_finished() {
self.tasks.remove(i); return Some(self.tasks.remove(i).rescan_after);
return true;
} }
} }
false None
} }
} }
struct BackgroundTask { struct BackgroundTask {
status: Arc<Mutex<String>>, status: Arc<Mutex<String>>,
thread: JoinHandle<Result<(), String>>, thread: JoinHandle<Result<(), String>>,
rescan_after: bool,
} }
impl BackgroundTask { impl BackgroundTask {
pub fn new( pub fn new(
text: String,
func: impl FnOnce(Arc<Mutex<String>>) -> Result<(), String> + Send + 'static, func: impl FnOnce(Arc<Mutex<String>>) -> Result<(), String> + Send + 'static,
rescan_after: bool,
) -> Self { ) -> Self {
let status = Arc::new(Mutex::new(String::new())); let status = Arc::new(Mutex::new(text));
Self { Self {
status: Arc::clone(&status), status: Arc::clone(&status),
thread: std::thread::spawn(move || func(status)), thread: std::thread::spawn(move || func(status)),
rescan_after,
} }
} }
} }
@ -226,6 +231,7 @@ struct TuiFile {
current_dir: PathBuf, current_dir: PathBuf,
dir_content: Vec<DirContent>, dir_content: Vec<DirContent>,
dir_content_len: usize, dir_content_len: usize,
dir_content_builder_task: Option<Arc<Mutex<Option<Result<Vec<DirContent>, String>>>>>,
scroll: usize, scroll: usize,
current_index: usize, current_index: usize,
focus: Focus, focus: Focus,
@ -238,15 +244,39 @@ struct TuiFile {
last_drawn_files_count: usize, last_drawn_files_count: usize,
last_files_max_scroll: usize, last_files_max_scroll: usize,
after_rescanning_files: Vec<Box<dyn FnOnce(&mut Self)>>, 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)] #[derive(Clone)]
struct DirContent { struct DirContent {
entry: Rc<DirEntry>, path: PathBuf,
name: String, name: String,
name_charlen: usize, name_charlen: usize,
rel_depth: usize, rel_depth: usize,
passes_filter: bool, passes_filter: bool,
selected: bool, selected: bool,
info: String,
more: DirContentType, more: DirContentType,
} }
#[derive(Clone)] #[derive(Clone)]
@ -257,7 +287,6 @@ enum DirContentType {
metadata: Metadata, metadata: Metadata,
}, },
File { File {
size: String,
metadata: Metadata, metadata: Metadata,
}, },
Symlink { Symlink {
@ -286,6 +315,7 @@ impl TuiFile {
current_dir: self.current_dir.clone(), current_dir: self.current_dir.clone(),
dir_content: self.dir_content.clone(), dir_content: self.dir_content.clone(),
dir_content_len: self.dir_content_len, dir_content_len: self.dir_content_len,
dir_content_builder_task: None,
scroll: self.scroll, scroll: self.scroll,
current_index: self.current_index, current_index: self.current_index,
focus: self.focus.clone(), focus: self.focus.clone(),
@ -298,6 +328,7 @@ impl TuiFile {
last_drawn_files_count: self.last_drawn_files_count, last_drawn_files_count: self.last_drawn_files_count,
last_files_max_scroll: self.last_files_max_scroll, last_files_max_scroll: self.last_files_max_scroll,
after_rescanning_files: vec![], after_rescanning_files: vec![],
scan_files_mode: ScanFilesMode::default(),
} }
} }
pub fn new(current_dir: PathBuf) -> io::Result<Self> { pub fn new(current_dir: PathBuf) -> io::Result<Self> {
@ -310,6 +341,7 @@ impl TuiFile {
current_dir, current_dir,
dir_content: vec![], dir_content: vec![],
dir_content_len: 0, dir_content_len: 0,
dir_content_builder_task: None,
scroll: 0, scroll: 0,
current_index: 0, current_index: 0,
focus: Focus::Files, focus: Focus::Files,
@ -322,6 +354,7 @@ impl TuiFile {
last_drawn_files_count: 0, last_drawn_files_count: 0,
last_files_max_scroll: 0, last_files_max_scroll: 0,
after_rescanning_files: vec![], after_rescanning_files: vec![],
scan_files_mode: ScanFilesMode::default(),
}) })
} }
fn set_current_index(&mut self, mut i: usize) { 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 regex::RegexBuilder;
use crate::updates::Updates; 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::io::Write;
use std::os::unix::prelude::PermissionsExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::{fs, io}; use std::{fs, io};
use crate::TuiFile; use crate::TuiFile;
@ -33,58 +37,201 @@ impl TuiFile {
} }
pub fn run(&mut self, share: &mut Share) -> io::Result<AppCmd> { pub fn run(&mut self, share: &mut Share) -> io::Result<AppCmd> {
loop { 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); return Ok(AppCmd::TaskFinished);
} }
}
// rescan files if necessary // 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() { if self.updates.rescan_files() {
self.updates.dont_rescan_files(); self.updates.dont_rescan_files();
self.updates.request_filter_files(); if self.dir_content_builder_task.is_none() {
self.files_status_is_special = false;
self.dir_content.clear(); self.dir_content.clear();
get_files(self, self.current_dir.clone(), 0); self.files_status_is_special = false;
fn get_files(s: &mut TuiFile, dir: PathBuf, depth: usize) { 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) { match fs::read_dir(&dir) {
Err(e) => { Err(e) => {
if depth == 0 { if depth == 0 {
s.dir_content = vec![]; return Err(format!("{e}"));
s.files_status = format!("{e}");
s.files_status_is_special = true;
} }
} }
Ok(files) => { Ok(files) => {
for entry in files { for entry in files {
if let Ok(entry) = entry { 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 metadata = entry.metadata();
let p = entry.path(); let p = entry.path();
let more = match metadata { let info = if let Ok(metadata) = &metadata {
Err(e) => DirContentType::Err(e.to_string()), // in each line:
Ok(metadata) => { // first char:
if metadata.is_symlink() { // < left-aligned
DirContentType::Symlink { metadata } // > right-aligned
} else if metadata.is_file() { // anything else -> centered
DirContentType::File { // sep. line: "< | "
size: { let mut info = String::new();
for info_what in info_what {
match info_what {
0 => {
let mut bytes = metadata.len(); let mut bytes = metadata.len();
let mut i = 0; let mut i = 0;
loop { loop {
if bytes < 1024 if bytes < 1024
|| i + 1 >= BYTE_UNITS.len() || i + 1 >= BYTE_UNITS.len()
{ {
break format!( info.push_str(&format!(
"{bytes}{}", "< | \n>{bytes}\n>{}\n",
BYTE_UNITS[i] BYTE_UNITS[i]
); ));
break;
} else { } else {
i += 1; i += 1;
// divide by 1024 but cooler // divide by 1024 but cooler
bytes >>= 10; 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() { } else if metadata.is_dir() {
DirContentType::Dir { metadata } DirContentType::Dir { metadata }
} else { } else {
@ -97,23 +244,43 @@ impl TuiFile {
if let DirContentType::Dir { .. } = more { if let DirContentType::Dir { .. } = more {
name.push('/'); name.push('/');
} }
s.dir_content.push(DirContent { dir_content.push(DirContent {
entry: Rc::new(entry), path: entry.path(),
name_charlen: name.chars().count(), name_charlen: name.chars().count(),
name, name,
rel_depth: depth, rel_depth: depth,
passes_filter: true, passes_filter: true,
selected: false, selected: false,
info,
more, more,
}); });
if depth < s.scan_files_max_depth { if let Some((since, max)) = time_limit {
get_files(s, p, depth + 1); 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() { if self.current_index >= self.dir_content.len() {
self.current_index = self.dir_content.len().saturating_sub(1); self.current_index = self.dir_content.len().saturating_sub(1);
} }
@ -129,6 +296,7 @@ impl TuiFile {
self.search_text.clear(); self.search_text.clear();
self.search_regex = None; self.search_regex = None;
self.updates.request_redraw_searchbar(); self.updates.request_redraw_searchbar();
self.updates.request_filter_files();
} }
} }
if self.updates.filter_files() { if self.updates.filter_files() {
@ -226,17 +394,26 @@ impl TuiFile {
cursor::MoveTo(0, 0), cursor::MoveTo(0, 0),
style::PrintStyledContent( style::PrintStyledContent(
pathstring pathstring
.with(Color::Cyan) .green()
.underlined()
.bold()
.attribute(Attribute::Underlined) .attribute(Attribute::Underlined)
) )
)?; )?;
} }
} }
if self.updates.redraw_filelist() { if self.updates.redraw_filebar() || self.updates.redraw_filelist() {
self.updates.dont_redraw_filelist(); self.updates.request_redraw_filebar();
self.updates.dont_redraw_filebar();
self.updates.request_move_cursor(); self.updates.request_move_cursor();
self.last_drawn_files_height = share.size.1.saturating_sub(3) as _; 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 { while status.len() < share.size.0 as usize {
status.push(' '); status.push(' ');
} }
@ -245,6 +422,8 @@ impl TuiFile {
cursor::MoveTo(0, 1), cursor::MoveTo(0, 1),
style::PrintStyledContent(status.attribute(Attribute::Italic)), style::PrintStyledContent(status.attribute(Attribute::Italic)),
)?; )?;
if self.updates.redraw_filelist() {
self.updates.dont_redraw_filelist();
self.last_files_max_scroll = self self.last_files_max_scroll = self
.dir_content_len .dir_content_len
.saturating_sub(self.last_drawn_files_height); .saturating_sub(self.last_drawn_files_height);
@ -309,35 +488,11 @@ impl TuiFile {
text.push(endchar); text.push(endchar);
vec![text.red()] vec![text.red()]
} }
DirContentType::Dir { metadata: _ } => { DirContentType::File { metadata }
let filenamelen = share.size.0 as usize - 2 - text_charlen; | DirContentType::Dir { metadata }
if entry.name_charlen < filenamelen { | DirContentType::Symlink { metadata } => {
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: _ } => {
let filenamelen = 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 { if entry.name_charlen < filenamelen {
text.push_str(&entry.name); text.push_str(&entry.name);
for _ in 0..(filenamelen - entry.name_charlen) { 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(&entry.name[0..i.saturating_sub(3)]);
text.push_str("..."); text.push_str("...");
} }
text.push(' '); text.push_str(&entry.info);
text.push_str(&size);
text.push(' '); text.push(' ');
text.push(endchar); text.push(endchar);
vec![text.stylize()] vec![match entry.more {
} DirContentType::File { .. } => text.blue(),
DirContentType::Symlink { metadata: _ } => { DirContentType::Dir { .. } => text.yellow(),
let filenamelen = share.size.0 as usize - 2 - text_charlen; DirContentType::Symlink { .. } => text.grey(),
if entry.name_charlen < filenamelen { DirContentType::Err { .. } => text.red(),
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()]
} }
}; };
queue!(share.stdout, cursor::MoveToNextLine(1))?; queue!(share.stdout, cursor::MoveToNextLine(1))?;
@ -410,6 +543,7 @@ impl TuiFile {
)?; )?;
} }
} }
}
if self.updates.redraw_searchbar() { if self.updates.redraw_searchbar() {
self.updates.dont_redraw_searchbar(); self.updates.dont_redraw_searchbar();
self.updates.request_move_cursor(); self.updates.request_move_cursor();
@ -426,7 +560,7 @@ impl TuiFile {
share.stdout, share.stdout,
cursor::MoveTo(0, share.size.1 - 1), cursor::MoveTo(0, share.size.1 - 1),
style::PrintStyledContent(text.underlined()) style::PrintStyledContent(text.underlined())
); )?;
} }
if self.updates.move_cursor() { if self.updates.move_cursor() {
self.updates.dont_move_cursor(); self.updates.dont_move_cursor();
@ -474,6 +608,10 @@ impl TuiFile {
}, },
Event::Key(e) => match (&self.focus, e.code) { Event::Key(e) => match (&self.focus, e.code) {
// - - - Global - - - // - - - Global - - -
// Ctrl+C/D -> Quit
(_, KeyCode::Char('c' | 'd')) if e.modifiers == KeyModifiers::CONTROL => {
return Ok(AppCmd::Quit);
}
// Ctrl+Left/H -> Close // Ctrl+Left/H -> Close
(_, KeyCode::Left | KeyCode::Char('h')) (_, KeyCode::Left | KeyCode::Char('h'))
if e.modifiers == KeyModifiers::CONTROL => if e.modifiers == KeyModifiers::CONTROL =>
@ -526,7 +664,7 @@ impl TuiFile {
(Focus::Files, KeyCode::Right | KeyCode::Char('l')) => { (Focus::Files, KeyCode::Right | KeyCode::Char('l')) => {
// descend into directory // descend into directory
if let Some(entry) = self.dir_content.get(self.current_index) { 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; self.updates = u32::MAX;
} }
} }
@ -559,6 +697,25 @@ impl TuiFile {
self.focus = Focus::SearchBar; self.focus = Focus::SearchBar;
self.updates.request_move_cursor(); 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 // N -> New Directory
(Focus::Files, KeyCode::Char('n')) => { (Focus::Files, KeyCode::Char('n')) => {
let dir = self.current_dir.join(&self.search_text); let dir = self.current_dir.join(&self.search_text);
@ -573,7 +730,7 @@ impl TuiFile {
(Focus::Files, KeyCode::Char('c')) => { (Focus::Files, KeyCode::Char('c')) => {
if let Some(e) = self.dir_content.get(self.current_index) { if let Some(e) = self.dir_content.get(self.current_index) {
if let DirContentType::Dir { .. } = e.more { 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() .iter()
.rev() .rev()
.filter(|e| e.selected) .filter(|e| e.selected)
.map(|e| e.entry.path()) .map(|e| e.path.clone())
.collect(); .collect();
tasks::task_del(paths, share); 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 // T -> Open Terminal
(Focus::Files, KeyCode::Char('t')) => 'term: { (Focus::Files, KeyCode::Char('t')) => 'term: {
Command::new(&share.terminal_command) Command::new(&share.terminal_command)
@ -600,7 +777,7 @@ impl TuiFile {
(Focus::Files, KeyCode::Char('e')) => { (Focus::Files, KeyCode::Char('e')) => {
Self::term_reset(share)?; Self::term_reset(share)?;
if let Some(entry) = self.dir_content.get(self.current_index) { 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) Command::new(&share.editor_command)
.arg(&entry_path) .arg(&entry_path)
.current_dir(&self.current_dir) .current_dir(&self.current_dir)

View File

@ -1,7 +1,9 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
fs, io, fs, io,
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration,
}; };
use crate::{BackgroundTask, Share}; use crate::{BackgroundTask, Share};
@ -11,7 +13,9 @@ pub(crate) fn task_copy(
target: PathBuf, target: PathBuf,
share: &mut Share, 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(); let mut total: usize = src.iter().map(|v| v.1.len()).sum();
for (parent, rel_paths) in src { for (parent, rel_paths) in src {
let mut created: HashSet<PathBuf> = HashSet::new(); let mut created: HashSet<PathBuf> = HashSet::new();
@ -49,7 +53,9 @@ pub(crate) fn task_copy(
} }
} }
Ok(()) Ok(())
})); },
true,
));
} }
fn copy_dir( fn copy_dir(
file_from: impl AsRef<Path>, file_from: impl AsRef<Path>,
@ -76,10 +82,13 @@ fn copy_dir(
} }
pub(crate) fn task_del(paths: Vec<PathBuf>, share: &mut Share) { pub(crate) fn task_del(paths: Vec<PathBuf>, share: &mut Share) {
share.tasks.push(BackgroundTask::new(move |status| { let mut total: usize = paths.len();
let total: usize = paths.len(); share.tasks.push(BackgroundTask::new(
format!("rm {total}"),
move |status| {
for path in paths { for path in paths {
{ {
total -= 1;
let s = format!("rm {total}"); let s = format!("rm {total}");
*status.lock().unwrap() = s; *status.lock().unwrap() = s;
} }
@ -90,5 +99,25 @@ pub(crate) fn task_del(paths: Vec<PathBuf>, share: &mut Share) {
} }
} }
Ok(()) 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 reset_search(&self) -> bool;
fn dont_reset_search(&mut self); fn dont_reset_search(&mut self);
fn request_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 { impl Updates for u32 {
fn rescan_files(&self) -> bool { fn rescan_files(&self) -> bool {
@ -123,4 +131,22 @@ impl Updates for u32 {
fn request_reset_search(&mut self) { fn request_reset_search(&mut self) {
*self |= 0b100000000; *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;
}
} }