mirror of
https://github.com/Dummi26/tuifile.git
synced 2025-03-10 11:43:53 +01:00
updated color scheme, added threaded modes, added a way to set permission bits (chmod)
This commit is contained in:
parent
37557aa358
commit
71968f7f64
35
README.md
35
README.md
@ -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`.
|
||||||
|
61
src/main.rs
61
src/main.rs
@ -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) {
|
||||||
|
639
src/run.rs
639
src/run.rs
@ -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,87 +37,250 @@ 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() {
|
||||||
return Ok(AppCmd::TaskFinished);
|
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
|
// 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();
|
self.files_status_is_special = false;
|
||||||
get_files(self, self.current_dir.clone(), 0);
|
let (scan_dir_blocking, mut scan_dir_threaded, timeout) =
|
||||||
fn get_files(s: &mut TuiFile, dir: PathBuf, depth: usize) {
|
match self.scan_files_mode {
|
||||||
match fs::read_dir(&dir) {
|
ScanFilesMode::Blocking => (true, false, None),
|
||||||
Err(e) => {
|
ScanFilesMode::Threaded => (false, true, None),
|
||||||
if depth == 0 {
|
ScanFilesMode::Timeout(t) => (true, false, Some(t)),
|
||||||
s.dir_content = vec![];
|
ScanFilesMode::TimeoutThenThreaded(t) => (true, true, Some(t)),
|
||||||
s.files_status = format!("{e}");
|
};
|
||||||
s.files_status_is_special = true;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(files) => {
|
for e in o.iter_mut() {
|
||||||
for entry in files {
|
let src = std::mem::replace(&mut e.info, String::new());
|
||||||
if let Ok(entry) = entry {
|
for (i, line) in src.lines().enumerate() {
|
||||||
let mut name = entry.file_name().to_string_lossy().into_owned();
|
let rem = lengths[i] - line.len();
|
||||||
let metadata = entry.metadata();
|
if line.starts_with('<') {
|
||||||
let p = entry.path();
|
e.info.push_str(&line[1..]);
|
||||||
let more = match metadata {
|
for _ in 0..rem {
|
||||||
Err(e) => DirContentType::Err(e.to_string()),
|
e.info.push(' ');
|
||||||
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('/');
|
|
||||||
}
|
}
|
||||||
s.dir_content.push(DirContent {
|
} else if line.starts_with('>') {
|
||||||
entry: Rc::new(entry),
|
for _ in 0..rem {
|
||||||
name_charlen: name.chars().count(),
|
e.info.push(' ');
|
||||||
name,
|
}
|
||||||
rel_depth: depth,
|
e.info.push_str(&line[1..]);
|
||||||
passes_filter: true,
|
} else {
|
||||||
selected: false,
|
let r = rem / 2;
|
||||||
more,
|
for _ in 0..r {
|
||||||
});
|
e.info.push(' ');
|
||||||
if depth < s.scan_files_max_depth {
|
}
|
||||||
get_files(s, p, depth + 1);
|
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 {
|
||||||
|
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() {
|
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() {
|
||||||
@ -199,7 +367,7 @@ impl TuiFile {
|
|||||||
pathstring.push_str(task.status.lock().unwrap().as_str());
|
pathstring.push_str(task.status.lock().unwrap().as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pathstring.push_str(" - ");
|
pathstring.push_str(" - ");
|
||||||
if share.size.0 as usize > pathstring.len() {
|
if share.size.0 as usize > pathstring.len() {
|
||||||
let mut pathchars = Vec::with_capacity(self.current_dir.as_os_str().len());
|
let mut pathchars = Vec::with_capacity(self.current_dir.as_os_str().len());
|
||||||
let mut maxlen = share.size.0 as usize - pathstring.len();
|
let mut maxlen = share.size.0 as usize - pathstring.len();
|
||||||
@ -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,169 +422,126 @@ impl TuiFile {
|
|||||||
cursor::MoveTo(0, 1),
|
cursor::MoveTo(0, 1),
|
||||||
style::PrintStyledContent(status.attribute(Attribute::Italic)),
|
style::PrintStyledContent(status.attribute(Attribute::Italic)),
|
||||||
)?;
|
)?;
|
||||||
self.last_files_max_scroll = self
|
if self.updates.redraw_filelist() {
|
||||||
.dir_content_len
|
self.updates.dont_redraw_filelist();
|
||||||
.saturating_sub(self.last_drawn_files_height);
|
self.last_files_max_scroll = self
|
||||||
let scrollbar_where = if self.last_files_max_scroll > 0 {
|
.dir_content_len
|
||||||
Some(
|
.saturating_sub(self.last_drawn_files_height);
|
||||||
self.last_drawn_files_height.saturating_sub(1) * self.scroll
|
let scrollbar_where = if self.last_files_max_scroll > 0 {
|
||||||
/ self.last_files_max_scroll,
|
Some(
|
||||||
)
|
self.last_drawn_files_height.saturating_sub(1) * self.scroll
|
||||||
} else {
|
/ self.last_files_max_scroll,
|
||||||
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 {
|
|
||||||
'|'
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
' '
|
None
|
||||||
};
|
};
|
||||||
let styled = match &entry.more {
|
let mut drawn_files = 0;
|
||||||
DirContentType::Err(e) => {
|
for (line, entry) in self
|
||||||
text.push_str(&entry.name);
|
.dir_content
|
||||||
text_charlen += entry.name_charlen;
|
.iter()
|
||||||
while text_charlen + 9 > share.size.0 as usize {
|
.skip(self.scroll)
|
||||||
text.pop();
|
.filter(|e| e.passes_filter)
|
||||||
text_charlen -= 1;
|
.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: ");
|
} else {
|
||||||
text_charlen += 8;
|
' '
|
||||||
for ch in e.chars() {
|
};
|
||||||
if ch == '\n' || ch == '\r' {
|
let styled = match &entry.more {
|
||||||
continue;
|
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 {
|
text.push_str(" - Err: ");
|
||||||
break;
|
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_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)
|
DirContentType::File { metadata }
|
||||||
text_charlen += 1;
|
| DirContentType::Dir { metadata }
|
||||||
while text_charlen < share.size.0 as _ {
|
| 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::<usize>();
|
||||||
|
text.push_str(&entry.name[0..i.saturating_sub(3)]);
|
||||||
|
text.push_str("...");
|
||||||
|
}
|
||||||
|
text.push_str(&entry.info);
|
||||||
text.push(' ');
|
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()]
|
queue!(share.stdout, cursor::MoveToNextLine(1))?;
|
||||||
}
|
for mut s in styled {
|
||||||
DirContentType::Dir { metadata: _ } => {
|
if entry.selected {
|
||||||
let filenamelen = share.size.0 as usize - 2 - text_charlen;
|
s = s.bold();
|
||||||
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(' ');
|
queue!(share.stdout, style::PrintStyledContent(s))?;
|
||||||
text.push(endchar);
|
|
||||||
vec![text.stylize()]
|
|
||||||
}
|
}
|
||||||
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::<usize>();
|
|
||||||
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::<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))?;
|
|
||||||
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);
|
||||||
let empty_lines = self.last_drawn_files_count.saturating_sub(drawn_files);
|
self.last_drawn_files_count = drawn_files;
|
||||||
self.last_drawn_files_count = drawn_files;
|
let empty_line = " ".repeat(share.size.0 as _);
|
||||||
let empty_line = " ".repeat(share.size.0 as _);
|
for _ in 0..empty_lines {
|
||||||
for _ in 0..empty_lines {
|
queue!(
|
||||||
queue!(
|
share.stdout,
|
||||||
share.stdout,
|
cursor::MoveToNextLine(1),
|
||||||
cursor::MoveToNextLine(1),
|
style::PrintStyledContent(empty_line.as_str().stylize())
|
||||||
style::PrintStyledContent(empty_line.as_str().stylize())
|
)?;
|
||||||
)?;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.updates.redraw_searchbar() {
|
if self.updates.redraw_searchbar() {
|
||||||
@ -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)
|
||||||
|
123
src/tasks.rs
123
src/tasks.rs
@ -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,45 +13,49 @@ 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(
|
||||||
let mut total: usize = src.iter().map(|v| v.1.len()).sum();
|
"cp".to_string(),
|
||||||
for (parent, rel_paths) in src {
|
move |status| {
|
||||||
let mut created: HashSet<PathBuf> = HashSet::new();
|
let mut total: usize = src.iter().map(|v| v.1.len()).sum();
|
||||||
for (rel_path, copy_recursive) in rel_paths {
|
for (parent, rel_paths) in src {
|
||||||
total = total.saturating_sub(1);
|
let mut created: HashSet<PathBuf> = HashSet::new();
|
||||||
{
|
for (rel_path, copy_recursive) in rel_paths {
|
||||||
let s = format!("cp {total}");
|
total = total.saturating_sub(1);
|
||||||
*status.lock().unwrap() = s;
|
{
|
||||||
}
|
let s = format!("cp {total}");
|
||||||
let file_from = parent.join(&rel_path);
|
*status.lock().unwrap() = s;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} else {
|
let file_from = parent.join(&rel_path);
|
||||||
let rel_path = rel_path.file_name().unwrap();
|
|
||||||
let file_to = target.join(&rel_path);
|
let file_to = target.join(&rel_path);
|
||||||
if is_dir {
|
let is_dir = file_from.is_dir();
|
||||||
copy_dir(file_from, file_to, copy_recursive);
|
let parent_created = if let Some(parent) = rel_path.parent() {
|
||||||
created.insert(rel_path.into());
|
parent.as_os_str().is_empty() || created.contains(parent)
|
||||||
} else {
|
} 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(
|
fn copy_dir(
|
||||||
file_from: impl AsRef<Path>,
|
file_from: impl AsRef<Path>,
|
||||||
@ -76,19 +82,42 @@ 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(
|
||||||
for path in paths {
|
format!("rm {total}"),
|
||||||
{
|
move |status| {
|
||||||
let s = format!("rm {total}");
|
for path in paths {
|
||||||
*status.lock().unwrap() = s;
|
{
|
||||||
|
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() {
|
Ok(())
|
||||||
fs::remove_dir(path);
|
},
|
||||||
} else {
|
true,
|
||||||
fs::remove_file(path);
|
));
|
||||||
}
|
}
|
||||||
}
|
pub(crate) fn task_chmod(paths: Vec<PathBuf>, mode: u32, share: &mut Share) {
|
||||||
Ok(())
|
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,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user