From 009debaeb75dee166fd64b46955c3dc4c823b4da Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 27 Aug 2023 05:09:40 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 461 ++++++++++++++++++++++++++++++++ Cargo.toml | 11 + README.md | 14 + src/main.rs | 392 +++++++++++++++++++++++++++ src/run.rs | 701 +++++++++++++++++++++++++++++++++++++++++++++++++ src/tasks.rs | 95 +++++++ src/updates.rs | 126 +++++++++ 8 files changed, 1801 insertions(+) create mode 100755 .gitignore create mode 100755 Cargo.lock create mode 100755 Cargo.toml create mode 100755 README.md create mode 100755 src/main.rs create mode 100755 src/run.rs create mode 100755 src/tasks.rs create mode 100755 src/updates.rs diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..1c054d0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,461 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tuifile" +version = "0.1.0" +dependencies = [ + "clap", + "crossterm", + "regex", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..afb2adc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tuifile" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.4.0", features = ["derive"] } +crossterm = "0.27.0" +regex = "1.9.4" diff --git a/README.md b/README.md new file mode 100755 index 0000000..a7e1411 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# TuiFile + +A file explorer for your terminal, with homerow-centric navigation. + +TuiFile can + +- have multiple instances, one for each open directory +- display recursive directory structures +- filter files using regex +- select multiple files at once +- create new directories +- copy, move and delete +- quickly open your `$TERM` and `$EDITOR` +- add more features (open an issue with ideas if you have any) diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..567d9f6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,392 @@ +mod run; +mod tasks; +mod updates; + +use std::{ + fs::{self, DirEntry, Metadata}, + io::{self, StdoutLock}, + path::PathBuf, + rc::Rc, + sync::{Arc, Mutex}, + thread::JoinHandle, + time::Duration, +}; + +use clap::{command, Parser}; +use crossterm::terminal; +use regex::Regex; +use updates::Updates; + +const EXIT_NO_ABSOLUTE_PATH: i32 = 1; + +fn main() -> io::Result<()> { + let args = Args::parse(); + let current_dir = match args.dir { + Some(dir) => { + if args.dir_relative || dir.is_absolute() { + dir + } else { + match fs::canonicalize(dir) { + Ok(p) => p, + Err(e) => { + eprintln!("Error getting absolute path: {e}."); + std::process::exit(EXIT_NO_ABSOLUTE_PATH); + } + } + } + } + None => std::env::current_dir().unwrap_or(PathBuf::from("/")), + }; + let mut share = Share { + status: String::new(), + tasks: vec![], + active_instance: 0, + total_instances: 1, + stdout: io::stdout().lock(), + size: terminal::size()?, + 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, + }; + if args.check { + eprintln!("Terminal: {}", share.terminal_command); + eprintln!("Editor: {}", share.editor_command); + return Ok(()); + } + let mut instances = vec![TuiFile::new(current_dir)?]; + TuiFile::term_setup_no_redraw(&mut share)?; + let mut redraw = true; + loop { + if instances.is_empty() { + break; + } + if share.active_instance >= instances.len() { + share.active_instance = instances.len() - 1; + } + share.total_instances = instances.len(); + let instance = &mut instances[share.active_instance]; + if redraw { + instance.updates.request_clear(); + instance.updates.request_redraw(); + if instance.active { + share.status = format!("{}", share.active_instance); + } + } + let cmd = instance.run(&mut share)?; + redraw = match cmd { + AppCmd::Quit => break, + AppCmd::CloseInstance => { + instances.remove(share.active_instance); + if share.active_instance > 0 { + share.active_instance -= 1; + } + true + } + AppCmd::NextInstance => { + if share.active_instance + 1 < instances.len() { + share.active_instance += 1; + } + true + } + AppCmd::PrevInstance => { + if share.active_instance > 0 { + share.active_instance -= 1; + } + true + } + AppCmd::AddInstance(new) => { + share.active_instance += 1; + instances.insert(share.active_instance, new); + true + } + AppCmd::CopyTo(destination) => { + instance.updates.request_redraw_infobar(); + let src = instances + .iter() + .filter(|v| v.active) + .map(|v| { + ( + v.current_dir.clone(), + v.dir_content + .iter() + .filter(|e| e.selected) + .filter_map(|e| { + Some(( + e.entry + .path() + .strip_prefix(&v.current_dir) + .ok()? + .to_owned(), + e.rel_depth == v.scan_files_max_depth, + )) + }) + .collect(), + ) + }) + .collect(); + tasks::task_copy(src, destination, &mut share); + false + } + AppCmd::TaskFinished => { + for i in &mut instances { + i.updates.request_rescan_files(); + } + false + } + }; + } + TuiFile::term_reset(&mut share)?; + Ok(()) +} + +/// TUI file explorer. Long Help is available with --help. +/// +/// Controls: +/// - Ctrl+Up/K => previous +/// - Ctrl+Down/J => next +/// - Ctrl+Left/H => close +/// - Ctrl+Right/L => duplicate +/// Files: +/// - Up/K or Down/J => move selection +/// - Left/H => go to parent directory +/// - Right/L => go into selected entry +/// - A => Alternate selection (toggle All) +/// - S => Select or toggle current +/// - D => Deselect all +/// - F => focus Find/Filter bar +/// - N => New directory from search text +/// - C => Copy selected files to this directory. +/// - 1-9 or 0 => set recursive depth limit (0 = infinite) +/// - T => open terminal here ($TERM) +/// - E => open in editor ($EDITOR ) +/// Find/Filter Bar: +/// - Esc: back and discard +/// - Enter: back and apply +/// - Backspace: delete +/// - type to enter search regex +#[derive(Parser, Debug)] +#[command(version, verbatim_doc_comment)] +struct Args { + /// the directory you want to view. + dir: Option, + /// skips converting the 'dir' argument to an absolute path. + /// this causes issues when trying to view parent directories + /// but may be necessary if tuifile doesn't start. + #[arg(long)] + dir_relative: bool, + /// performs some checks and prints results. + #[arg(long)] + check: bool, + /// disables live search, only filtering the file list when enter is pressed. + #[arg(long)] + no_live_search: bool, +} + +struct Share { + status: String, + tasks: Vec, + active_instance: usize, + total_instances: usize, + size: (u16, u16), + stdout: StdoutLock<'static>, + // + live_search: bool, + terminal_command: String, + editor_command: String, +} +impl Share { + fn check_bgtasks(&mut self) -> bool { + for (i, task) in self.tasks.iter_mut().enumerate() { + if task.thread.is_finished() { + self.tasks.remove(i); + return true; + } + } + false + } +} +struct BackgroundTask { + status: Arc>, + thread: JoinHandle>, +} +impl BackgroundTask { + pub fn new( + func: impl FnOnce(Arc>) -> Result<(), String> + Send + 'static, + ) -> Self { + let status = Arc::new(Mutex::new(String::new())); + Self { + status: Arc::clone(&status), + thread: std::thread::spawn(move || func(status)), + } + } +} +struct TuiFile { + active: bool, + updates: u32, + current_dir: PathBuf, + dir_content: Vec, + dir_content_len: usize, + scroll: usize, + current_index: usize, + focus: Focus, + scan_files_max_depth: usize, + files_status_is_special: bool, + files_status: String, + search_text: String, + search_regex: Option, + last_drawn_files_height: usize, + last_drawn_files_count: usize, + last_files_max_scroll: usize, + after_rescanning_files: Vec>, +} +#[derive(Clone)] +struct DirContent { + entry: Rc, + name: String, + name_charlen: usize, + rel_depth: usize, + passes_filter: bool, + selected: bool, + more: DirContentType, +} +#[derive(Clone)] +enum DirContentType { + /// Couldn't get more info on this entry + Err(String), + Dir { + metadata: Metadata, + }, + File { + size: String, + metadata: Metadata, + }, + Symlink { + metadata: Metadata, + }, +} +#[derive(Clone)] +enum Focus { + Files, + SearchBar, +} +enum AppCmd { + Quit, + CloseInstance, + NextInstance, + PrevInstance, + AddInstance(TuiFile), + CopyTo(PathBuf), + TaskFinished, +} +impl TuiFile { + pub fn clone(&self) -> Self { + Self { + active: self.active, + updates: 0, + current_dir: self.current_dir.clone(), + dir_content: self.dir_content.clone(), + dir_content_len: self.dir_content_len, + scroll: self.scroll, + current_index: self.current_index, + focus: self.focus.clone(), + scan_files_max_depth: self.scan_files_max_depth, + files_status_is_special: self.files_status_is_special, + files_status: self.files_status.clone(), + search_text: self.search_text.clone(), + search_regex: self.search_regex.clone(), + last_drawn_files_height: self.last_drawn_files_height, + last_drawn_files_count: self.last_drawn_files_count, + last_files_max_scroll: self.last_files_max_scroll, + after_rescanning_files: vec![], + } + } + pub fn new(current_dir: PathBuf) -> io::Result { + // state + let (width, height) = terminal::size()?; + let updates = u32::MAX; + Ok(Self { + active: true, + updates, + current_dir, + dir_content: vec![], + dir_content_len: 0, + scroll: 0, + current_index: 0, + focus: Focus::Files, + scan_files_max_depth: 0, + files_status_is_special: false, + files_status: String::new(), + search_text: String::new(), + search_regex: None, + last_drawn_files_height: 0, + last_drawn_files_count: 0, + last_files_max_scroll: 0, + after_rescanning_files: vec![], + }) + } + fn set_current_index(&mut self, mut i: usize) { + if i >= self.dir_content.len() { + i = self.dir_content.len().saturating_sub(1); + } + if i == self.current_index { + return; + } + if i < self.scroll { + self.scroll = i; + self.updates.request_redraw_filelist(); + } + if i >= self.scroll + self.last_drawn_files_height { + self.scroll = 1 + i - self.last_drawn_files_height; + self.updates.request_redraw_filelist(); + } + self.updates.request_move_cursor(); + // self.updates.request_redraw_filelist(); + self.current_index = i; + } + /// starting from `start`, checks all indices until it finds a visible entry or there are no more entries. + /// If an entry was found, the current_index will be set to that entry. + fn set_current_index_to_visible(&mut self, start: usize, inc: bool) { + let mut i = start; + loop { + if self.dir_content.get(i).is_some_and(|e| e.passes_filter) { + self.set_current_index(i); + return; + } + if inc { + i += 1; + if i >= self.dir_content.len() { + break; + } + } else if i > 0 { + i -= 1; + } else { + break; + } + } + } + fn request_rescan_files_then_select( + &mut self, + find_by: impl FnMut(&DirContent) -> bool + 'static, + ) { + self.updates.request_rescan_files(); + self.after_rescanning_files.push(Box::new(move |s| { + if let Some(i) = s.dir_content.iter().position(find_by) { + s.set_current_index(i) + } else { + s.updates.request_reset_current_index(); + } + })); + } + fn request_rescan_files_then_select_by_name(&mut self, name: String) { + self.request_rescan_files_then_select(move |e| { + e.name == name || e.name.ends_with('/') && e.name[..e.name.len() - 1] == name + }); + } + fn request_rescan_files_then_select_current_again(&mut self) { + if let Some(c) = self.dir_content.get(self.current_index) { + self.request_rescan_files_then_select_by_name(c.name.clone()); + } else { + self.updates.request_rescan_files(); + } + } +} diff --git a/src/run.rs b/src/run.rs new file mode 100755 index 0000000..db28864 --- /dev/null +++ b/src/run.rs @@ -0,0 +1,701 @@ +use crossterm::event::{poll, read, Event, KeyCode, KeyModifiers}; +use crossterm::style::{Attribute, Color, Stylize}; +use crossterm::{cursor, queue, style, terminal, ExecutableCommand}; +use regex::RegexBuilder; + +use crate::updates::Updates; +use crate::{tasks, AppCmd, BackgroundTask, DirContent, DirContentType, Focus, Share}; +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::rc::Rc; +use std::time::Duration; +use std::{fs, io}; + +use crate::TuiFile; + +const BYTE_UNITS: [&'static str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; + +impl TuiFile { + pub fn term_setup(&mut self, share: &mut Share) -> io::Result<()> { + self.updates.request_redraw(); + Self::term_setup_no_redraw(share) + } + pub fn term_setup_no_redraw(share: &mut Share) -> io::Result<()> { + share.stdout.execute(terminal::EnterAlternateScreen)?; + terminal::enable_raw_mode()?; + Ok(()) + } + pub fn term_reset(share: &mut Share) -> io::Result<()> { + terminal::disable_raw_mode()?; + share.stdout.execute(terminal::LeaveAlternateScreen)?; + Ok(()) + } + pub fn run(&mut self, share: &mut Share) -> io::Result { + loop { + if share.check_bgtasks() { + return Ok(AppCmd::TaskFinished); + } + // rescan files if necessary + if self.updates.rescan_files() { + self.updates.dont_rescan_files(); + self.updates.request_filter_files(); + self.files_status_is_special = false; + self.dir_content.clear(); + get_files(self, self.current_dir.clone(), 0); + fn get_files(s: &mut TuiFile, dir: PathBuf, depth: usize) { + match fs::read_dir(&dir) { + Err(e) => { + if depth == 0 { + s.dir_content = vec![]; + s.files_status = format!("{e}"); + s.files_status_is_special = true; + } + } + Ok(files) => { + for entry in files { + if let Ok(entry) = entry { + let mut name = entry.file_name().to_string_lossy().into_owned(); + let metadata = entry.metadata(); + let p = entry.path(); + let more = match metadata { + Err(e) => DirContentType::Err(e.to_string()), + Ok(metadata) => { + if metadata.is_symlink() { + DirContentType::Symlink { metadata } + } else if metadata.is_file() { + DirContentType::File { + size: { + let mut bytes = metadata.len(); + let mut i = 0; + loop { + if bytes < 1024 + || i + 1 >= BYTE_UNITS.len() + { + break format!( + "{bytes}{}", + BYTE_UNITS[i] + ); + } else { + i += 1; + // divide by 1024 but cooler + bytes >>= 10; + } + } + }, + metadata, + } + } else if metadata.is_dir() { + DirContentType::Dir { metadata } + } else { + DirContentType::Err(format!( + "not a file, dir or symlink" + )) + } + } + }; + if let DirContentType::Dir { .. } = more { + name.push('/'); + } + s.dir_content.push(DirContent { + entry: Rc::new(entry), + name_charlen: name.chars().count(), + name, + rel_depth: depth, + passes_filter: true, + selected: false, + more, + }); + if depth < s.scan_files_max_depth { + get_files(s, p, depth + 1); + } + } + } + } + } + } + if self.current_index >= self.dir_content.len() { + self.current_index = self.dir_content.len().saturating_sub(1); + } + if !self.after_rescanning_files.is_empty() { + for func in std::mem::replace(&mut self.after_rescanning_files, vec![]) { + func(self); + } + } + } + if self.updates.reset_search() { + self.updates.dont_reset_search(); + if !self.search_text.is_empty() { + self.search_text.clear(); + self.search_regex = None; + self.updates.request_redraw_searchbar(); + } + } + if self.updates.filter_files() { + if self.search_regex.is_none() && !self.search_text.is_empty() { + self.search_regex = RegexBuilder::new(&self.search_text) + .case_insensitive(true) + .build() + .ok(); + } + self.updates.dont_filter_files(); + self.updates.request_redraw_filelist(); + if let Some(regex) = &self.search_regex { + self.dir_content_len = 0; + for entry in &mut self.dir_content { + entry.passes_filter = regex.is_match(&entry.name); + if entry.passes_filter { + self.dir_content_len += 1; + } + } + } else { + for entry in &mut self.dir_content { + entry.passes_filter = true; + } + self.dir_content_len = self.dir_content.len(); + } + if !self.files_status_is_special { + self.files_status = match ( + self.dir_content_len != self.dir_content.len(), + self.dir_content.len() == 1, + ) { + (false, false) => format!("{} entries", self.dir_content_len), + (false, true) => format!("1 entry"), + (true, false) => format!( + "{} of {} entries", + self.dir_content_len, + self.dir_content.len() + ), + (true, true) => format!("{} of 1 entry", self.dir_content_len), + }; + if self.scan_files_max_depth > 0 { + if let Some(v) = self.scan_files_max_depth.checked_add(1) { + self.files_status.push_str(&format!(" ({v} layers)",)); + } else { + self.files_status.push_str(&format!(" (recursive)",)); + } + } + } + } + if self.updates.reset_current_index() { + self.updates.dont_reset_current_index(); + self.set_current_index_to_visible(0, true); + } + // draw tui + if share.size.0 > 0 && share.size.1 > 0 { + if self.updates.clear() { + self.updates.dont_clear(); + self.updates.request_move_cursor(); + queue!(share.stdout, terminal::Clear(terminal::ClearType::All))?; + } + if self.updates.redraw_infobar() { + self.updates.dont_redraw_infobar(); + self.updates.request_move_cursor(); + let mut pathstring = share.status.clone(); + if share.tasks.len() > 0 { + self.updates.request_redraw_infobar(); + for task in share.tasks.iter() { + pathstring.push_str(" | "); + pathstring.push_str(task.status.lock().unwrap().as_str()); + } + } + pathstring.push_str(" - "); + if share.size.0 as usize > pathstring.len() { + let mut pathchars = Vec::with_capacity(self.current_dir.as_os_str().len()); + let mut maxlen = share.size.0 as usize - pathstring.len(); + for ch in self + .current_dir + .as_os_str() + .to_string_lossy() + .to_string() + .chars() + .rev() + { + if maxlen > 0 { + pathchars.push(ch); + maxlen -= 1; + } + } + pathstring.extend(pathchars.into_iter().rev()); + pathstring.reserve_exact(maxlen as usize); + for _ in 0..maxlen { + pathstring.push(' '); + } + queue!( + share.stdout, + cursor::MoveTo(0, 0), + style::PrintStyledContent( + pathstring + .with(Color::Cyan) + .attribute(Attribute::Underlined) + ) + )?; + } + } + if self.updates.redraw_filelist() { + self.updates.dont_redraw_filelist(); + self.updates.request_move_cursor(); + self.last_drawn_files_height = share.size.1.saturating_sub(3) as _; + let mut status = format!(" {}", self.files_status); + while status.len() < share.size.0 as usize { + status.push(' '); + } + queue!( + share.stdout, + cursor::MoveTo(0, 1), + style::PrintStyledContent(status.attribute(Attribute::Italic)), + )?; + self.last_files_max_scroll = self + .dir_content_len + .saturating_sub(self.last_drawn_files_height); + let scrollbar_where = if self.last_files_max_scroll > 0 { + Some( + self.last_drawn_files_height.saturating_sub(1) * self.scroll + / self.last_files_max_scroll, + ) + } else { + None + }; + let mut drawn_files = 0; + for (line, entry) in self + .dir_content + .iter() + .skip(self.scroll) + .filter(|e| e.passes_filter) + .take(self.last_drawn_files_height) + .enumerate() + { + drawn_files += 1; + let (mut text, mut text_charlen) = ("- ".to_string(), 2); + for _ in 0..entry.rel_depth { + text.push_str(" | "); + } + text_charlen += entry.rel_depth * 4; + let endchar = if let Some(sb_where) = scrollbar_where { + if line == sb_where { + '#' + } else { + '|' + } + } else { + ' ' + }; + let styled = match &entry.more { + DirContentType::Err(e) => { + text.push_str(&entry.name); + text_charlen += entry.name_charlen; + while text_charlen + 9 > share.size.0 as usize { + text.pop(); + text_charlen -= 1; + } + text.push_str(" - Err: "); + text_charlen += 8; + for ch in e.chars() { + if ch == '\n' || ch == '\r' { + continue; + } + if text_charlen >= share.size.0 as usize { + break; + } + text_charlen += 1; + text.push(ch); + } + // make text_charlen 1 too large (for the endchar) + text_charlen += 1; + while text_charlen < share.size.0 as _ { + text.push(' '); + text_charlen += 1; + } + text.push(endchar); + vec![text.red()] + } + DirContentType::Dir { metadata } => { + let filenamelen = share.size.0 as usize - 2 - text_charlen; + if entry.name_charlen < filenamelen { + text.push_str(&entry.name); + for _ in 0..(filenamelen - entry.name_charlen) { + text.push(' '); + } + } else if entry.name_charlen == filenamelen { + text.push_str(&entry.name); + } else { + // the new length is the old length minus the combined length of the characters we want to cut off + let i = entry.name.len() + - entry + .name + .chars() + .rev() + .take(entry.name_charlen - filenamelen) + .map(|char| char.len_utf8()) + .sum::(); + text.push_str(&entry.name[0..i.saturating_sub(3)]); + text.push_str("..."); + } + text.push(' '); + 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::(); + text.push_str(&entry.name[0..i.saturating_sub(3)]); + text.push_str("..."); + } + text.push(' '); + text.push_str(&size); + text.push(' '); + text.push(endchar); + vec![text.stylize()] + } + DirContentType::Symlink { metadata } => { + let filenamelen = share.size.0 as usize - 2 - text_charlen; + if entry.name_charlen < filenamelen { + text.push_str(&entry.name); + for _ in 0..(filenamelen - entry.name_charlen) { + text.push(' '); + } + } else if entry.name_charlen == filenamelen { + text.push_str(&entry.name); + } else { + // the new length is the old length minus the combined length of the characters we want to cut off + let i = entry.name.len() + - entry + .name + .chars() + .rev() + .take(entry.name_charlen - filenamelen) + .map(|char| char.len_utf8()) + .sum::(); + text.push_str(&entry.name[0..i.saturating_sub(3)]); + text.push_str("..."); + } + text.push(' '); + text.push(endchar); + vec![text.italic()] + } + }; + queue!(share.stdout, cursor::MoveToNextLine(1))?; + for mut s in styled { + if entry.selected { + s = s.bold(); + } + queue!(share.stdout, style::PrintStyledContent(s))?; + } + } + let empty_lines = self.last_drawn_files_count.saturating_sub(drawn_files); + self.last_drawn_files_count = drawn_files; + let empty_line = " ".repeat(share.size.0 as _); + for _ in 0..empty_lines { + queue!( + share.stdout, + cursor::MoveToNextLine(1), + style::PrintStyledContent(empty_line.as_str().stylize()) + )?; + } + } + if self.updates.redraw_searchbar() { + self.updates.dont_redraw_searchbar(); + self.updates.request_move_cursor(); + let mut text = if self.search_text.len() > share.size.0 as _ { + self.search_text[(self.search_text.len() - share.size.0 as usize)..] + .to_string() + } else { + self.search_text.clone() + }; + while text.len() < share.size.0 as _ { + text.push(' '); + } + queue!( + share.stdout, + cursor::MoveTo(0, share.size.1 - 1), + style::PrintStyledContent(text.underlined()) + ); + } + if self.updates.move_cursor() { + self.updates.dont_move_cursor(); + match self.focus { + Focus::Files => { + if self + .dir_content + .get(self.current_index) + .is_some_and(|e| e.passes_filter) + { + let height = self + .dir_content + .iter() + .skip(self.scroll) + .take(self.current_index.saturating_sub(self.scroll)) + .filter(|e| e.passes_filter) + .count(); + if height < self.last_drawn_files_height { + queue!(share.stdout, cursor::MoveTo(0, 2 + height as u16))?; + } else { + queue!(share.stdout, cursor::MoveTo(0, 1))?; + } + } else { + queue!(share.stdout, cursor::MoveTo(0, 1))?; + } + } + Focus::SearchBar => { + queue!( + share.stdout, + cursor::MoveTo(self.search_text.len() as _, share.size.1 - 1) + )?; + } + } + } + } + // end of draw + share.stdout.flush()?; + // events + if poll(Duration::from_millis(100))? { + match read()? { + Event::FocusGained => {} + Event::FocusLost => {} + Event::Mouse(e) => match (e.kind, e.column, e.row, e.modifiers) { + _ => {} + }, + Event::Key(e) => match (&self.focus, e.code) { + // - - - Global - - - + // Ctrl+Left/H -> Close + (_, KeyCode::Left | KeyCode::Char('h')) + if e.modifiers == KeyModifiers::CONTROL => + { + return Ok(AppCmd::CloseInstance); + } + // Ctrl+Right/L -> Duplicate + (_, KeyCode::Right | KeyCode::Char('l')) + if e.modifiers == KeyModifiers::CONTROL => + { + return Ok(AppCmd::AddInstance(self.clone())); + } + // Ctrl+Up/K -> Prev + (_, KeyCode::Up | KeyCode::Char('k')) + if e.modifiers == KeyModifiers::CONTROL => + { + return Ok(AppCmd::PrevInstance); + } + // Ctrl+Down/J -> Next + (_, KeyCode::Down | KeyCode::Char('j')) + if e.modifiers == KeyModifiers::CONTROL => + { + return Ok(AppCmd::NextInstance); + } + // - - - Files - - - + // Down/J -> Down + (Focus::Files, KeyCode::Down | KeyCode::Char('j')) => { + self.set_current_index_to_visible(self.current_index + 1, true) + } + // Up/K -> Up + (Focus::Files, KeyCode::Up | KeyCode::Char('k')) => { + if self.current_index > 0 { + self.set_current_index_to_visible(self.current_index - 1, false) + } + } + // Left/H -> Leave Directory + (Focus::Files, KeyCode::Left | KeyCode::Char('h')) => { + // leave directory + if let Some(this_dir) = self + .current_dir + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + { + self.current_dir.pop(); + self.updates.request_redraw_infobar(); + self.request_rescan_files_then_select_by_name(this_dir); + } + } + // Right/L -> Enter Directory + (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.updates = u32::MAX; + } + } + // A -> Select All + (Focus::Files, KeyCode::Char('a')) => { + self.updates.request_redraw_filelist(); + for e in &mut self.dir_content { + if e.passes_filter { + e.selected = !e.selected; + } + } + } + // S -> Toggle Select + (Focus::Files, KeyCode::Char('s')) => { + self.updates.request_redraw_filelist(); + if let Some(e) = self.dir_content.get_mut(self.current_index) { + e.selected = !e.selected; + } + } + // D -> Deselect All + (Focus::Files, KeyCode::Char('d')) => { + self.updates.request_redraw_filelist(); + for e in &mut self.dir_content { + if e.passes_filter { + e.selected = false; + } + } + } + (Focus::Files, KeyCode::Char('f')) => { + self.focus = Focus::SearchBar; + self.updates.request_move_cursor(); + } + // N -> New Directory + (Focus::Files, KeyCode::Char('n')) => { + let dir = self.current_dir.join(&self.search_text); + if fs::create_dir_all(&dir).is_ok() { + self.updates.request_reset_search(); + self.current_dir = dir; + self.updates.request_redraw_infobar(); + } + self.updates.request_rescan_files(); + } + // C -> Copy + (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())); + } + } + } + // R -> Remove + (Focus::Files, KeyCode::Char('r')) => { + let paths = self + .dir_content + .iter() + .rev() + .filter(|e| e.selected) + .map(|e| e.entry.path()) + .collect(); + tasks::task_del(paths, share); + } + // T -> Open Terminal + (Focus::Files, KeyCode::Char('t')) => 'term: { + Command::new(&share.terminal_command) + .current_dir(&self.current_dir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn(); + } + // E -> Edit + (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(); + Command::new(&share.editor_command) + .arg(&entry_path) + .current_dir(&self.current_dir) + .status(); + } + self.term_setup(share)?; + } + // 0-9 -> set scan_files_max_depth + (Focus::Files, KeyCode::Char('0')) => { + self.scan_files_max_depth = usize::MAX; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('1')) => { + self.scan_files_max_depth = 0; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('2')) => { + self.scan_files_max_depth = 1; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('3')) => { + self.scan_files_max_depth = 2; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('4')) => { + self.scan_files_max_depth = 3; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('5')) => { + self.scan_files_max_depth = 4; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('6')) => { + self.scan_files_max_depth = 5; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('7')) => { + self.scan_files_max_depth = 6; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('8')) => { + self.scan_files_max_depth = 7; + self.request_rescan_files_then_select_current_again(); + } + (Focus::Files, KeyCode::Char('9')) => { + self.scan_files_max_depth = 8; + self.request_rescan_files_then_select_current_again(); + } + // - - - SearchBar - - - + // Esc -> Nevermind + (Focus::SearchBar, KeyCode::Esc) => { + self.focus = Focus::Files; + self.search_text.clear(); + self.search_regex = None; + self.updates.request_redraw_searchbar(); + self.updates.request_move_cursor(); + if share.live_search { + self.updates.request_filter_files(); + } + } + // Enter -> Apply + (Focus::SearchBar, KeyCode::Enter) => { + self.focus = Focus::Files; + self.updates.request_move_cursor(); + if !share.live_search { + self.updates.request_filter_files(); + } + self.updates.request_reset_current_index(); + } + (Focus::SearchBar, KeyCode::Char(ch)) => { + self.search_text.push(ch); + self.search_regex = None; + self.updates.request_redraw_searchbar(); + if share.live_search { + self.updates.request_filter_files(); + } + } + (Focus::SearchBar, KeyCode::Backspace) => { + self.search_text.pop(); + self.search_regex = None; + self.updates.request_redraw_searchbar(); + if share.live_search { + self.updates.request_filter_files(); + } + } + _ => {} + }, + Event::Paste(e) => {} + Event::Resize(w, h) => { + share.size.0 = w; + share.size.1 = h; + self.updates.request_redraw(); + } + } + } + } + } +} diff --git a/src/tasks.rs b/src/tasks.rs new file mode 100755 index 0000000..5e85ce7 --- /dev/null +++ b/src/tasks.rs @@ -0,0 +1,95 @@ +use std::{ + collections::HashSet, + fs, io, + path::{Path, PathBuf}, + time::Duration, +}; + +use crate::{updates::Updates, BackgroundTask, Share, TuiFile}; + +pub(crate) fn task_copy( + src: Vec<(PathBuf, Vec<(PathBuf, bool)>)>, + target: PathBuf, + share: &mut Share, +) { + share.tasks.push(BackgroundTask::new(move |status| { + let mut total: usize = src.iter().map(|v| v.1.len()).sum(); + for (parent, rel_paths) in src { + let mut created: HashSet = HashSet::new(); + for (rel_path, copy_recursive) in rel_paths { + total = total.saturating_sub(1); + { + let s = format!("cp {total}"); + *status.lock().unwrap() = s; + } + let file_from = parent.join(&rel_path); + let file_to = target.join(&rel_path); + let is_dir = file_from.is_dir(); + let parent_created = if let Some(parent) = rel_path.parent() { + parent.as_os_str().is_empty() || created.contains(parent) + } else { + true + }; + if parent_created { + if is_dir { + copy_dir(file_from, file_to, copy_recursive); + created.insert(rel_path); + } else { + fs::copy(&file_from, &file_to); + } + } 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(()) + })); +} +fn copy_dir( + file_from: impl AsRef, + file_to: impl AsRef, + recursive: bool, +) -> io::Result<()> { + fs::create_dir(&file_to)?; + if recursive { + if let Ok(e) = fs::read_dir(file_from) { + for e in e { + if let Ok(e) = e { + let p = e.path(); + let t = file_to.as_ref().join(e.file_name()); + if p.is_dir() { + copy_dir(p, t, recursive); + } else { + fs::copy(&p, &t); + } + } + } + } + } + Ok(()) +} + +pub(crate) fn task_del(paths: Vec, share: &mut Share) { + share.tasks.push(BackgroundTask::new(move |status| { + let mut total: usize = paths.len(); + for path in paths { + { + let s = format!("rm {total}"); + *status.lock().unwrap() = s; + } + if path.is_dir() { + fs::remove_dir(path); + } else { + fs::remove_file(path); + } + } + Ok(()) + })); +} diff --git a/src/updates.rs b/src/updates.rs new file mode 100755 index 0000000..b030099 --- /dev/null +++ b/src/updates.rs @@ -0,0 +1,126 @@ +pub(crate) trait Updates { + fn request_redraw(&mut self) { + self.request_redraw_infobar(); + self.request_redraw_searchbar(); + self.request_redraw_filelist(); + } + + fn rescan_files(&self) -> bool; + fn dont_rescan_files(&mut self); + fn request_rescan_files(&mut self); + + fn clear(&self) -> bool; + fn dont_clear(&mut self); + fn request_clear(&mut self); + + fn redraw_infobar(&self) -> bool; + fn dont_redraw_infobar(&mut self); + fn request_redraw_infobar(&mut self); + + fn redraw_searchbar(&self) -> bool; + fn dont_redraw_searchbar(&mut self); + fn request_redraw_searchbar(&mut self); + + fn redraw_filelist(&self) -> bool; + fn dont_redraw_filelist(&mut self); + fn request_redraw_filelist(&mut self); + + fn move_cursor(&self) -> bool; + fn dont_move_cursor(&mut self); + fn request_move_cursor(&mut self); + + fn filter_files(&self) -> bool; + fn dont_filter_files(&mut self); + fn request_filter_files(&mut self); + + fn reset_current_index(&self) -> bool; + fn dont_reset_current_index(&mut self); + fn request_reset_current_index(&mut self); + + fn reset_search(&self) -> bool; + fn dont_reset_search(&mut self); + fn request_reset_search(&mut self); +} +impl Updates for u32 { + fn rescan_files(&self) -> bool { + 0 != self & 0b1 + } + fn dont_rescan_files(&mut self) { + *self ^= 0b1; + } + fn request_rescan_files(&mut self) { + *self |= 0b1; + } + fn clear(&self) -> bool { + 0 != self & 0b10 + } + fn dont_clear(&mut self) { + *self ^= 0b10; + } + fn request_clear(&mut self) { + *self |= 0b10; + } + fn redraw_infobar(&self) -> bool { + 0 != self & 0b100 + } + fn dont_redraw_infobar(&mut self) { + *self ^= 0b100; + } + fn request_redraw_infobar(&mut self) { + *self |= 0b100; + } + fn redraw_searchbar(&self) -> bool { + 0 != self & 0b1000 + } + fn dont_redraw_searchbar(&mut self) { + *self ^= 0b1000; + } + fn request_redraw_searchbar(&mut self) { + *self |= 0b1000; + } + fn redraw_filelist(&self) -> bool { + 0 != self & 0b10000 + } + fn dont_redraw_filelist(&mut self) { + *self ^= 0b10000; + } + fn request_redraw_filelist(&mut self) { + *self |= 0b10000; + } + fn move_cursor(&self) -> bool { + 0 != self & 0b100000 + } + fn dont_move_cursor(&mut self) { + *self ^= 0b100000; + } + fn request_move_cursor(&mut self) { + *self |= 0b100000; + } + fn filter_files(&self) -> bool { + 0 != self & 0b1000000 + } + fn dont_filter_files(&mut self) { + *self ^= 0b1000000; + } + fn request_filter_files(&mut self) { + *self |= 0b1000000; + } + fn reset_current_index(&self) -> bool { + 0 != self & 0b10000000 + } + fn dont_reset_current_index(&mut self) { + *self ^= 0b10000000; + } + fn request_reset_current_index(&mut self) { + *self |= 0b10000000; + } + fn reset_search(&self) -> bool { + 0 != self & 0b100000000 + } + fn dont_reset_search(&mut self) { + *self ^= 0b100000000; + } + fn request_reset_search(&mut self) { + *self |= 0b100000000; + } +}