This commit is contained in:
sxyazi 2023-08-13 22:18:08 +08:00
parent 0a4cb798bf
commit 08590c6d9e
No known key found for this signature in database
9 changed files with 121 additions and 194 deletions

37
Cargo.lock generated
View file

@ -1315,12 +1315,6 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.66"
@ -1357,36 +1351,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ratatui"
version = "0.22.0"
@ -1565,7 +1529,6 @@ dependencies = [
"crossterm 0.27.0",
"libc",
"parking_lot",
"rand",
"ratatui",
"tokio",
]

View file

@ -1,46 +0,0 @@
use std::{env, ffi::{OsStr, OsString}, path::Path, str::FromStr};
use anyhow::{Context, Result};
use once_cell::sync::OnceCell;
use tokio::process::Command;
#[derive(Debug)]
struct Editor {
name: String,
options: Vec<String>,
}
static EDITOR: OnceCell<Editor> = OnceCell::new();
fn try_init() -> Result<&'static Editor> {
EDITOR.get_or_try_init(|| {
env::var_os("EDITOR")
.context("environment variable `EDITOR` is undefined")?
.to_string_lossy()
.parse()
})
}
pub async fn edit(file: impl AsRef<Path>) -> Result<()> {
let editor = try_init()?;
// TODO: 编辑并返回是否成功
// let output = Command::new(&editor.name)
// .args(&editor.options)
// .arg(file.as_ref())
// .kill_on_drop(true)
// .output()
// .await?;
todo!()
}
impl FromStr for Editor {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut args = s.split(' ');
Ok(Self {
name: args.next().context("environment variable `EDITOR` is empty")?.to_owned(),
options: args.map(str::to_owned).collect(),
})
}
}

View file

@ -1,11 +1,10 @@
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::{Path, PathBuf}, process::exit};
use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, ffi::OsStr, io::{stdout, Write}, mem, os::unix::prelude::OsStrExt, path::{Path, PathBuf}};
use anyhow::Error;
use config::{open::Opener, OPEN};
use indexmap::IndexSet;
use shared::{temp_path, MIME_DIR};
use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}};
use tracing::{debug, error};
use anyhow::{bail, Error, Result};
use config::{open::Opener, BOOT, OPEN};
use crossterm::{execute, terminal::{Clear, ClearType}};
use shared::{in_same_root, Defer, Term, MIME_DIR};
use tokio::{fs::{self, OpenOptions}, io::{AsyncReadExt, AsyncWriteExt}};
use super::{PreviewData, Tab, Tabs, Watcher};
use crate::{emit, external, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks};
@ -225,56 +224,103 @@ impl Manager {
}
pub fn bulk_rename(&self) -> bool {
let selected: Vec<_> = self.selected().iter().map(|&f| f.path.clone()).collect();
let mut old: Vec<_> = self.selected().iter().map(|&f| f.path()).collect();
if old.is_empty() {
return false;
}
let root = in_same_root(&old);
if let Some(ref root) = root {
old = old.into_iter().map(|p| p.strip_prefix(root).unwrap().to_owned()).collect();
}
let tmp = BOOT.tmpfile();
tokio::spawn(async move {
let files: Vec<_> =
selected.iter().map(|p| p.file_name().unwrap().to_string_lossy()).collect();
let rename_file_path = temp_path(Some("txt"));
let Some(opener) = OPEN.block_opener("bulk-rename.txt", "text/plain") else {
bail!("No opener for bulk rename");
};
{
let mut rename_file =
match OpenOptions::new().write(true).create_new(true).open(&rename_file_path).await {
Ok(f) => BufWriter::new(f),
Err(e) => {
error!("failed to open rename buffer: {e}");
return;
}
};
if let Err(e) = rename_file.write_all(files.join("\n").as_bytes()).await {
error!("failed to write content to rename buffer: {e}");
return;
}
let _ = rename_file.flush().await;
let b = old.iter().map(|o| o.as_os_str()).collect::<Vec<_>>().join(OsStr::new("\n"));
let mut f = OpenOptions::new().write(true).create_new(true).open(&tmp).await?;
f.write_all(b.as_bytes()).await?;
}
emit!(Open(vec![(rename_file_path.as_os_str().to_owned(), "text/plain".to_owned())], None));
let _guard = BLOCKER.acquire().await.unwrap();
let _defer = Defer::new(|| Event::Stop(false, None).emit());
emit!(Stop(true)).await;
let mut buf = String::new();
{
let mut rename_file = match tokio::fs::File::open(&rename_file_path).await {
Ok(f) => BufReader::new(f),
Err(e) => {
error!("failed to read rename buffer: {e}");
return;
}
};
let _ = rename_file.read_to_string(&mut buf).await;
}
let new_names = match parse_new_names(&buf, selected.len()) {
Ok(names) => names,
Err(e) => {
println!("yazi: {e}");
return;
}
};
println!("new names: {new_names:?}");
let mut child = external::shell(ShellOpt {
cmd: (*opener.exec).into(),
args: vec![tmp.to_owned().into()],
piped: false,
})?;
child.wait().await?;
let new: Vec<_> = fs::read_to_string(tmp).await?.lines().map(|l| l.into()).collect();
Self::bulk_rename_do(root, old, new).await
});
false
}
async fn bulk_rename_do(
root: Option<PathBuf>,
old: Vec<PathBuf>,
new: Vec<PathBuf>,
) -> Result<()> {
Term::clear()?;
if old.len() != new.len() {
println!("Number of old and new differ, press ENTER to exit");
tokio::io::stdin().read_exact(&mut [0]).await?;
return Ok(());
}
let mut todo = Vec::with_capacity(old.len());
for (o, n) in old.into_iter().zip(new) {
if n != o {
stdout().write_all(o.as_os_str().as_bytes())?;
stdout().write_all(b" -> ")?;
stdout().write_all(n.as_os_str().as_bytes())?;
stdout().write_all(b"\n")?;
todo.push(if let Some(ref root) = root { (root.join(o), root.join(n)) } else { (o, n) });
}
}
if todo.is_empty() {
return Ok(());
} else {
print!("Continue to rename? (y/N): ");
stdout().flush()?;
}
let mut buf = [0];
tokio::io::stdin().read_exact(&mut buf).await?;
if buf[0] != b'y' && buf[0] != b'Y' {
return Ok(());
}
let mut failed = Vec::new();
for (o, n) in todo {
if let Err(e) = fs::rename(&o, &n).await {
failed.push((o, n, e));
}
}
if !failed.is_empty() {
Term::clear()?;
println!("Failed to rename:");
for (o, n, e) in failed {
stdout().write_all(o.as_os_str().as_bytes())?;
stdout().write_all(b" -> ")?;
stdout().write_all(n.as_os_str().as_bytes())?;
stdout().write_fmt(format_args!(": {e}\n"))?;
}
println!("\nPress ENTER to exit");
tokio::io::stdin().read_exact(&mut [0]).await?;
}
Ok(())
}
pub fn shell(&self, exec: &str, block: bool, confirm: bool) -> bool {
let mut exec = exec.to_owned();
tokio::spawn(async move {
@ -437,19 +483,3 @@ impl Manager {
self.active().mode.is_visual() || self.current().has_selected()
}
}
fn parse_new_names(text: &str, count: usize) -> anyhow::Result<IndexSet<&str>> {
// NOTE: call `size_hint` on `str::Split` always returns 0
let new_names: Vec<_> = text.split('\n').collect();
if new_names.len() != count {
anyhow::bail!("the number of new names doesn't match the number of old names");
}
let mut names = IndexSet::with_capacity(count);
for name in new_names {
if !names.insert(name) {
anyhow::bail!("there are more than one new entries named {name:?}");
}
}
Ok(names)
}

View file

@ -82,7 +82,7 @@ impl Tasks {
emit!(Stop(true)).await;
let _defer = Defer::new(|| {
disable_raw_mode().ok();
Event::Stop(false, None).emit()
Event::Stop(false, None).emit();
});
stdout().write_all("\n".repeat(tty_size().ws_row as usize).as_bytes()).ok();

View file

@ -1,13 +1,13 @@
pub struct Defer<F: FnOnce()>(Option<F>);
pub struct Defer<F: FnOnce() -> T, T>(Option<F>);
impl<F: FnOnce()> Defer<F> {
impl<F: FnOnce() -> T, T> Defer<F, T> {
pub fn new(f: F) -> Self { Defer(Some(f)) }
}
impl<F: FnOnce()> Drop for Defer<F> {
impl<F: FnOnce() -> T, T> Drop for Defer<F, T> {
fn drop(&mut self) {
if let Some(f) = self.0.take() {
f();
let _ = f();
}
}
}

View file

@ -1,4 +1,4 @@
use std::{collections::VecDeque, path::Path};
use std::{collections::VecDeque, path::{Path, PathBuf}};
use anyhow::Result;
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
@ -149,3 +149,14 @@ pub fn file_mode(mode: u32) -> String {
s
}
// Note: files must contain at least one file path
pub fn in_same_root(files: &[PathBuf]) -> Option<PathBuf> {
if files.is_empty() {
return None;
}
let mut files = files.iter();
let parent = files.next().map(|f| f.parent().unwrap_or(f).to_path_buf()).unwrap();
if files.all(|f| f.parent().unwrap_or(f) == parent) { Some(parent) } else { None }
}

View file

@ -4,7 +4,6 @@ mod defer;
mod fns;
mod fs;
mod mime;
mod temp_path;
mod term;
mod throttle;
mod tty;
@ -15,7 +14,6 @@ pub use defer::*;
pub use fns::*;
pub use fs::*;
pub use mime::*;
pub use temp_path::*;
pub use term::*;
pub use throttle::*;
pub use tty::*;

View file

@ -1,36 +0,0 @@
use std::path::PathBuf;
use rand::{distributions::Alphanumeric, Rng};
const FILE_PREFIX: &str = "yazi-";
const ID_LEN: usize = 10;
/// generate a path under the system temporary directory
///
/// `ext`: an ASCII string
pub fn temp_path(ext: Option<&str>) -> PathBuf {
let (ext, suffix_len) = ext.map(|ext| (ext, ext.len())).unwrap_or_default();
let mut name = String::with_capacity(FILE_PREFIX.len() + ID_LEN + suffix_len);
name.push_str(FILE_PREFIX);
rand::thread_rng().sample_iter(&Alphanumeric).take(ID_LEN).for_each(|c| name.push(c as char));
if !ext.is_empty() {
name.push('.');
name.push_str(ext);
}
let tmp = std::env::temp_dir();
tmp.join(&name)
}
#[cfg(test)]
mod tests {
use super::temp_path;
#[test]
fn test_temp_path() {
let p = temp_path(Some("txt"));
println!("{p:?}");
}
}

View file

@ -1,7 +1,7 @@
use std::{io::{stdout, Stdout}, ops::{Deref, DerefMut}};
use std::{io::{stdout, Stdout, Write}, ops::{Deref, DerefMut}};
use anyhow::Result;
use crossterm::{cursor::{MoveTo, SetCursorStyle}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, EnterAlternateScreen, LeaveAlternateScreen}};
use crossterm::{cursor::{MoveTo, SetCursorStyle}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}};
use ratatui::{backend::CrosstermBackend, Terminal};
pub struct Term {
@ -32,6 +32,13 @@ impl Term {
Ok(term)
}
pub fn clear() -> Result<()> {
execute!(stdout(), Clear(ClearType::All))?;
println!();
stdout().flush()?;
Ok(())
}
pub fn move_to(x: u16, y: u16) -> Result<()> { Ok(execute!(stdout(), MoveTo(x, y))?) }
pub fn set_cursor_block() -> Result<()> { Ok(execute!(stdout(), SetCursorStyle::BlinkingBlock)?) }