feat: new reveal command (#341)

This commit is contained in:
三咲雅 · Misaki Masa 2023-11-09 09:07:12 +08:00 committed by GitHub
parent a0ba853718
commit 1bbb323509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 449 additions and 224 deletions

3
Cargo.lock generated
View File

@ -2487,6 +2487,7 @@ version = "0.1.5"
dependencies = [
"anyhow",
"async-channel",
"bitflags 2.4.1",
"clipboard-win",
"crossterm",
"futures",
@ -2559,10 +2560,12 @@ name = "yazi-shared"
version = "0.1.5"
dependencies = [
"anyhow",
"bitflags 2.4.1",
"crossterm",
"futures",
"libc",
"parking_lot",
"percent-encoding",
"ratatui",
"regex",
"tokio",

View File

@ -1 +1 @@
{"language":"en","version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ"]}
{"version":"0.2","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags"],"language":"en","flagWords":[]}

View File

@ -16,6 +16,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.1.5" }
# External dependencies
anyhow = "^1"
async-channel = "^1"
bitflags = "^2"
crossterm = "^0"
futures = "^0"
indexmap = "^2"

View File

@ -21,7 +21,6 @@ pub enum Event {
Call(Vec<Exec>, KeymapLayer),
// Manager
Cd(Url),
Refresh,
Files(FilesOp),
Pages(usize),
@ -74,9 +73,6 @@ macro_rules! emit {
$crate::Event::Call($exec, $layer).emit();
};
(Cd($url:expr)) => {
$crate::Event::Cd($url).emit();
};
(Files($op:expr)) => {
$crate::Event::Files($op).emit();
};

View File

@ -1,27 +1,22 @@
use std::process::Stdio;
use std::{path::Path, process::Stdio};
use anyhow::Result;
use tokio::{process::Command, sync::oneshot::{self, Receiver}};
use anyhow::{bail, Result};
use tokio::process::Command;
use yazi_shared::Url;
pub struct FzfOpt {
pub cwd: Url,
}
pub fn fzf(opt: FzfOpt) -> Result<Receiver<Result<Url>>> {
pub async fn fzf(opt: FzfOpt) -> Result<Url> {
let child =
Command::new("fzf").current_dir(&opt.cwd).kill_on_drop(true).stdout(Stdio::piped()).spawn()?;
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
if let Ok(output) = child.wait_with_output().await {
let output = child.wait_with_output().await?;
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
tx.send(Ok(opt.cwd.join(selected))).ok();
return;
if selected.is_empty() {
bail!("No match")
}
}
tx.send(Err(anyhow::anyhow!("No match"))).ok();
});
Ok(rx)
return Ok(Url::from(Path::new(&opt.cwd).join(selected)));
}

View File

@ -1,14 +1,14 @@
use std::process::Stdio;
use anyhow::Result;
use tokio::{process::Command, sync::oneshot::{self, Receiver}};
use anyhow::{bail, Result};
use tokio::process::Command;
use yazi_shared::Url;
pub struct ZoxideOpt {
pub cwd: Url,
}
pub fn zoxide(opt: ZoxideOpt) -> Result<Receiver<Result<Url>>> {
pub async fn zoxide(opt: ZoxideOpt) -> Result<Url> {
let child = Command::new("zoxide")
.args(["query", "-i", "--exclude"])
.arg(&opt.cwd)
@ -16,16 +16,11 @@ pub fn zoxide(opt: ZoxideOpt) -> Result<Receiver<Result<Url>>> {
.stdout(Stdio::piped())
.spawn()?;
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
if let Ok(output) = child.wait_with_output().await {
let output = child.wait_with_output().await?;
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
tx.send(Ok(Url::from(selected))).ok();
return;
return Ok(Url::from(selected));
}
}
tx.send(Err(anyhow::anyhow!("No match"))).ok();
});
Ok(rx)
bail!("No match")
}

View File

