This commit is contained in:
sxyazi 2023-08-11 13:32:48 +08:00
parent 192e930670
commit dfbe212859
No known key found for this signature in database
14 changed files with 376 additions and 282 deletions

33
Cargo.lock generated
View file

@ -34,6 +34,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8f9420f797f2d9e935edf629310eb938a0d839f984e25327f3c7eed22300c"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -341,6 +350,7 @@ dependencies = [
"glob",
"once_cell",
"ratatui",
"regex",
"serde",
"shared",
"toml",
@ -1384,6 +1394,29 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.4"

View file

@ -14,6 +14,7 @@ futures = "^0"
glob = "^0"
once_cell = "^1"
ratatui = "^0"
regex = "^1"
serde = { version = "^1", features = [ "derive" ] }
toml = { version = "^0", features = [ "preserve_order" ] }
xdg = "^2"

View file

@ -43,9 +43,9 @@ text = [
Available parameters are as follows:
- exec: The command to open the selected files, with the following variables available:
- `"$n"`: The N-th selected file
- `"$*"`: All selected files
- `"foo"`: Literal string to be passed
- `$n`: The N-th selected file
- `$*`: All selected files
- `foo`: Literal string to be passed
- block: Open in a blocking manner. After setting this, Yazi will hide into a secondary screen and display the program on the main screen until it exits. During this time, it can receive I/O signals, which is useful for interactive programs.
## open

View file

@ -21,15 +21,15 @@ text = [
]
image = [
{ exec = "open $*", display_name = "Open" },
{ exec = "exiftool $0", block = true, display_name = "Show EXIF" },
{ exec = "sh -c 'exiftool $0; echo \"\n\nPress enter to exit\"; read'", block = true, display_name = "Show EXIF" },
]
video = [
{ exec = "mpv $*" },
{ exec = "mediainfo $0", block = true, display_name = "Show media info" },
{ exec = "sh -c 'mediainfo $0; echo \"\n\nPress enter to exit\"; read'", block = true, display_name = "Show media info" },
]
audio = [
{ exec = "mpv $*" },
{ exec = "mediainfo $0", block = true, display_name = "Show media info" },
{ exec = "sh -c 'mediainfo $0; echo \"\n\nPress enter to exit\"; read'", block = true, display_name = "Show media info" },
]
fallback = [
{ exec = "open $*", display_name = "Open" },

189
config/src/exec/exec.rs Normal file
View file

@ -0,0 +1,189 @@
use std::{collections::BTreeSet, fmt::{self, Debug}};
use regex::Regex;
use serde::{de::{self, Visitor}, Deserializer};
use super::ExecItem;
#[derive(Clone, Debug)]
pub struct Exec {
items: Vec<ExecItem>,
}
impl From<&str> for Exec {
fn from(s: &str) -> Self { Self { items: Self::parse(s) } }
}
impl ToString for Exec {
fn to_string(&self) -> String {
self.items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join("")
}
}
impl Exec {
pub fn parse(s: &str) -> Vec<ExecItem> {
let mut item = ExecItem::Word(Default::default());
let mut last = b'\0';
let mut esc = 0;
let mut items = vec![];
#[inline]
fn add(items: &mut Vec<ExecItem>, item: ExecItem) {
if !item.is_empty() {
items.push(item);
}
}
for c in s.trim().chars() {
if last == b'\\' && !matches!(c, '\\' | '"' | '\'') {
item.push('\\');
esc = 0;
last = b'\0';
}
match c {
' ' => match item {
ExecItem::Str(ref mut s, ..) => s.push(c),
ExecItem::Word(ref mut s) if last == b'\\' => {
s.push('\\');
s.push(c);
}
_ => {
item.push(c);
add(&mut items, item);
item = ExecItem::Word(Default::default());
}
},
'-' => match item {
ExecItem::Word(ref mut w) => {
if w.is_empty() {
item = ExecItem::Arg(Default::default(), false);
} else {
w.push(c);
}
}
ExecItem::Arg(_, ref mut b) => *b = true,
ExecItem::Str(ref mut s, ..) => s.push(c),
},
'\\' => {
if last == b'\\' {
esc += 1;
last = b'\0';
} else {
last = b'\\';
}
}
'"' | '\'' => {
if last == b'\\' {
esc += 1;
last = b'\0';
}
if matches!(item, ExecItem::Str(_, e) if e == esc) {
item.push(c);
add(&mut items, item);
item = ExecItem::Str(Default::default(), esc);
} else {
add(&mut items, item);
item = ExecItem::Str(c.to_string(), esc);
}
esc = 0;
}
c => {
item.push(c);
}
}
}
add(&mut items, item);
items
}
pub fn build(&self, args: Vec<String>) -> String {
let re = Regex::new(r"\$(\d+|\*)").unwrap();
let mut occurs = BTreeSet::new();
let mut replace = |s: &mut String| {
*s = re
.replace_all(s, |caps: &regex::Captures| {
let idx = caps.get(1).unwrap().as_str();
if idx == "*" {
return args
.iter()
.enumerate()
.filter(|(i, _)| !occurs.contains(i))
.map(|(_, s)| s.as_str())
.collect::<Vec<_>>()
.join(" ");
}
if let Ok(idx) = idx.parse::<usize>() {
if idx < args.len() {
occurs.insert(idx);
return args[idx].to_owned();
}
}
Default::default()
})
.into_owned();
};
let items = self
.items
.iter()
.cloned()
.map(|mut i| {
match i {
ExecItem::Word(ref mut s) => replace(s),
ExecItem::Arg(..) => (),
ExecItem::Str(ref mut s, _) => replace(s),
}
i
})
.collect::<Vec<_>>();
Self { items }.to_string()
}
pub fn has() {}
pub fn arg() {}
pub fn named() {}
}
impl Exec {
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Exec>, D::Error>
where
D: Deserializer<'de>,
{
struct ExecVisitor;
impl<'de> Visitor<'de> for ExecVisitor {
type Value = Vec<Exec>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a command string, e.g. tab_switch 0")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut execs = Vec::new();
while let Some(value) = &seq.next_element::<String>()? {
execs.push(Exec::from(value.as_str()));
}
Ok(execs)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![Exec::from(value)])
}
}
deserializer.deserialize_any(ExecVisitor)
}
}

