Compare commits
10 commits
6b0731a6f1
...
7a35f4ac3e
Author | SHA1 | Date | |
---|---|---|---|
7a35f4ac3e | |||
1f009db27d | |||
418d23fdc5 | |||
1a1f199ad1 | |||
39b56c390f | |||
28a36d80cd | |||
6c9d912df3 | |||
82474262b8 | |||
ade02bfe22 | |||
243df232c3 |
4 changed files with 84 additions and 53 deletions
|
@ -1,11 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "find-images"
|
name = "find-images"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
shlex = "1.2"
|
shlex = "1.3"
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
|
|
|
@ -3,4 +3,4 @@ Find images (sort by last modified by default).
|
||||||
Useful for piping into sxiv/nsxiv when hidden subfiles/directories are significant.
|
Useful for piping into sxiv/nsxiv when hidden subfiles/directories are significant.
|
||||||
|
|
||||||
## Find non-images
|
## Find non-images
|
||||||
obtain non-image files by giving any extension to `--extensions`
|
Obtain non-image files by giving any extensions to `--extensions`
|
||||||
|
|
Binary file not shown.
101
src/main.rs
101
src/main.rs
|
@ -5,6 +5,9 @@ use crate::structopt::StructOpt;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path,PathBuf};
|
use std::path::{Path,PathBuf};
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
#[structopt(about="Recursively get images")]
|
#[structopt(about="Recursively get images")]
|
||||||
|
@ -26,7 +29,7 @@ struct CLIArguments {
|
||||||
quote: bool,
|
quote: bool,
|
||||||
|
|
||||||
#[structopt(short, long, value_name="EXT",
|
#[structopt(short, long, value_name="EXT",
|
||||||
help="File extensions to filter for (default: jpg jpeg png webp gif heic tiff dpx exr svg)")]
|
help="File extensions to filter for (default: dpx exr gif heic jpeg jpg png svg tiff webp)")]
|
||||||
extensions: Vec<String>,
|
extensions: Vec<String>,
|
||||||
|
|
||||||
#[structopt(value_name="TARGET",
|
#[structopt(value_name="TARGET",
|
||||||
|
@ -34,6 +37,8 @@ struct CLIArguments {
|
||||||
targets: Vec<String>,
|
targets: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>>{
|
fn main() -> Result<(), Box<dyn std::error::Error>>{
|
||||||
let args = {
|
let args = {
|
||||||
let mut args = CLIArguments::from_args();
|
let mut args = CLIArguments::from_args();
|
||||||
|
@ -44,64 +49,89 @@ fn main() -> Result<(), Box<dyn std::error::Error>>{
|
||||||
};
|
};
|
||||||
|
|
||||||
let valid_extensions: HashSet<&str> = if args.extensions.is_empty() {
|
let valid_extensions: HashSet<&str> = if args.extensions.is_empty() {
|
||||||
["jpg", "jpeg", "png", "webp", "gif", "heic", "tiff", "dpx", "exr", "svg"].into_iter().collect()
|
["dpx", "exr", "gif", "heic", "jpeg", "jpg", "png", "svg", "tiff", "webp"].into_iter().collect()
|
||||||
} else {
|
} else {
|
||||||
args.extensions.iter().map(|s| s.as_str()).collect()
|
args.extensions.iter().map(|s| s.as_str()).collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut registry: Vec<PathBuf> = Vec::new();
|
let mut registry = Registry::new(valid_extensions);
|
||||||
|
|
||||||
args.targets.into_iter().map(|target| Path::new(&target).to_path_buf() ).for_each(
|
registry.populate(args.targets.into_iter().map(|target| Path::new(&target).to_path_buf() ), args.dohidden);
|
||||||
|path| if path.is_file() {
|
|
||||||
register_file_if_image(&mut registry, path, &valid_extensions);
|
|
||||||
} else if path.is_dir() {
|
|
||||||
register_dir(&mut registry, path, &valid_extensions, args.dohidden);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if !args.no_sort {
|
if !args.no_sort {
|
||||||
registry.sort_by_key(|entry| {
|
registry.sort_by_modified();
|
||||||
entry.metadata().ok().and_then(|meta| meta.modified().ok()).unwrap_or_else(
|
}
|
||||||
|
|
||||||
|
let stdout = std::io::stdout();
|
||||||
|
let mut stdout_buffer = std::io::BufWriter::new(stdout.lock());
|
||||||
|
registry.write_all(&mut stdout_buffer, args.null, args.quote)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct Registry<'a> {
|
||||||
|
registry: Vec<(std::fs::Metadata, PathBuf)>,
|
||||||
|
valid_extensions: HashSet<&'a str>
|
||||||
|
}
|
||||||
|
impl<'a> Registry<'a> {
|
||||||
|
pub fn new(valid_extensions: HashSet<&'a str>) -> Self {
|
||||||
|
Self { registry: Vec::new(), valid_extensions }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_all(&self, writer: &mut impl Write, separator_null: bool, quote: bool) -> std::io::Result<()> {
|
||||||
|
let sep = if separator_null { '\0' } else { '\n' };
|
||||||
|
for (_, file) in &self.registry {
|
||||||
|
if quote {
|
||||||
|
writer.write_all(&shlex::bytes::try_quote(&file.as_os_str().as_bytes()).unwrap())?;
|
||||||
|
} else {
|
||||||
|
writer.write_all(file.as_os_str().as_bytes())?;
|
||||||
|
}
|
||||||
|
write!(writer, "{}", sep)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_by_modified(&mut self) {
|
||||||
|
self.registry.sort_by_key(|(meta,_)| {
|
||||||
|
meta.modified().ok().unwrap_or_else(
|
||||||
|| std::time::SystemTime::UNIX_EPOCH,
|
|| std::time::SystemTime::UNIX_EPOCH,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let stdout = std::io::stdout();
|
pub fn populate(&mut self, source_paths: impl Iterator<Item=PathBuf>, dohidden: bool) {
|
||||||
let mut stdout_buffer = std::io::BufWriter::new(stdout.lock());
|
for path in source_paths {
|
||||||
|
if path.is_file() {
|
||||||
if args.null {
|
if let Ok(metadata) = std::fs::metadata(&path) { // intentionally not symlink_metadata
|
||||||
if args.quote { for file in registry { write!(stdout_buffer, "{}\0", shlex::quote(&file.to_string_lossy()))?; } }
|
self.add_file(path, metadata);
|
||||||
else { for file in registry { write!(stdout_buffer, "{}\0", &file.to_string_lossy() )?; } }
|
}
|
||||||
} else {
|
} else if path.is_dir() {
|
||||||
if args.quote { for file in registry { writeln!(stdout_buffer, "{}", shlex::quote(&file.to_string_lossy()))?; } }
|
self.add_dir(path, dohidden);
|
||||||
else { for file in registry { writeln!(stdout_buffer, "{}", &file.to_string_lossy() )?; } }
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
fn add_file(&mut self, path: PathBuf, metadata: std::fs::Metadata) {
|
||||||
}
|
|
||||||
|
|
||||||
fn register_file_if_image(registry: &mut Vec<PathBuf>, path: PathBuf, valid_extensions: &HashSet<&str>) {
|
|
||||||
if let Some(osstr_ext) = path.extension() {
|
if let Some(osstr_ext) = path.extension() {
|
||||||
match osstr_ext.to_str() {
|
match osstr_ext.to_str() {
|
||||||
Some(ext) => {
|
Some(ext) => if self.valid_extensions.contains(ext) {
|
||||||
if valid_extensions.contains(ext) {
|
self.registry.push((metadata, path));
|
||||||
registry.push(path);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
None => eprintln!(
|
None => eprintln!(
|
||||||
"Cannot read non-utf-8 file extension: {} on {}",
|
"Cannot read non-utf-8 file extension: {} on {}",
|
||||||
shlex::quote(&osstr_ext.to_string_lossy()),
|
shlex::try_quote(&osstr_ext.to_string_lossy()).unwrap(),
|
||||||
shlex::quote(&path.to_string_lossy())
|
shlex::try_quote(&path.to_string_lossy()).unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_dir(registry: &mut Vec<PathBuf>, path: PathBuf, valid_extensions: &HashSet<&str>, dohidden: bool) {
|
fn add_dir(&mut self, path: PathBuf, dohidden: bool) {
|
||||||
if let Ok(entries) = std::fs::read_dir(path) {
|
if let Ok(entries) = std::fs::read_dir(path) {
|
||||||
for path in entries.filter_map(|e| e.ok() ).map(|e| e.path() ) {
|
for path in entries.filter_map(|e| e.ok() ).map(|e| e.path() ) {
|
||||||
if !dohidden && path.file_name().map(|name| name.to_string_lossy().starts_with('.')).unwrap_or(false) {
|
if !dohidden && path.file_name().map(|name| name.to_string_lossy().starts_with('.')).unwrap_or(true) { // this unwraps to None if the file_name is .. or is root / (neither of which would happen in this scenario)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let Ok(metadata) = std::fs::symlink_metadata(&path) {
|
if let Ok(metadata) = std::fs::symlink_metadata(&path) {
|
||||||
|
@ -109,9 +139,10 @@ fn register_dir(registry: &mut Vec<PathBuf>, path: PathBuf, valid_extensions: &H
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
register_file_if_image(registry, path, valid_extensions);
|
self.add_file(path, metadata)
|
||||||
} else if path.is_dir() {
|
} else if path.is_dir() {
|
||||||
register_dir(registry, path, valid_extensions, dohidden);
|
self.add_dir(path, dohidden);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue