diff --git a/Cargo.lock b/Cargo.lock index fa7939fd..4d5af202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/cspell.json b/cspell.json index a97d04ed..89b6f851 100644 --- a/cspell.json +++ b/cspell.json @@ -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"]} \ No newline at end of file +{"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":[]} diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index f85ed497..d034d6b7 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -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" diff --git a/yazi-core/src/event.rs b/yazi-core/src/event.rs index d09f30f0..82baeb99 100644 --- a/yazi-core/src/event.rs +++ b/yazi-core/src/event.rs @@ -21,7 +21,6 @@ pub enum Event { Call(Vec, 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(); }; diff --git a/yazi-core/src/external/fzf.rs b/yazi-core/src/external/fzf.rs index 475367d9..dfe67329 100644 --- a/yazi-core/src/external/fzf.rs +++ b/yazi-core/src/external/fzf.rs @@ -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>> { +pub async fn fzf(opt: FzfOpt) -> Result { 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 selected = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !selected.is_empty() { - tx.send(Ok(opt.cwd.join(selected))).ok(); - return; - } - } - tx.send(Err(anyhow::anyhow!("No match"))).ok(); - }); - Ok(rx) + let output = child.wait_with_output().await?; + let selected = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + if selected.is_empty() { + bail!("No match") + } + return Ok(Url::from(Path::new(&opt.cwd).join(selected))); } diff --git a/yazi-core/src/external/zoxide.rs b/yazi-core/src/external/zoxide.rs index f15ed57c..11900af8 100644 --- a/yazi-core/src/external/zoxide.rs +++ b/yazi-core/src/external/zoxide.rs @@ -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>> { +pub async fn zoxide(opt: ZoxideOpt) -> Result { let child = Command::new("zoxide") .args(["query", "-i", "--exclude"]) .arg(&opt.cwd) @@ -16,16 +16,11 @@ pub fn zoxide(opt: ZoxideOpt) -> Result>> { .stdout(Stdio::piped()) .spawn()?; - let (tx, rx) = oneshot::channel(); - tokio::spawn(async move { - if let Ok(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; - } - } - tx.send(Err(anyhow::anyhow!("No match"))).ok(); - }); - Ok(rx) + let output = child.wait_with_output().await?; + let selected = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + if !selected.is_empty() { + return Ok(Url::from(selected)); + } + bail!("No match") } diff --git a/yazi-core/src/files/file.rs b/yazi-core/src/files/file.rs index c136a04a..30ccfd6c 100644 --- a/yazi-core/src/files/file.rs +++ b/yazi-core/src/files/file.rs @@ -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, - 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 { - 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 { 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() } diff --git a/yazi-core/src/files/files.rs b/yazi-core/src/files/files.rs index 944947a6..a11d89ad 100644 --- a/yazi-core/src/files/files.rs +++ b/yazi-core/src/files/files.rs @@ -128,7 +128,7 @@ impl Files { pub fn update_full(&mut self, mut items: Vec) -> 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) -> 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; diff --git a/yazi-core/src/files/sorter.rs b/yazi-core/src/files/sorter.rs index 67436796..7d8609d3 100644 --- a/yazi-core/src/files/sorter.rs +++ b/yazi-core/src/files/sorter.rs @@ -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; } diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index f759b566..c0aac644 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -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, watched: Arc>>>) { + async fn on_changed( + rx: UnboundedReceiver, + watched: Arc>>>, + ) { // 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>>>) { + async fn files_changed(urls: &[Url], watched: &IndexMap>) { 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>>>) { - 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>) { + 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 { + let linked_files = |files: &[File], linked: &Url| -> Vec { 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>) -> Vec<&'a Url> { + watched + .iter() + .filter_map(|(k, v)| v.as_ref().map(|v| (k, v))) + .filter(|(_, v)| *v == url) + .map(|(k, _)| k) + .collect() + } } diff --git a/yazi-core/src/tab/commands/backstack.rs b/yazi-core/src/tab/commands/backstack.rs index a54c7633..5d11b7ab 100644 --- a/yazi-core/src/tab/commands/backstack.rs +++ b/yazi-core/src/tab/commands/backstack.rs @@ -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 } diff --git a/yazi-core/src/tab/commands/cd.rs b/yazi-core/src/tab/commands/cd.rs index 2722a6d0..4ff3240f 100644 --- a/yazi-core/src/tab/commands/cd.rs +++ b/yazi-core/src/tab/commands/cd.rs @@ -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( diff --git a/yazi-core/src/tab/commands/escape.rs b/yazi-core/src/tab/commands/escape.rs index 3d073808..e8e7eca5 100644 --- a/yazi-core/src/tab/commands/escape.rs +++ b/yazi-core/src/tab/commands/escape.rs @@ -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) -> 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 diff --git a/yazi-core/src/tab/commands/jump.rs b/yazi-core/src/tab/commands/jump.rs index 58297e19..6a0aea4c 100644 --- a/yazi-core/src/tab/commands/jump.rs +++ b/yazi-core/src/tab/commands/jump.rs @@ -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 diff --git a/yazi-core/src/tab/commands/mod.rs b/yazi-core/src/tab/commands/mod.rs index 47fa94f7..fa8d1c39 100644 --- a/yazi-core/src/tab/commands/mod.rs +++ b/yazi-core/src/tab/commands/mod.rs @@ -9,6 +9,7 @@ mod hidden; mod jump; mod leave; mod linemode; +mod reveal; mod search; mod select; mod shell; diff --git a/yazi-core/src/tab/commands/reveal.rs b/yazi-core/src/tab/commands/reveal.rs new file mode 100644 index 00000000..e6f5781b --- /dev/null +++ b/yazi-core/src/tab/commands/reveal.rs @@ -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) -> 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 + } +} diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index c471baa2..80191ce2 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -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))); diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index bd6d8ad9..00a6ee02 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -252,7 +252,7 @@ impl Tasks { pub fn precache_mime(&self, targets: &[File], mimetype: &HashMap) -> 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(); diff --git a/yazi-fm/src/app.rs b/yazi-fm/src/app.rs index 40f80ae6..a0b4759f 100644 --- a/yazi-fm/src/app.rs +++ b/yazi-fm/src/app.rs @@ -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(); } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 9ad78584..9a27f06e 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -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" => { diff --git a/yazi-plugin/src/bindings/files.rs b/yazi-plugin/src/bindings/files.rs index 4d75df9f..99bdfb67 100644 --- a/yazi-plugin/src/bindings/files.rs +++ b/yazi-plugin/src/bindings/files.rs @@ -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::(|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::, + ) }); - 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::()?; if !file.is_dir() { - return Ok(Some(file.meta.len())); + return Ok(Some(file.len)); } let folder = me.named_user_value::>("folder")?; diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 596ff820..2dd4fbe0 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -9,11 +9,13 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -anyhow = "^1" -crossterm = "^0" -futures = "^0" -libc = "^0" -parking_lot = "^0" -ratatui = "^0" -regex = "^1" -tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs" ] } +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" ] } diff --git a/yazi-shared/src/cha.rs b/yazi-shared/src/cha.rs new file mode 100644 index 00000000..e2170598 --- /dev/null +++ b/yazi-shared/src/cha.rs @@ -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, + pub created: Option, + pub modified: Option, + #[cfg(unix)] + pub permissions: u32, +} + +impl From 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) } +} diff --git a/yazi-shared/src/fns.rs b/yazi-shared/src/fns.rs index 7e4cb931..59e0e24a 100644 --- a/yazi-shared/src/fns.rs +++ b/yazi-shared/src/fns.rs @@ -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) -> 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) -> 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: ®ex::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) -> PathBuf { env::current_dir().map_or_else(|_| p.to_path_buf(), |c| c.join(p)) } +#[inline] +pub fn expand_path(p: impl AsRef) -> 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; diff --git a/yazi-shared/src/fs.rs b/yazi-shared/src/fs.rs index 5b2ec8a5..cfd1f6d7 100644 --- a/yazi-shared/src/fs.rs +++ b/yazi-shared/src/fs.rs @@ -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 Option { None } - // Convert a file mode to a string representation #[cfg(unix)] #[allow(clippy::collapsible_else_if)] -pub fn permissions(permissions: Permissions) -> Option { - 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 { if m & S_ISVTX != 0 { 'T' } else { '-' } }); - Some(s) + s } // Find the max common root of a list of files diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 20f3a9e9..215001a3 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -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::*; diff --git a/yazi-shared/src/url.rs b/yazi-shared/src/url.rs index 10e45db3..4724c4c7 100644 --- a/yazi-shared/src/url.rs +++ b/yazi-shared/src/url.rs @@ -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 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 for Url { @@ -65,6 +95,32 @@ impl AsRef 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) -> 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, + } + } +}