From 19465f0e9f7932344a585a17165e546832eccb03 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Fri, 11 Aug 2023 13:32:48 +0800 Subject: [PATCH] .. --- Cargo.lock | 33 +++++++ config/Cargo.toml | 1 + config/docs/yazi.md | 6 +- config/preset/yazi.toml | 6 +- config/src/exec/exec.rs | 189 ++++++++++++++++++++++++++++++++++++ config/src/exec/item.rs | 88 +++++++++++++++++ config/src/exec/mod.rs | 6 ++ config/src/exec/tests.rs | 49 ++++++++++ config/src/exec_new.rs | 185 ----------------------------------- config/src/keymap/exec.rs | 85 ---------------- config/src/keymap/keymap.rs | 4 +- config/src/keymap/mod.rs | 2 - config/src/lib.rs | 2 +- cspell.json | 2 +- 14 files changed, 376 insertions(+), 282 deletions(-) create mode 100644 config/src/exec/exec.rs create mode 100644 config/src/exec/item.rs create mode 100644 config/src/exec/mod.rs create mode 100644 config/src/exec/tests.rs delete mode 100644 config/src/exec_new.rs delete mode 100644 config/src/keymap/exec.rs diff --git a/Cargo.lock b/Cargo.lock index 4ca59c32..5e0eb3bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/config/Cargo.toml b/config/Cargo.toml index a2c2e90a..74622901 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -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" diff --git a/config/docs/yazi.md b/config/docs/yazi.md index 4a69db63..0fe3ab0e 100644 --- a/config/docs/yazi.md +++ b/config/docs/yazi.md @@ -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 diff --git a/config/preset/yazi.toml b/config/preset/yazi.toml index a7894a0a..169244ab 100644 --- a/config/preset/yazi.toml +++ b/config/preset/yazi.toml @@ -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" }, diff --git a/config/src/exec/exec.rs b/config/src/exec/exec.rs new file mode 100644 index 00000000..e30db82e --- /dev/null +++ b/config/src/exec/exec.rs @@ -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, +} + +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::>().join("") + } +} + +impl Exec { + pub fn parse(s: &str) -> Vec { + 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, 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 { + let re = Regex::new(r"\$(\d+|\*)").unwrap(); + let mut occurs = BTreeSet::new(); + let mut replace = |s: &mut String| { + *s = re + .replace_all(s, |caps: ®ex::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::>() + .join(" "); + } + + if let Ok(idx) = idx.parse::() { + 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::>(); + + Self { items }.to_string() + } + + pub fn has() {} + + pub fn arg() {} + + pub fn named() {} +} + +impl Exec { + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct ExecVisitor; + + impl<'de> Visitor<'de> for ExecVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a command string, e.g. tab_switch 0") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut execs = Vec::new(); + while let Some(value) = &seq.next_element::()? { + execs.push(Exec::from(value.as_str())); + } + Ok(execs) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(vec![Exec::from(value)]) + } + } + + deserializer.deserialize_any(ExecVisitor) + } +} diff --git a/config/src/exec/item.rs b/config/src/exec/item.rs new file mode 100644 index 00000000..85e663e6 --- /dev/null +++ b/config/src/exec/item.rs @@ -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 + } + } + } +} diff --git a/config/src/exec/mod.rs b/config/src/exec/mod.rs new file mode 100644 index 00000000..69330caf --- /dev/null +++ b/config/src/exec/mod.rs @@ -0,0 +1,6 @@ +mod exec; +mod item; +mod tests; + +pub use exec::*; +pub use item::*; diff --git a/config/src/exec/tests.rs b/config/src/exec/tests.rs new file mode 100644 index 00000000..9177423a --- /dev/null +++ b/config/src/exec/tests.rs @@ -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, 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'"#, + ); +} diff --git a/config/src/exec_new.rs b/config/src/exec_new.rs deleted file mode 100644 index 8bdbb538..00000000 --- a/config/src/exec_new.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::fmt::{Debug, Formatter}; - -#[derive(Debug)] -pub struct ExecNew { - items: Vec, -} - -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, 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::>().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\"" "#); -} diff --git a/config/src/keymap/exec.rs b/config/src/keymap/exec.rs deleted file mode 100644 index 64a79f36..00000000 --- a/config/src/keymap/exec.rs +++ /dev/null @@ -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, - pub named: BTreeMap, -} - -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, D::Error> - where - D: Deserializer<'de>, - { - struct ExecVisitor; - - impl<'de> Visitor<'de> for ExecVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a command string, e.g. tab_switch 0") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: de::SeqAccess<'de>, - { - let mut execs = Vec::new(); - while let Some(value) = &seq.next_element::()? { - execs.push(Exec::from(value.as_str())); - } - Ok(execs) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - Ok(value.split(';').map(Exec::from).collect()) - } - } - - deserializer.deserialize_any(ExecVisitor) - } -} diff --git a/config/src/keymap/keymap.rs b/config/src/keymap/keymap.rs index 3c864368..de5c0f04 100644 --- a/config/src/keymap/keymap.rs +++ b/config/src/keymap/keymap.rs @@ -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 { diff --git a/config/src/keymap/mod.rs b/config/src/keymap/mod.rs index 12bd2809..e0d6b94e 100644 --- a/config/src/keymap/mod.rs +++ b/config/src/keymap/mod.rs @@ -1,7 +1,5 @@ -mod exec; mod key; mod keymap; -pub use exec::*; pub use key::*; pub use keymap::*; diff --git a/config/src/lib.rs b/config/src/lib.rs index 2755319d..f8fd48db 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -3,7 +3,7 @@ use once_cell::sync::Lazy; mod boot; -mod exec_new; +mod exec; pub mod keymap; mod log; pub mod manager; diff --git a/cspell.json b/cspell.json index ebb081b7..cb823e74 100644 --- a/cspell.json +++ b/cspell.json @@ -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"}