mirror of
https://github.com/sxyazi/yazi.git
synced 2024-12-18 14:21:32 +03:00
feat: new reveal
command (#341)
This commit is contained in:
parent
a0ba853718
commit
1bbb323509
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
@ -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":[]}
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
};
|
||||
|
27
yazi-core/src/external/fzf.rs
vendored
27
yazi-core/src/external/fzf.rs
vendored
@ -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 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)));
|
||||
}
|
||||
|
25
yazi-core/src/external/zoxide.rs
vendored
25
yazi-core/src/external/zoxide.rs
vendored
@ -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 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")
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -9,6 +9,7 @@ mod hidden;
|
||||
mod jump;
|
||||
mod leave;
|
||||
mod linemode;
|
||||
mod reveal;
|
||||
mod search;
|
||||
mod select;
|
||||
mod shell;
|
||||
|
32
yazi-core/src/tab/commands/reveal.rs
Normal file
32
yazi-core/src/tab/commands/reveal.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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)));
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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" => {
|
||||
|
@ -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")?;
|
||||
|
@ -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" ] }
|
||||
|
108
yazi-shared/src/cha.rs
Normal file
108
yazi-shared/src/cha.rs
Normal 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) }
|
||||
}
|
@ -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: ®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<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;
|
||||
|
@ -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
|
||||
|
@ -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::*;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user