mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: cross-system opener rule support (#289)
This commit is contained in:
parent
e4930a7ef6
commit
9d4ba510df
8 changed files with 112 additions and 67 deletions
|
|
@ -64,7 +64,7 @@ impl Executor {
|
|||
}
|
||||
"peek" => {
|
||||
let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
cx.manager.active_mut().preview_arrow(step);
|
||||
cx.manager.active_mut().preview.arrow(step);
|
||||
cx.manager.peek(true, cx.image_layer())
|
||||
}
|
||||
"leave" => cx.manager.active_mut().leave(),
|
||||
|
|
|
|||
|
|
@ -14,55 +14,52 @@ max_height = 900
|
|||
cache_dir = ""
|
||||
|
||||
[opener]
|
||||
folder = [
|
||||
{ exec = 'open -R "$@"', display_name = "Reveal in Finder" },
|
||||
{ exec = '$EDITOR "$@"' },
|
||||
edit = [
|
||||
{ exec = '$EDITOR "$@"', block = true, for = "unix" },
|
||||
{ exec = 'code "%*"', for = "windows" },
|
||||
]
|
||||
archive = [
|
||||
{ exec = 'unar "$1"', display_name = "Extract here" },
|
||||
open = [
|
||||
{ exec = 'xdg-open "$@"', desc = "Open", for = "linux" },
|
||||
{ exec = 'open "$@"', desc = "Open", for = "macos" },
|
||||
{ exec = '%1', desc = "Open", for = "windows" }
|
||||
]
|
||||
text = [
|
||||
{ exec = '$EDITOR "$@"', block = true },
|
||||
reveal = [
|
||||
{ exec = 'open -R "$@"', desc = "Reveal", for = "macos" },
|
||||
{ exec = 'explorer "%*"', desc = "Reveal", for = "windows" },
|
||||
{ exec = '''exiftool "$1"; echo "Press enter to exit"; read''', block = true, desc = "Show EXIF", for = "unix" },
|
||||
]
|
||||
image = [
|
||||
{ exec = 'open "$@"', display_name = "Open" },
|
||||
{ exec = '''exiftool "$1"; echo "Press enter to exit"; read''', block = true, display_name = "Show EXIF" },
|
||||
extract = [
|
||||
{ exec = 'unar "$1"', desc = "Extract here", for = "unix" },
|
||||
{ exec = 'unar "%1"', desc = "Extract here", for = "windows" },
|
||||
]
|
||||
video = [
|
||||
{ exec = 'mpv "$@"' },
|
||||
{ exec = '''mediainfo "$1"; echo "Press enter to exit"; read''', block = true, display_name = "Show media info" },
|
||||
]
|
||||
audio = [
|
||||
{ exec = 'mpv "$@"' },
|
||||
{ exec = '''mediainfo "$1"; echo "Press enter to exit"; read''', block = true, display_name = "Show media info" },
|
||||
]
|
||||
fallback = [
|
||||
{ exec = 'open "$@"', display_name = "Open" },
|
||||
{ exec = 'open -R "$@"', display_name = "Reveal in Finder" },
|
||||
play = [
|
||||
{ exec = 'mpv "$@"', orphan = true, for = "unix" },
|
||||
{ exec = 'mpv "%1"', for = "windows" },
|
||||
{ exec = '''mediainfo "$1"; echo "Press enter to exit"; read''', block = true, desc = "Show media info", for = "unix" },
|
||||
]
|
||||
|
||||
[open]
|
||||
rules = [
|
||||
{ name = "*/", use = "folder" },
|
||||
{ name = "*/", use = [ "edit", "open", "reveal" ] },
|
||||
|
||||
{ mime = "text/*", use = "text" },
|
||||
{ mime = "image/*", use = "image" },
|
||||
{ mime = "video/*", use = "video" },
|
||||
{ mime = "audio/*", use = "audio" },
|
||||
{ mime = "inode/x-empty", use = "text" },
|
||||
{ mime = "text/*", use = [ "edit", "reveal" ] },
|
||||
{ mime = "image/*", use = [ "open", "reveal" ] },
|
||||
{ mime = "video/*", use = [ "play", "reveal" ] },
|
||||
{ mime = "audio/*", use = [ "play", "reveal" ] },
|
||||
{ mime = "inode/x-empty", use = [ "edit", "reveal" ] },
|
||||
|
||||
{ mime = "application/json", use = "text" },
|
||||
{ mime = "*/javascript", use = "text" },
|
||||
{ mime = "application/json", use = [ "edit", "reveal" ] },
|
||||
{ mime = "*/javascript", use = [ "edit", "reveal" ] },
|
||||
|
||||
{ mime = "application/zip", use = "archive" },
|
||||
{ mime = "application/gzip", use = "archive" },
|
||||
{ mime = "application/x-tar", use = "archive" },
|
||||
{ mime = "application/x-bzip", use = "archive" },
|
||||
{ mime = "application/x-bzip2", use = "archive" },
|
||||
{ mime = "application/x-7z-compressed", use = "archive" },
|
||||
{ mime = "application/x-rar", use = "archive" },
|
||||
{ mime = "application/zip", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/gzip", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-tar", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-bzip", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-bzip2", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-7z-compressed", use = [ "extract", "reveal" ] },
|
||||
{ mime = "application/x-rar", use = [ "extract", "reveal" ] },
|
||||
|
||||
{ mime = "*", use = "fallback" },
|
||||
{ mime = "*", use = [ "open", "reveal" ] },
|
||||
]
|
||||
|
||||
[tasks]
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use crate::{open::OpenRule, MERGED_YAZI};
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Open {
|
||||
openers: BTreeMap<String, IndexSet<Opener>>,
|
||||
rules: Vec<OpenRule>,
|
||||
openers: BTreeMap<String, IndexSet<Opener>>,
|
||||
}
|
||||
|
||||
impl Default for Open {
|
||||
|
|
@ -23,8 +23,8 @@ impl Open {
|
|||
P: AsRef<Path>,
|
||||
M: AsRef<str>,
|
||||
{
|
||||
let is_folder = Some(mime.as_ref() == MIME_DIR);
|
||||
self.rules.iter().find_map(|rule| {
|
||||
let is_folder = Some(mime.as_ref() == MIME_DIR);
|
||||
if rule.mime.as_ref().is_some_and(|m| m.matches(&mime))
|
||||
|| rule.name.as_ref().is_some_and(|n| n.match_path(&path, is_folder))
|
||||
{
|
||||
|
|
@ -74,7 +74,11 @@ impl<'de> Deserialize<'de> for Open {
|
|||
}
|
||||
|
||||
let outer = Outer::deserialize(deserializer)?;
|
||||
let openers = outer.opener.into_iter().map(|(k, v)| (k, IndexSet::from_iter(v))).collect();
|
||||
Ok(Self { openers, rules: outer.open.rules })
|
||||
let openers = outer
|
||||
.opener
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.into_iter().filter_map(|o| o.take()).collect::<IndexSet<_>>()))
|
||||
.collect();
|
||||
Ok(Self { rules: outer.open.rules, openers })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,27 @@ use serde::{Deserialize, Deserializer};
|
|||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Opener {
|
||||
pub exec: String,
|
||||
pub block: bool,
|
||||
pub orphan: bool,
|
||||
pub display_name: String,
|
||||
pub spread: bool,
|
||||
pub exec: String,
|
||||
pub block: bool,
|
||||
pub orphan: bool,
|
||||
pub desc: String,
|
||||
pub for_: Option<String>,
|
||||
pub spread: bool,
|
||||
}
|
||||
|
||||
impl Opener {
|
||||
pub fn take(mut self) -> Option<Self> {
|
||||
if let Some(for_) = self.for_.take() {
|
||||
match for_.as_bytes() {
|
||||
b"unix" if cfg!(unix) => {}
|
||||
b"windows" if cfg!(windows) => {}
|
||||
b"linux" if cfg!(target_os = "linux") => {}
|
||||
b"macos" if cfg!(target_os = "macos") => {}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Opener {
|
||||
|
|
@ -16,25 +32,49 @@ impl<'de> Deserialize<'de> for Opener {
|
|||
{
|
||||
#[derive(Deserialize)]
|
||||
pub struct Shadow {
|
||||
pub exec: String,
|
||||
exec: String,
|
||||
#[serde(default)]
|
||||
pub block: bool,
|
||||
block: bool,
|
||||
#[serde(default)]
|
||||
pub orphan: bool,
|
||||
pub display_name: Option<String>,
|
||||
orphan: bool,
|
||||
desc: Option<String>,
|
||||
#[serde(rename = "for")]
|
||||
for_: Option<String>,
|
||||
|
||||
// TODO: remove this when v1.0.5 is released --
|
||||
display_name: Option<String>,
|
||||
// TODO: -- remove this when v1.0.5 is released
|
||||
}
|
||||
|
||||
let shadow = Shadow::deserialize(deserializer)?;
|
||||
|
||||
let mut shadow = Shadow::deserialize(deserializer)?;
|
||||
if shadow.exec.is_empty() {
|
||||
return Err(serde::de::Error::custom("`exec` cannot be empty"));
|
||||
}
|
||||
|
||||
let display_name = shadow
|
||||
.display_name
|
||||
.unwrap_or_else(|| shadow.exec.split_whitespace().next().unwrap().to_string());
|
||||
// TODO: remove this when v1.0.5 is released --
|
||||
if shadow.display_name.is_some() {
|
||||
println!(
|
||||
"WARNING: `display_name` is deprecated and will be removed in Yazi v1.0.6. Use `desc` instead.\ne.g. {}\n\n",
|
||||
r#"{ exec = 'nvim "$@"', display_name = "Edit" } ==> { exec = 'nvim "$@"', desc = "Edit" }"#
|
||||
);
|
||||
}
|
||||
if shadow.display_name.is_some() && shadow.desc.is_none() {
|
||||
shadow.desc = shadow.display_name.clone();
|
||||
}
|
||||
// TODO: -- remove this when v1.0.5 is released
|
||||
|
||||
let spread = shadow.exec.contains("$*") || shadow.exec.contains("$@");
|
||||
Ok(Self { exec: shadow.exec, block: shadow.block, orphan: shadow.orphan, display_name, spread })
|
||||
let desc =
|
||||
shadow.desc.unwrap_or_else(|| shadow.exec.split_whitespace().next().unwrap().to_string());
|
||||
|
||||
let spread =
|
||||
shadow.exec.contains("$@") || shadow.exec.contains("%*") || shadow.exec.contains("$*");
|
||||
Ok(Self {
|
||||
exec: shadow.exec,
|
||||
block: shadow.block,
|
||||
orphan: shadow.orphan,
|
||||
desc,
|
||||
for_: shadow.for_,
|
||||
spread,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ impl Default for Preview {
|
|||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Shadow {
|
||||
pub tab_size: u32,
|
||||
pub max_width: u32,
|
||||
pub max_height: u32,
|
||||
tab_size: u32,
|
||||
max_width: u32,
|
||||
max_height: u32,
|
||||
|
||||
pub cache_dir: Option<String>,
|
||||
cache_dir: Option<String>,
|
||||
}
|
||||
|
||||
let preview = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().preview;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ impl Manager {
|
|||
|
||||
let result = emit!(Select(SelectOpt::hovered(
|
||||
"Open with:",
|
||||
openers.iter().map(|o| o.display_name.clone()).collect()
|
||||
openers.iter().map(|o| o.desc.clone()).collect()
|
||||
)));
|
||||
if let Ok(choice) = result.await {
|
||||
emit!(Open(files, Some(openers[choice].clone())));
|
||||
|
|
|
|||
|
|
@ -26,7 +26,14 @@ impl Tab {
|
|||
|
||||
emit!(Open(
|
||||
selected,
|
||||
Some(Opener { exec, block, orphan: false, display_name: Default::default(), spread: true })
|
||||
Some(Opener {
|
||||
exec,
|
||||
block,
|
||||
orphan: false,
|
||||
desc: Default::default(),
|
||||
for_: None,
|
||||
spread: true
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,6 @@ impl Tab {
|
|||
#[inline]
|
||||
pub fn preview_reset_image(&mut self) -> bool { self.preview.reset(|l| l.is_image()) }
|
||||
|
||||
#[inline]
|
||||
pub fn preview_arrow(&mut self, step: isize) -> bool { self.preview.arrow(step) }
|
||||
|
||||
// --- Sorter
|
||||
pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool {
|
||||
if sorter == self.sorter {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue