From 9d4ba510dfd921aed1efbd488c5c0a14761dfd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Thu, 19 Oct 2023 10:48:05 +0800 Subject: [PATCH] feat: cross-system opener rule support (#289) --- app/src/executor.rs | 2 +- config/preset/yazi.toml | 71 +++++++++++++++--------------- config/src/open/open.rs | 12 ++++-- config/src/open/opener.rs | 72 ++++++++++++++++++++++++------- config/src/preview/preview.rs | 8 ++-- core/src/manager/commands/open.rs | 2 +- core/src/tab/commands/shell.rs | 9 +++- core/src/tab/tab.rs | 3 -- 8 files changed, 112 insertions(+), 67 deletions(-) diff --git a/app/src/executor.rs b/app/src/executor.rs index d875ed6f..143a66c0 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -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(), diff --git a/config/preset/yazi.toml b/config/preset/yazi.toml index b650e1bc..601ad845 100644 --- a/config/preset/yazi.toml +++ b/config/preset/yazi.toml @@ -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] diff --git a/config/src/open/open.rs b/config/src/open/open.rs index 1345492d..11a04f45 100644 --- a/config/src/open/open.rs +++ b/config/src/open/open.rs @@ -9,8 +9,8 @@ use crate::{open::OpenRule, MERGED_YAZI}; #[derive(Debug)] pub struct Open { - openers: BTreeMap>, rules: Vec, + openers: BTreeMap>, } impl Default for Open { @@ -23,8 +23,8 @@ impl Open { P: AsRef, M: AsRef, { + 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::>())) + .collect(); + Ok(Self { rules: outer.open.rules, openers }) } } diff --git a/config/src/open/opener.rs b/config/src/open/opener.rs index e20a5fe5..8b00d8d5 100644 --- a/config/src/open/opener.rs +++ b/config/src/open/opener.rs @@ -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, + pub spread: bool, +} + +impl Opener { + pub fn take(mut self) -> Option { + 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, + orphan: bool, + desc: Option, + #[serde(rename = "for")] + for_: Option, + + // TODO: remove this when v1.0.5 is released -- + display_name: Option, + // 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, + }) } } diff --git a/config/src/preview/preview.rs b/config/src/preview/preview.rs index 7dfdfad6..8d3818f8 100644 --- a/config/src/preview/preview.rs +++ b/config/src/preview/preview.rs @@ -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, + cache_dir: Option, } let preview = toml::from_str::(&MERGED_YAZI).unwrap().preview; diff --git a/core/src/manager/commands/open.rs b/core/src/manager/commands/open.rs index 516a3739..e1c7aa77 100644 --- a/core/src/manager/commands/open.rs +++ b/core/src/manager/commands/open.rs @@ -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()))); diff --git a/core/src/tab/commands/shell.rs b/core/src/tab/commands/shell.rs index 9a86991c..ffcc9a7b 100644 --- a/core/src/tab/commands/shell.rs +++ b/core/src/tab/commands/shell.rs @@ -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 + }) )); }); diff --git a/core/src/tab/tab.rs b/core/src/tab/tab.rs index b4e19b2f..b94e0884 100644 --- a/core/src/tab/tab.rs +++ b/core/src/tab/tab.rs @@ -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 {