88
config/src/exec/item.rs Normal file
View file

@ -0,0 +1,88 @@
use std::fmt::{Debug, Formatter};
#[derive(Clone)]
pub enum ExecItem {
Word(String),
Arg(String, bool),
Str(String, usize),
}
impl ExecItem {
#[inline]
pub(super) fn push(&mut self, c: char) {
match self {
ExecItem::Word(s) => s.push(c),
ExecItem::Arg(s, _) => s.push(c),
ExecItem::Str(s, _) => s.push(c),
}
}
#[inline]
pub(super) fn is_empty(&self) -> bool {
match self {
ExecItem::Word(s) => s.is_empty(),
ExecItem::Arg(..) => false,
ExecItem::Str(s, _) => s.is_empty(),
}
}
#[inline]
pub(super) fn slash(&self) -> Option<&str> {
match self {
ExecItem::Str(_, n) => {
if *n == 0 {
return None;
}
let s = "\\\\".repeat(*n);
Some(&s[..s.len() - 1])
}
_ => None,
}
}
}
impl Debug for ExecItem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ExecItem::Word(s) => write!(f, "Word({s})"),
ExecItem::Arg(s, _) => write!(f, "Arg({s})"),
ExecItem::Str(s, n) => write!(f, "Str{n}({s})"),
}
}
}
impl ToString for ExecItem {
fn to_string(&self) -> String {
match self {
ExecItem::Word(s) => s.clone(),
ExecItem::Arg(s, b) => format!("-{}{s}", if *b { "-" } else { "" }),
ExecItem::Str(s, n) => {
let slash = if let Some(s) = self.slash() {
s
} else {
return s.to_owned();
};
if s == "'" || s == "\"" {
return format!("{slash}{s}");
}
let mut s = s.clone();
if let Some(sub) = s.strip_prefix('"') {
s = format!("{slash}\"{sub}");
}
if let Some(sub) = s.strip_suffix('"') {
s = format!("{sub}{slash}\"");
}
if let Some(sub) = s.strip_prefix('\'') {
s = format!("{slash}'{sub}");
}
if let Some(sub) = s.strip_suffix('\'') {
s = format!("{sub}{slash}'");
}
s
}
}
}
}

6
config/src/exec/mod.rs Normal file
View file

@ -0,0 +1,6 @@
mod exec;
mod item;
mod tests;
pub use exec::*;
pub use item::*;

49
config/src/exec/tests.rs Normal file
View file

@ -0,0 +1,49 @@
// cargo test --package config --lib -- exec::tests::build --exact --nocapture
#[test]
fn parse() {
use crate::exec::Exec;
fn assert(a: &str) {
let exec = Exec::parse(a);
println!("{:?}", exec);
let a = a.trim();
let b = Exec::from(a).to_string();
if a != b {
println!("A: {}", a);
println!("B: {}", b);
}
}
assert(r#" echo 123 "foo" 'bar' "#);
assert(r#" sh -c "sh -c \"\";" "#);
assert(r#" aaa - "bbb --opt \"ccc \\\"Meow\\\"\"" "#);
assert(r#" python4 --code 'bash -c "echo \'\\\'\'"'; "#);
assert(r#" sh -c "sh -c \"exiftool $0; echo \\\"\nPress enter to exit: \\\"; read\"" "#);
assert(r#"sh -c 'exiftool $0; echo \"\n\nPress enter to exit\"; read'"#)
}
#[test]
fn build() {
use crate::exec::Exec;
fn assert(s: &str, args: Vec<String>, expected: &str) {
let exec = Exec::parse(s);
println!("{:?}", exec);
let got = Exec::from(s).build(args);
if got != expected {
println!("A: {}", expected);
println!("B: {}", got);
}
}
assert(
r#"sh -c 'exiftool $0 "$1" \'$2\'; echo "\n\nPress enter to exit"; read'"#,
vec!["fo o".into(), "b'a\"r".into(), "b\"a'z".into()],
r#"sh -c 'exiftool foo; echo "\n\nPress enter to exit"; read'"#,
);
}

View file

@ -1,185 +0,0 @@
use std::fmt::{Debug, Formatter};
#[derive(Debug)]
pub struct ExecNew {
items: Vec<ExecItem>,
}
pub enum ExecItem {
Word(String),
Arg(String, bool),
Str(String, usize),
}
impl ExecItem {
#[inline]
fn push(&mut self, c: char) {
match self {
ExecItem::Word(s) => s.push(c),
ExecItem::Arg(s, _) => s.push(c),
ExecItem::Str(s, _) => s.push(c),
}
}
#[inline]
fn is_empty(&self) -> bool {
match self {
ExecItem::Word(s) => s.is_empty(),
ExecItem::Arg(..) => false,
ExecItem::Str(s, _) => s.is_empty(),
}
}
}
impl Debug for ExecItem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ExecItem::Word(s) => write!(f, "Word({s})"),
ExecItem::Arg(s, _) => write!(f, "Arg({s})"),
ExecItem::Str(s, n) => write!(f, "Str{n}({s})"),
}
}
}
impl ToString for ExecItem {
fn to_string(&self) -> String {
match self {
ExecItem::Word(s) => s.clone(),
ExecItem::Arg(s, b) => format!("-{}{s}", if *b { "-" } else { "" }),
ExecItem::Str(s, n) => {
if *n == 0 {
return s.to_string();
}
let rep = "\\\\".repeat(*n);
let rep = rep[..rep.len() - 1].to_string();
if s == "'" || s == "\"" {
return format!("{rep}{s}");
}
let mut s = s.clone();
if let Some(ss) = s.strip_prefix('"') {
s = format!("{rep}\"{ss}");
}
if let Some(ss) = s.strip_suffix('"') {
s = format!("{ss}{rep}\"");
}
if let Some(ss) = s.strip_prefix('\'') {
s = format!("{rep}'{ss}");
}
if let Some(ss) = s.strip_suffix('\'') {
s = format!("{ss}{rep}'");
}
s
}
}
}
}
impl ExecNew {
pub fn parse(s: &str) -> Self {
let mut item = ExecItem::Word(Default::default());
let mut last = b'\0';
let mut esc = 0;
let mut items = vec![];
#[inline]
fn add(items: &mut Vec<ExecItem>, item: ExecItem) {
if !item.is_empty() {
items.push(item);
}
}
for c in s.trim().chars() {
if last == b'\\' && !matches!(c, '\\' | '"' | '\'') {
item.push('\\');
esc = 0;
last = b'\0';
}
match c {
' ' => match item {
ExecItem::Str(ref mut s, ..) => s.push(c),
ExecItem::Word(ref mut s) if last == b'\\' => {
s.push('\\');
s.push(c);
}
_ => {
item.push(c);
add(&mut items, item);
item = ExecItem::Word(Default::default());
}
},
'-' => match item {
ExecItem::Word(ref mut w) => {
if w.is_empty() {
item = ExecItem::Arg(Default::default(), false);
} else {
w.push(c);
}
}
ExecItem::Arg(_, ref mut b) => *b = true,
ExecItem::Str(ref mut s, ..) => s.push(c),
},
'\\' => {
if last == b'\\' {
esc += 1;
last = b'\0';
} else {
last = b'\\';
}
}
'"' | '\'' => {
if last == b'\\' {
esc += 1;
last = b'\0';
}
if matches!(item, ExecItem::Str(_, e) if e == esc) {
item.push(c);
add(&mut items, item);
item = ExecItem::Str(Default::default(), esc);
} else {
add(&mut items, item);
item = ExecItem::Str(c.to_string(), esc);
}
esc = 0;
}
c => {
item.push(c);
}
}
}
add(&mut items, item);
Self { items }
}
}
impl ToString for ExecNew {
fn to_string(&self) -> String {
self.items.iter().map(|i| i.to_string()).collect::<Vec<_>>().join("")
}
}
#[test]
fn test() {
fn assert(a: &str) {
let exec = ExecNew::parse(a);
let a = a.trim();
let b = exec.to_string().trim().to_string();
println!("{:?}", exec);
if a != b {
println!("A: {}", a);
println!("B: {}", b);
}
}
assert(r#" echo 123 "foo" 'bar' "#);
assert(r#" sh -c "sh -c \"\";" "#);
assert(r#" aaa - "bbb --opt \"ccc \\\"Meow\\\"\"" "#);
assert(r#" python4 --code 'bash -c "echo \'\\\'\'"'; "#);
assert(r#" sh -c "sh -c \"exiftool $0; echo \\\"\nPress enter to exit: \\\"; read\"" "#);
}

View file

@ -1,85 +0,0 @@
use std::{collections::BTreeMap, fmt};
use serde::{de::{self, Visitor}, Deserializer};
#[derive(Clone, Debug, Default)]
pub struct Exec {
pub cmd: String,
pub args: Vec<String>,
pub named: BTreeMap<String, String>,
}
impl From<&str> for Exec {
fn from(value: &str) -> Self {
let mut exec = Self::default();
for x in value.split_whitespace() {
if let Some(kv) = x.strip_prefix("--") {
let mut it = kv.splitn(2, '=');
let key = it.next().unwrap();
let value = it.next().unwrap_or("");
exec.named.insert(key.to_string(), value.to_string());
} else if exec.cmd.is_empty() {
exec.cmd = x.to_string();
} else {
exec.args.push(x.to_string());
}
}
exec
}
}
impl ToString for Exec {
fn to_string(&self) -> String {
let mut s = self.cmd.clone();
for arg in &self.args {
s.push(' ');
s.push_str(arg);
}
for (name, value) in &self.named {
s.push_str(" --");
s.push_str(name);
if !value.is_empty() {
s.push('=');
s.push_str(value);
}
}
s
}
}
impl Exec {
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Exec>, D::Error>
where
D: Deserializer<'de>,
{
struct ExecVisitor;
impl<'de> Visitor<'de> for ExecVisitor {
type Value = Vec<Exec>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a command string, e.g. tab_switch 0")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut execs = Vec::new();
while let Some(value) = &seq.next_element::<String>()? {
execs.push(Exec::from(value.as_str()));
}
Ok(execs)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(value.split(';').map(Exec::from).collect())
}
}
deserializer.deserialize_any(ExecVisitor)
}
}

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Deserializer};
use super::{Exec, Key};
use crate::MERGED_KEYMAP;
use super::Key;
use crate::{exec::Exec, MERGED_KEYMAP};
#[derive(Clone, Debug, Deserialize)]
pub struct Control {

View file

@ -1,7 +1,5 @@
mod exec;
mod key;
mod keymap;
pub use exec::*;
pub use key::*;
pub use keymap::*;

View file

@ -3,7 +3,7 @@
use once_cell::sync::Lazy;
mod boot;
mod exec_new;
mod exec;
pub mod keymap;
mod log;
pub mod manager;

View file

@ -1 +1 @@
{"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"],"version":"0.2","language":"en"}
{"language":"en","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","nocapture"],"version":"0.2"}