@ -1,36 +1,55 @@
use std::{borrow::Cow, collections::BTreeMap, ffi::OsStr, fs::Metadata};
use std::{borrow::Cow, collections::BTreeMap, ffi::OsStr, fs::Metadata, ops::Deref};
use anyhow::Result;
use tokio::fs;
use yazi_shared::Url;
use yazi_shared::{Cha, ChaMeta, Url};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct File {
pub url: Url,
pub meta: Metadata,
pub cha: Cha,
pub(super) link_to: Option<Url>,
pub is_link: bool,
pub is_hidden: bool,
}
impl Deref for File {
type Target = Cha;
#[inline]
fn deref(&self) -> &Self::Target { &self.cha }
}
impl File {
#[inline]
pub async fn from(url: Url) -> Result<Self> {
let meta = fs::metadata(&url).await?;
let meta = fs::symlink_metadata(&url).await?;
Ok(Self::from_meta(url, meta).await)
}
pub async fn from_meta(url: Url, mut meta: Metadata) -> Self {
let is_link = meta.is_symlink();
let mut link_to = None;
let mut cm = ChaMeta::empty();
let (is_link, mut link_to) = (meta.is_symlink(), None);
if is_link {
cm |= ChaMeta::LINK;
meta = fs::metadata(&url).await.unwrap_or(meta);
link_to = fs::read_link(&url).await.map(Url::from).ok();
}
let is_hidden = url.file_name().map(|s| s.to_string_lossy().starts_with('.')).unwrap_or(false);
Self { url, meta, link_to, is_link, is_hidden }
if is_link && meta.is_symlink() {
cm |= ChaMeta::BAD_LINK;
}
if url.is_hidden() {
cm |= ChaMeta::HIDDEN;
}
Self { url, cha: Cha::from(meta).with_meta(cm), link_to }
}
#[inline]
pub fn from_dummy(url: Url) -> Self {
let cm = if url.is_hidden() { ChaMeta::HIDDEN } else { ChaMeta::empty() };
Self { url, cha: Cha::default().with_meta(cm), link_to: None }
}
#[inline]
@ -60,13 +79,6 @@ impl File {
#[inline]
pub fn parent(&self) -> Option<Url> { self.url.parent_url() }
// --- Meta
#[inline]
pub fn is_file(&self) -> bool { self.meta.is_file() }
#[inline]
pub fn is_dir(&self) -> bool { self.meta.is_dir() }
// --- Link to / Is link
#[inline]
pub fn link_to(&self) -> Option<&Url> { self.link_to.as_ref() }

View File

@ -128,7 +128,7 @@ impl Files {
pub fn update_full(&mut self, mut items: Vec<File>) -> bool {
if !self.show_hidden {
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden);
(self.hidden, items) = items.into_iter().partition(|f| f.is_hidden());
}
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
self.sorter.sort(&mut items, &self.sizes);
@ -146,7 +146,7 @@ impl Files {
if self.show_hidden {
self.items.extend(items);
} else {
let (hidden, items): (Vec<_>, Vec<_>) = items.into_iter().partition(|f| f.is_hidden);
let (hidden, items): (Vec<_>, Vec<_>) = items.into_iter().partition(|f| f.is_hidden());
self.items.extend(items);
self.hidden.extend(hidden);
}
@ -178,7 +178,7 @@ impl Files {
pub fn update_creating(&mut self, mut todo: BTreeMap<Url, File>) -> bool {
if !self.show_hidden {
todo.retain(|_, f| !f.is_hidden);
todo.retain(|_, f| !f.is_hidden());
}
let b = self.update_replacing(&mut todo);
@ -335,7 +335,7 @@ impl Files {
self.sorter.sort(&mut self.items, &self.sizes);
} else {
let items = mem::take(&mut self.items);
(self.hidden, self.items) = items.into_iter().partition(|f| f.is_hidden);
(self.hidden, self.items) = items.into_iter().partition(|f| f.is_hidden());
}
self.show_hidden = state;

View File

@ -33,13 +33,13 @@ impl FilesSorter {
)
}),
SortBy::Created => items.sort_unstable_by(|a, b| {
if let (Ok(aa), Ok(bb)) = (a.meta.created(), b.meta.created()) {
if let (Some(aa), Some(bb)) = (a.created, b.created) {
return self.cmp(aa, bb, self.promote(a, b));
}
Ordering::Equal
}),
SortBy::Modified => items.sort_unstable_by(|a, b| {
if let (Ok(aa), Ok(bb)) = (a.meta.modified(), b.meta.modified()) {
if let (Some(aa), Some(bb)) = (a.modified, b.modified) {
return self.cmp(aa, bb, self.promote(a, b));
}
Ordering::Equal
@ -48,7 +48,7 @@ impl FilesSorter {
SortBy::Size => items.sort_unstable_by(|a, b| {
let aa = if a.is_dir() { sizes.get(&a.url).copied() } else { None };
let bb = if b.is_dir() { sizes.get(&b.url).copied() } else { None };
self.cmp(aa.unwrap_or(a.meta.len()), bb.unwrap_or(b.meta.len()), self.promote(a, b))
self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len), self.promote(a, b))
}),
}
true
@ -72,17 +72,9 @@ impl FilesSorter {
if self.reverse { ordering.reverse() } else { ordering }
});
let dummy = File {
url: Default::default(),
meta: items[0].meta.clone(),
link_to: None,
is_link: false,
is_hidden: false,
};
let mut new = Vec::with_capacity(indices.len());
for i in indices {
new.push(mem::replace(&mut items[i], dummy.clone()));
new.push(mem::take(&mut items[i]));
}
*items = new;
}

View File

@ -62,7 +62,7 @@ impl Watcher {
);
let instance = Self { watcher: watcher.unwrap(), watched: Default::default() };
tokio::spawn(Self::changed(rx, instance.watched.clone()));
tokio::spawn(Self::on_changed(rx, instance.watched.clone()));
instance
}
@ -124,13 +124,17 @@ impl Watcher {
let watched = self.watched.clone();
tokio::spawn(async move {
let watched = watched.read().clone();
for dir in dirs {
Self::dir_changed(&dir, watched.clone()).await;
Self::dir_changed(&dir, &watched).await;
}
});
}
async fn changed(rx: UnboundedReceiver<Url>, watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>) {
async fn on_changed(
rx: UnboundedReceiver<Url>,
watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>,
) {
// TODO: revert this once a new notification is implemented
// let rx = UnboundedReceiverStream::new(rx).chunks_timeout(100,
// Duration::from_millis(200));
@ -147,70 +151,76 @@ impl Watcher {
}
}
Self::file_changed(&files, watched.clone()).await;
let watched = watched.read().clone();
Self::files_changed(&files, &watched).await;
for file in files {
for u in Self::linked_urls(&file, &watched) {
emit!(Files(FilesOp::IOErr(u.clone())));
}
emit!(Files(FilesOp::IOErr(file)));
}
for dir in dirs {
Self::dir_changed(&dir, watched.clone()).await;
Self::dir_changed(&dir, &watched).await;
}
}
}
async fn file_changed(urls: &[Url], watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>) {
async fn files_changed(urls: &[Url], watched: &IndexMap<Url, Option<Url>>) {
let Ok(mut mimes) = external::file(urls).await else {
return;
};
let linked: Vec<_> = watched
.read()
.iter()
.filter_map(|(k, v)| v.as_ref().map(|v| (k, v)))
.fold(Vec::new(), |mut aac, (k, v)| {
let linked: Vec<_> = watched.iter().filter_map(|(k, v)| v.as_ref().map(|v| (k, v))).fold(
Vec::new(),
|mut aac, (k, v)| {
mimes
.iter()
.filter(|(f, _)| f.parent().map(|p| p == **v) == Some(true))
.for_each(|(f, m)| aac.push((k.join(f.file_name().unwrap()), m.clone())));
.filter(|(u, _)| u.parent().map(|p| p == **v) == Some(true))
.for_each(|(u, m)| aac.push((k.join(u.file_name().unwrap()), m.clone())));
aac
});
},
);
mimes.extend(linked);
emit!(Mimetype(mimes));
}
async fn dir_changed(url: &Url, watched: Arc<RwLock<IndexMap<Url, Option<Url>>>>) {
let linked: Vec<_> = watched
.read()
.iter()
.filter_map(|(k, v)| v.as_ref().map(|v| (k, v)))
.filter(|(_, v)| *v == url)
.map(|(k, _)| k.clone())
.collect();
async fn dir_changed(url: &Url, watched: &IndexMap<Url, Option<Url>>) {
let linked = Self::linked_urls(url, watched);
let Ok(rx) = Files::from_dir(url).await else {
emit!(Files(FilesOp::IOErr(url.clone())));
for ori in linked {
emit!(Files(FilesOp::IOErr(ori)));
for u in linked {
emit!(Files(FilesOp::IOErr(u.clone())));
}
return;
};
let linked_files = |files: &[File], ori: &Url| -> Vec<File> {
let linked_files = |files: &[File], linked: &Url| -> Vec<File> {
let mut new = Vec::with_capacity(files.len());
for file in files {
let mut file = file.clone();
file.url = ori.join(file.url.strip_prefix(url).unwrap());
file.url = linked.join(file.url.strip_prefix(url).unwrap());
new.push(file);
}
new
};
let files: Vec<_> = UnboundedReceiverStream::new(rx).collect().await;
for ori in linked {
let files = linked_files(&files, &ori);
emit!(Files(FilesOp::Full(ori, files)));
for u in linked {
let files = linked_files(&files, u);
emit!(Files(FilesOp::Full(u.clone(), files)));
}
emit!(Files(FilesOp::Full(url.clone(), files)));
}
fn linked_urls<'a>(url: &'a Url, watched: &'a IndexMap<Url, Option<Url>>) -> Vec<&'a Url> {
watched
.iter()
.filter_map(|(k, v)| v.as_ref().map(|v| (k, v)))
.filter(|(_, v)| *v == url)
.map(|(k, _)| k)
.collect()
}
}

View File

@ -3,14 +3,14 @@ use crate::tab::Tab;
impl Tab {
pub fn back(&mut self) -> bool {
if let Some(url) = self.backstack.shift_backward().cloned() {
futures::executor::block_on(self.cd(url));
self.cd(url);
}
false
}
pub fn forward(&mut self) -> bool {
if let Some(url) = self.backstack.shift_forward().cloned() {
futures::executor::block_on(self.cd(url));
self.cd(url);
}
false
}

View File

@ -1,31 +1,15 @@
use std::{mem, time::Duration};
use tokio::pin;
use tokio::{fs, pin};
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use yazi_config::keymap::{Exec, KeymapLayer};
use yazi_shared::{Debounce, InputError, Url};
use yazi_shared::{expand_path, Debounce, InputError, Url};
use crate::{emit, files::{File, FilesOp}, input::InputOpt, tab::Tab};
use crate::{emit, input::InputOpt, tab::Tab};
impl Tab {
// TODO: change to sync, and remove `Event::Cd`
pub async fn cd(&mut self, mut target: Url) -> bool {
let Ok(file) = File::from(target.clone()).await else {
return false;
};
let mut hovered = None;
if !file.is_dir() {
hovered = Some(file.url());
target = target.parent_url().unwrap();
emit!(Files(FilesOp::Creating(target.clone(), file.into_map())));
}
// Already in target
pub fn cd(&mut self, target: Url) -> bool {
if self.current.cwd == target {
if let Some(h) = hovered {
emit!(Hover(h));
}
return false;
}
@ -46,11 +30,6 @@ impl Tab {
self.parent = Some(self.history_new(&parent));
}
// Hover the file
if let Some(h) = hovered {
emit!(Hover(h));
}
// Backstack
if target.is_regular() {
self.backstack.push(target.clone());
@ -72,7 +51,18 @@ impl Tab {
while let Some(result) = rx.next().await {
match result {
Ok(s) => {
emit!(Cd(Url::from(s.trim())));
let p = expand_path(s);
let Ok(meta) = fs::metadata(&p).await else {
return;
};
emit!(Call(
Exec::call(if meta.is_dir() { "cd" } else { "reveal" }, vec![
p.to_string_lossy().to_string()
])
.vec(),
KeymapLayer::Manager
));
}
Err(InputError::Completed(before, ticket)) => {
emit!(Call(

View File

@ -1,19 +1,27 @@
use bitflags::bitflags;
use yazi_config::keymap::Exec;
use crate::tab::{Mode, Tab};
pub struct Opt(u8);
bitflags! {
pub struct Opt: u8 {
const FIND = 0b0001;
const VISUAL = 0b0010;
const SELECT = 0b0100;
const SEARCH = 0b1000;
}
}
impl From<&Exec> for Opt {
fn from(e: &Exec) -> Self {
Self(e.named.iter().fold(0, |acc, (k, _)| match k.as_str() {
"all" => 0b1111,
"find" => acc | 0b0001,
"visual" => acc | 0b0010,
"select" => acc | 0b0100,
"search" => acc | 0b1000,
e.named.iter().fold(Opt::empty(), |acc, (k, _)| match k.as_bytes() {
b"all" => Self::all(),
b"find" => acc | Self::FIND,
b"visual" => acc | Self::VISUAL,
b"select" => acc | Self::SELECT,
b"search" => acc | Self::SEARCH,
_ => acc,
}))
})
}
}
@ -38,8 +46,8 @@ impl Tab {
fn escape_search(&mut self) -> bool { self.search_stop() }
pub fn escape(&mut self, opt: impl Into<Opt>) -> bool {
let opt = opt.into().0;
if opt == 0 {
let opt = opt.into() as Opt;
if opt.is_empty() {
return self.escape_find()
|| self.escape_visual()
|| self.escape_select()
@ -47,16 +55,16 @@ impl Tab {
}
let mut b = false;
if opt & 0b0001 != 0 {
if opt.contains(Opt::FIND) {
b |= self.escape_find();
}
if opt & 0b0010 != 0 {
if opt.contains(Opt::VISUAL) {
b |= self.escape_visual();
}
if opt & 0b0100 != 0 {
if opt.contains(Opt::SELECT) {
b |= self.escape_select();
}
if opt & 0b1000 != 0 {
if opt.contains(Opt::SEARCH) {
b |= self.escape_search();
}
b

View File

@ -1,4 +1,5 @@
use yazi_shared::Defer;
use yazi_config::keymap::{Exec, KeymapLayer};
use yazi_shared::{ends_with_slash, Defer};
use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, tab::Tab, Event, BLOCKER};
@ -11,12 +12,14 @@ impl Tab {
let _defer = Defer::new(|| Event::Stop(false, None).emit());
emit!(Stop(true)).await;
let rx =
if global { external::fzf(FzfOpt { cwd }) } else { external::zoxide(ZoxideOpt { cwd }) }?;
let url = if global {
external::fzf(FzfOpt { cwd }).await
} else {
external::zoxide(ZoxideOpt { cwd }).await
}?;
if let Ok(target) = rx.await? {
emit!(Cd(target));
}
let op = if global && !ends_with_slash(&url) { "reveal" } else { "cd" };
emit!(Call(Exec::call(op, vec![url.to_string()]).vec(), KeymapLayer::Manager));
Ok::<(), anyhow::Error>(())
});
false

View File

@ -9,6 +9,7 @@ mod hidden;
mod jump;
mod leave;
mod linemode;
mod reveal;
mod search;
mod select;
mod shell;

View File

@ -0,0 +1,32 @@
use yazi_config::keymap::Exec;
use yazi_shared::{expand_path, Url};
use crate::{emit, files::{File, FilesOp}, tab::Tab};
pub struct Opt {
target: Url,
}
impl From<&Exec> for Opt {
fn from(e: &Exec) -> Self {
Self { target: Url::from(expand_path(e.args.first().map(|s| s.as_str()).unwrap_or(""))) }
}
}
impl Tab {
pub fn reveal(&mut self, opt: impl Into<Opt>) -> bool {
let opt = opt.into() as Opt;
let Some(parent) = opt.target.parent_url() else {
return false;
};
let b = self.cd(parent.clone());
emit!(Files(FilesOp::Creating(
parent.clone(),
File::from_dummy(opt.target.clone()).into_map()
)));
emit!(Hover(opt.target));
b
}
}

View File

@ -3,6 +3,7 @@ use std::{mem, time::Duration};
use anyhow::bail;
use tokio::pin;
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
use yazi_config::keymap::{Exec, KeymapLayer};
use crate::{emit, external, files::FilesOp, input::InputOpt, tab::Tab};
@ -17,7 +18,7 @@ impl Tab {
self.search = Some(tokio::spawn(async move {
let Some(Ok(subject)) = emit!(Input(InputOpt::top("Search:"))).recv().await else {
bail!("canceled")
bail!("")
};
cwd = cwd.into_search(subject.clone());
@ -34,7 +35,7 @@ impl Tab {
let mut first = true;
while let Some(chunk) = rx.next().await {
if first {
emit!(Cd(cwd.clone()));
emit!(Call(Exec::call("cd", vec![cwd.clone().to_string()]).vec(), KeymapLayer::Manager));
first = false;
}
emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk)));

View File

@ -252,7 +252,7 @@ impl Tasks {
pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap<Url, String>) -> bool {
let targets: Vec<_> = targets
.iter()
.filter(|f| f.is_file() && !mimetype.contains_key(&f.url))
.filter(|f| !f.is_dir() && !mimetype.contains_key(&f.url))
.map(|f| f.url())
.collect();

View File

@ -5,7 +5,7 @@ use crossterm::event::KeyEvent;
use tokio::sync::oneshot;
use yazi_config::{keymap::{Exec, Key, KeymapLayer}, BOOT};
use yazi_core::{emit, files::FilesOp, input::InputMode, Ctx, Event};
use yazi_shared::{expand_url, Term};
use yazi_shared::Term;
use crate::{Executor, Logs, Root, Signals};
@ -121,11 +121,6 @@ impl App {
let manager = &mut self.cx.manager;
let tasks = &mut self.cx.tasks;
match event {
Event::Cd(url) => {
futures::executor::block_on(async {
manager.active_mut().cd(expand_url(url)).await;
});
}
Event::Refresh => {
manager.refresh();
}

View File

@ -1,6 +1,6 @@
use yazi_config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP};
use yazi_core::{emit, input::InputMode, tab::FinderCase, Ctx};
use yazi_shared::{optional_bool, Url};
use yazi_core::{input::InputMode, tab::FinderCase, Ctx};
use yazi_shared::{expand_url, optional_bool, Url};
pub(super) struct Executor<'a> {
cx: &'a mut Ctx,
@ -96,10 +96,10 @@ impl<'a> Executor<'a> {
if exec.named.contains_key("interactive") {
self.cx.manager.active_mut().cd_interactive(url)
} else {
emit!(Cd(url));
false
self.cx.manager.active_mut().cd(expand_url(url))
}
}
"reveal" => self.cx.manager.active_mut().reveal(exec),
// Selection
"select" => {

View File

@ -16,8 +16,8 @@ impl UserData for File {
fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("url", |_, me| Ok(Url::from(&me.0.url)));
fields.add_field_method_get("link_to", |_, me| Ok(me.0.link_to().map(Url::from)));
fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link));
fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden));
fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link()));
fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden()));
}
}
@ -50,36 +50,37 @@ impl Files {
LUA.register_userdata_type::<yazi_core::files::File>(|reg| {
reg.add_field_method_get("url", |_, me| Ok(Url::from(&me.url)));
reg.add_field_method_get("link_to", |_, me| Ok(me.link_to().map(Url::from)));
reg.add_field_method_get("is_link", |_, me| Ok(me.is_link));
reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden));
reg.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden()));
// Metadata
reg.add_field_method_get("is_file", |_, me| Ok(me.is_file()));
reg.add_field_method_get("is_dir", |_, me| Ok(me.is_dir()));
reg.add_field_method_get("is_symlink", |_, me| Ok(me.meta.is_symlink()));
reg.add_field_method_get("is_symlink", |_, me| Ok(me.is_link()));
#[cfg(unix)]
{
use std::os::unix::prelude::FileTypeExt;
reg.add_field_method_get("is_block_device", |_, me| {
Ok(me.meta.file_type().is_block_device())
});
reg
.add_field_method_get("is_char_device", |_, me| Ok(me.meta.file_type().is_char_device()));
reg.add_field_method_get("is_fifo", |_, me| Ok(me.meta.file_type().is_fifo()));
reg.add_field_method_get("is_socket", |_, me| Ok(me.meta.file_type().is_socket()));
reg.add_field_method_get("is_block_device", |_, me| Ok(me.is_block_device()));
reg.add_field_method_get("is_char_device", |_, me| Ok(me.is_char_device()));
reg.add_field_method_get("is_fifo", |_, me| Ok(me.is_fifo()));
reg.add_field_method_get("is_socket", |_, me| Ok(me.is_socket()));
}
reg.add_field_method_get("length", |_, me| Ok(me.meta.len()));
reg.add_field_method_get("length", |_, me| Ok(me.len));
reg.add_field_method_get("created", |_, me| {
Ok(me.meta.created()?.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())
Ok(me.created.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok()))
});
reg.add_field_method_get("modified", |_, me| {
Ok(me.meta.modified()?.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())
Ok(me.modified.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok()))
});
reg.add_field_method_get("accessed", |_, me| {
Ok(me.meta.accessed()?.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())
Ok(me.accessed.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok()))
});
reg.add_method("permissions", |_, me, ()| {
Ok(
#[cfg(unix)]
Some(yazi_shared::permissions(me.permissions)),
#[cfg(windows)]
None::<String>,
)
});
reg
.add_method("permissions", |_, me, ()| Ok(yazi_shared::permissions(me.meta.permissions())));
// Extension
reg.add_field_method_get("name", |_, me| {
@ -88,7 +89,7 @@ impl Files {
reg.add_function("size", |_, me: AnyUserData| {
let file = me.borrow::<yazi_core::files::File>()?;
if !file.is_dir() {
return Ok(Some(file.meta.len()));
return Ok(Some(file.len));
}
let folder = me.named_user_value::<UserDataRef<yazi_core::tab::Folder>>("folder")?;

View File

@ -10,10 +10,12 @@ repository = "https://github.com/sxyazi/yazi"
[dependencies]
anyhow = "^1"
bitflags = "^2"
crossterm = "^0"
futures = "^0"
libc = "^0"
parking_lot = "^0"
percent-encoding = "^2"
ratatui = "^0"
regex = "^1"
tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs" ] }

108
yazi-shared/src/cha.rs Normal file
View File

@ -0,0 +1,108 @@
use std::{fs::Metadata, time::SystemTime};
use bitflags::bitflags;
bitflags! {
#[derive(Clone, Copy, Debug, Default)]
pub struct ChaMeta: u8 {
const DIR = 0b00000001;
const HIDDEN = 0b00000010;
const LINK = 0b00000100;
const BAD_LINK = 0b00001000;
const BLOCK_DEVICE = 0b00010000;
const CHAR_DEVICE = 0b00100000;
const FIFO = 0b01000000;
const SOCKET = 0b10000000;
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Cha {
pub meta: ChaMeta,
pub len: u64,
pub accessed: Option<SystemTime>,
pub created: Option<SystemTime>,
pub modified: Option<SystemTime>,
#[cfg(unix)]
pub permissions: u32,
}
impl From<Metadata> for Cha {
fn from(m: Metadata) -> Self {
let mut cm = ChaMeta::empty();
if m.is_dir() {
cm |= ChaMeta::DIR;
}
#[cfg(unix)]
{
use std::os::unix::prelude::FileTypeExt;
if m.file_type().is_block_device() {
cm |= ChaMeta::BLOCK_DEVICE;
}
if m.file_type().is_char_device() {
cm |= ChaMeta::CHAR_DEVICE;
}
if m.file_type().is_fifo() {
cm |= ChaMeta::FIFO;
}
if m.file_type().is_socket() {
cm |= ChaMeta::SOCKET;
}
}
Self {
meta: cm,
len: m.len(),
accessed: m.accessed().ok(),
created: m.created().ok(),
modified: m.modified().ok(),
#[cfg(unix)]
permissions: {
use std::os::unix::prelude::PermissionsExt;
m.permissions().mode()
},
}
}
}
impl Cha {
#[inline]
pub fn with_meta(mut self, meta: ChaMeta) -> Self {
self.meta |= meta;
self
}
}
impl Cha {
#[inline]
pub fn is_dir(self) -> bool { self.meta.contains(ChaMeta::DIR) }
#[inline]
pub fn is_hidden(self) -> bool { self.meta.contains(ChaMeta::HIDDEN) }
#[inline]
pub fn is_link(self) -> bool { self.meta.contains(ChaMeta::LINK) }
#[inline]
pub fn is_bad_link(self) -> bool { self.meta.contains(ChaMeta::BAD_LINK) }
#[cfg(unix)]
#[inline]
pub fn is_block_device(self) -> bool { self.meta.contains(ChaMeta::BLOCK_DEVICE) }
#[cfg(unix)]
#[inline]
pub fn is_char_device(self) -> bool { self.meta.contains(ChaMeta::CHAR_DEVICE) }
#[cfg(unix)]
#[inline]
pub fn is_fifo(self) -> bool { self.meta.contains(ChaMeta::FIFO) }
#[cfg(unix)]
#[inline]
pub fn is_socket(self) -> bool { self.meta.contains(ChaMeta::SOCKET) }
}

View File

@ -1,10 +1,10 @@
use std::{borrow::Cow, env, ffi::OsString, path::{Component, Path, PathBuf}};
use std::{borrow::Cow, env, ffi::OsString, path::{Component, Path, PathBuf, MAIN_SEPARATOR}};
use tokio::fs;
use crate::Url;
pub fn expand_path(p: impl AsRef<Path>) -> PathBuf {
fn _expand_path(p: &Path) -> PathBuf {
// ${HOME} or $HOME
#[cfg(unix)]
let re = regex::Regex::new(r"\$(?:\{([^}]+)\}|([a-zA-Z\d_]+))").unwrap();
@ -13,22 +13,20 @@ pub fn expand_path(p: impl AsRef<Path>) -> PathBuf {
#[cfg(windows)]
let re = regex::Regex::new(r"%([^%]+)%").unwrap();
let s = p.as_ref().to_string_lossy();
let s = p.to_string_lossy();
let s = re.replace_all(&s, |caps: &regex::Captures| {
let name = caps.get(2).or_else(|| caps.get(1)).unwrap();
env::var(name.as_str()).unwrap_or_else(|_| caps.get(0).unwrap().as_str().to_owned())
});
let p = Path::new(s.as_ref());
if let Ok(p) = p.strip_prefix("~") {
if let Ok(rest) = p.strip_prefix("~") {
#[cfg(unix)]
if let Some(home) = env::var_os("HOME") {
return Path::new(&home).join(p);
}
let home = env::var_os("HOME");
#[cfg(windows)]
if let Some(home) = env::var_os("USERPROFILE") {
return Path::new(&home).join(p);
}
let home = env::var_os("USERPROFILE");
return if let Some(p) = home { PathBuf::from(p).join(rest) } else { rest.to_path_buf() };
}
if p.is_absolute() {
@ -37,12 +35,36 @@ pub fn expand_path(p: impl AsRef<Path>) -> PathBuf {
env::current_dir().map_or_else(|_| p.to_path_buf(), |c| c.join(p))
}
#[inline]
pub fn expand_path(p: impl AsRef<Path>) -> PathBuf { _expand_path(p.as_ref()) }
#[inline]
pub fn expand_url(mut u: Url) -> Url {
u.set_path(expand_path(&u));
u.set_path(_expand_path(&u));
u
}
#[inline]
pub fn ends_with_slash(p: &Path) -> bool {
// TODO: uncomment this when Rust 1.74 is released
// let b = p.as_os_str().as_encoded_bytes();
// if let [.., last] = b { *last == MAIN_SEPARATOR as u8 } else { false }
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let b = p.as_os_str().as_bytes();
if let [.., last] = b { *last == MAIN_SEPARATOR as u8 } else { false }
}
#[cfg(windows)]
{
let s = p.to_string_lossy();
let b = s.as_bytes();
if let [.., last] = b { *last == MAIN_SEPARATOR as u8 } else { false }
}
}
pub async fn unique_path(mut p: Url) -> Url {
let Some(stem) = p.file_stem().map(|s| s.to_owned()) else {
return p;

View File

@ -1,4 +1,4 @@
use std::{collections::VecDeque, fs::Permissions, path::{Path, PathBuf}};
use std::{collections::VecDeque, path::{Path, PathBuf}};
use anyhow::Result;
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
@ -91,29 +91,14 @@ pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver<Result<u64,
rx
}
// Convert a file mode to a string representation
#[cfg(windows)]
#[allow(clippy::collapsible_else_if)]
pub fn permissions(_: Permissions) -> Option<String> { None }
// Convert a file mode to a string representation
#[cfg(unix)]
#[allow(clippy::collapsible_else_if)]
pub fn permissions(permissions: Permissions) -> Option<String> {
use std::os::unix::prelude::PermissionsExt;
use libc::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
#[cfg(target_os = "macos")]
let m = permissions.mode() as u16;
#[cfg(target_os = "freebsd")]
let m = permissions.mode() as u16;
#[cfg(target_os = "netbsd")]
let m = permissions.mode();
#[cfg(target_os = "linux")]
let m = permissions.mode();
pub fn permissions(mode: u32) -> String {
use libc::{mode_t, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
let mut s = String::with_capacity(10);
let m = mode as mode_t;
// File type
s.push(match m & S_IFMT {
@ -153,7 +138,7 @@ pub fn permissions(permissions: Permissions) -> Option<String> {
if m & S_ISVTX != 0 { 'T' } else { '-' }
});
Some(s)
s
}
// Find the max common root of a list of files

View File

@ -1,5 +1,6 @@
#![allow(clippy::option_map_unit_fn)]
mod cha;
mod chars;
mod debounce;
mod defer;
@ -14,6 +15,7 @@ mod throttle;
mod time;
mod url;
pub use cha::*;
pub use chars::*;
pub use debounce::*;
pub use defer::*;

View File

@ -1,5 +1,9 @@
use std::{ffi::{OsStr, OsString}, fmt::{Debug, Formatter}, ops::{Deref, DerefMut}, path::{Path, PathBuf}};
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS};
const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#');
#[derive(Clone, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Url {
scheme: UrlScheme,
@ -42,15 +46,41 @@ impl From<&Path> for Url {
}
impl From<String> for Url {
fn from(path: String) -> Self { Self::from(PathBuf::from(path)) }
fn from(path: String) -> Self { Self::from(path.as_str()) }
}
impl From<&String> for Url {
fn from(path: &String) -> Self { Self::from(PathBuf::from(path)) }
fn from(path: &String) -> Self { Self::from(path.as_str()) }
}
impl From<&str> for Url {
fn from(path: &str) -> Self { Self::from(PathBuf::from(path)) }
fn from(mut path: &str) -> Self {
let mut url = Url::default();
match path.split_once("://").map(|(a, b)| (UrlScheme::from(a), b)) {
None => {
url.path = PathBuf::from(path);
return url;
}
Some((UrlScheme::Regular, b)) => {
url.path = PathBuf::from(b);
return url;
}
Some((a, b)) => {
url.scheme = a;
path = b;
}
}
match path.split_once('#') {
None => {
url.path = percent_decode_str(path).decode_utf8_lossy().into_owned().into();
}
Some((a, b)) => {
url.path = percent_decode_str(a).decode_utf8_lossy().into_owned().into();
url.frag = Some(b.to_string()).filter(|s| !s.is_empty());
}
}
url
}
}
impl AsRef<Url> for Url {
@ -65,6 +95,32 @@ impl AsRef<OsStr> for Url {
fn as_ref(&self) -> &OsStr { self.path.as_os_str() }
}
impl ToString for Url {
fn to_string(&self) -> String {
if self.scheme == UrlScheme::Regular {
return self.path.to_string_lossy().to_string();
}
let scheme = match self.scheme {
UrlScheme::Regular => unreachable!(),
UrlScheme::Search => "search://",
UrlScheme::Archive => "archive://",
};
#[cfg(unix)]
let path = {
use std::os::unix::ffi::OsStrExt;
percent_encode(self.path.as_os_str().as_bytes(), ENCODE_SET)
};
#[cfg(windows)]
let path = percent_encode(self.path.to_string_lossy().as_bytes(), ENCODE_SET).to_string();
let frag = self.frag.as_ref().map(|s| format!("#{s}")).unwrap_or_default();
format!("{scheme}{path}{frag}")
}
}
impl Url {
#[inline]
pub fn join(&self, path: impl AsRef<Path>) -> Self {
@ -95,6 +151,11 @@ impl Url {
#[inline]
pub fn into_os_string(self) -> OsString { self.path.into_os_string() }
#[inline]
pub fn is_hidden(&self) -> bool {
self.file_name().map_or(false, |s| s.to_string_lossy().starts_with('.'))
}
}
impl Url {
@ -144,3 +205,13 @@ impl Url {
#[inline]
pub fn frag(&self) -> Option<&str> { self.frag.as_deref() }
}
impl From<&str> for UrlScheme {
fn from(value: &str) -> Self {
match value {
"search" => UrlScheme::Search,
"archive" => UrlScheme::Archive,
_ => UrlScheme::Regular,
}
}
}