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
|
||||
- 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`.
|
||||
|
61
src/main.rs
61
src/main.rs
@ -3,10 +3,9 @@ mod tasks;
|
||||
mod updates;
|
||||
|
||||
use std::{
|
||||
fs::{self, DirEntry, Metadata},
|
||||
fs::{self, Metadata},
|
||||
io::{self, StdoutLock},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
@ -46,6 +45,7 @@ fn main() -> io::Result<()> {
|
||||
terminal_command: std::env::var("TERM").unwrap_or("alacritty".to_string()),
|
||||
editor_command: std::env::var("EDITOR").unwrap_or("nano".to_string()),
|
||||
live_search: !args.no_live_search,
|
||||
info_what: vec![0, 1],
|
||||
};
|
||||
if args.check {
|
||||
eprintln!("Terminal: {}", share.terminal_command);
|
||||
@ -111,11 +111,7 @@ fn main() -> io::Result<()> {
|
||||
.filter(|e| e.selected)
|
||||
.filter_map(|e| {
|
||||
Some((
|
||||
e.entry
|
||||
.path()
|
||||
.strip_prefix(&v.current_dir)
|
||||
.ok()?
|
||||
.to_owned(),
|
||||
e.path.strip_prefix(&v.current_dir).ok()?.to_owned(),
|
||||
e.rel_depth == v.scan_files_max_depth,
|
||||
))
|
||||
})
|
||||
@ -193,30 +189,39 @@ struct Share {
|
||||
live_search: bool,
|
||||
terminal_command: String,
|
||||
editor_command: String,
|
||||
/// 0: size
|
||||
/// 1: mode (permissions)
|
||||
info_what: Vec<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) {
|
||||
|
365
src/run.rs
365
src/run.rs
@ -4,12 +4,16 @@ use crossterm::{cursor, queue, style, terminal, ExecutableCommand};
|
||||
use regex::RegexBuilder;
|
||||
|
||||
use crate::updates::Updates;
|
||||
use crate::{tasks, AppCmd, DirContent, DirContentType, Focus, Share};
|
||||
use crate::{
|
||||
tasks, AppCmd, BackgroundTask, DirContent, DirContentType, Focus, ScanFilesMode, Share,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, io};
|
||||
|
||||
use crate::TuiFile;
|
||||
@ -33,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)
|
||||
|
39
src/tasks.rs
39
src/tasks.rs
@ -1,7 +1,9 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs, io,
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{BackgroundTask, Share};
|
||||
@ -11,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,
|
||||
));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user