mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
57728ec1e9
commit
df18c54fe8
20 changed files with 230 additions and 402 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -353,6 +353,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"shared",
|
"shared",
|
||||||
|
"shell-words",
|
||||||
"toml",
|
"toml",
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
@ -1530,6 +1531,12 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ impl App {
|
||||||
|
|
||||||
Event::Open(targets, opener) => {
|
Event::Open(targets, opener) => {
|
||||||
if let Some(opener) = opener {
|
if let Some(opener) = opener {
|
||||||
tasks.file_open_with(&opener, &targets.iter().map(|(f, _)| f).collect::<Vec<_>>());
|
tasks.file_open_with(&opener, &targets.into_iter().map(|(f, _)| f).collect::<Vec<_>>());
|
||||||
} else {
|
} else {
|
||||||
tasks.file_open(&targets);
|
tasks.file_open(&targets);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,6 @@ once_cell = "^1"
|
||||||
ratatui = "^0"
|
ratatui = "^0"
|
||||||
regex = "^1"
|
regex = "^1"
|
||||||
serde = { version = "^1", features = [ "derive" ] }
|
serde = { version = "^1", features = [ "derive" ] }
|
||||||
|
shell-words = "^1"
|
||||||
toml = { version = "^0", features = [ "preserve_order" ] }
|
toml = { version = "^0", features = [ "preserve_order" ] }
|
||||||
xdg = "^2"
|
xdg = "^2"
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ Configure available openers, for example:
|
||||||
```toml
|
```toml
|
||||||
[opener]
|
[opener]
|
||||||
archive = [
|
archive = [
|
||||||
{ exec = "unar $0" },
|
{ exec = "unar $1" },
|
||||||
]
|
]
|
||||||
text = [
|
text = [
|
||||||
{ exec = "nvim $*", block = true },
|
{ exec = "nvim $*", block = true },
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,22 @@ folder = [
|
||||||
{ exec = "vim $*" },
|
{ exec = "vim $*" },
|
||||||
]
|
]
|
||||||
archive = [
|
archive = [
|
||||||
{ exec = "unar $0", display_name = "Extract here" },
|
{ exec = "unar $1", display_name = "Extract here" },
|
||||||
]
|
]
|
||||||
text = [
|
text = [
|
||||||
{ exec = "vim $*", block = true },
|
{ exec = "vim $*", block = true },
|
||||||
]
|
]
|
||||||
image = [
|
image = [
|
||||||
{ exec = "open $*", display_name = "Open" },
|
{ exec = "open $*", display_name = "Open" },
|
||||||
{ exec = "sh -c 'exiftool $0; echo \"\n\nPress enter to exit\"; read'", block = true, display_name = "Show EXIF" },
|
{ exec = "exiftool $1; echo '\n\nPress enter to exit'; read", block = true, display_name = "Show EXIF" },
|
||||||
]
|
]
|
||||||
video = [
|
video = [
|
||||||
{ exec = "mpv $*" },
|
{ exec = "mpv $*" },
|
||||||
{ exec = "sh -c 'mediainfo $0; echo \"\n\nPress enter to exit\"; read'", block = true, display_name = "Show media info" },
|
{ exec = "mediainfo $1; echo '\n\nPress enter to exit'; read", block = true, display_name = "Show media info" },
|
||||||
]
|
]
|
||||||
audio = [
|
audio = [
|
||||||
{ exec = "mpv $*" },
|
{ exec = "mpv $*" },
|
||||||
{ exec = "sh -c 'mediainfo $0; echo \"\n\nPress enter to exit\"; read'", block = true, display_name = "Show media info" },
|
{ exec = "mediainfo $1; echo '\n\nPress enter to exit'; read", block = true, display_name = "Show media info" },
|
||||||
]
|
]
|
||||||
fallback = [
|
fallback = [
|
||||||
{ exec = "open $*", display_name = "Open" },
|
{ exec = "open $*", display_name = "Open" },
|
||||||
|
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
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: ®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::<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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
mod exec;
|
|
||||||
mod item;
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
pub use exec::*;
|
|
||||||
pub use item::*;
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
// 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'"#,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
85
config/src/keymap/exec.rs
Normal file
85
config/src/keymap/exec.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
use std::{collections::BTreeMap, fmt::{self, Debug}};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use serde::{de::{self, Visitor}, Deserializer};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Exec {
|
||||||
|
pub cmd: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub named: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Exec {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
let s = shell_words::split(s)?;
|
||||||
|
if s.is_empty() {
|
||||||
|
bail!("`exec` cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut exec = Self { cmd: s[0].clone(), args: Vec::new(), named: BTreeMap::new() };
|
||||||
|
for arg in s.into_iter().skip(1) {
|
||||||
|
if arg.starts_with("--") {
|
||||||
|
let mut arg = arg.splitn(2, '=');
|
||||||
|
let key = arg.next().unwrap().trim_start_matches('-');
|
||||||
|
let val = arg.next().unwrap_or("").to_string();
|
||||||
|
exec.named.insert(key.to_string(), val);
|
||||||
|
} else {
|
||||||
|
exec.args.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(exec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Exec {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = Vec::with_capacity(self.args.len() + self.named.len() + 1);
|
||||||
|
s.push(self.cmd.clone());
|
||||||
|
s.extend(self.args.iter().cloned());
|
||||||
|
for (key, val) in self.named.iter() {
|
||||||
|
s.push(format!("--{}={}", key, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_words::join(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 exec 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::try_from(value.as_str()).map_err(de::Error::custom)?);
|
||||||
|
}
|
||||||
|
Ok(execs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(vec![Exec::try_from(value).map_err(de::Error::custom)?])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(ExecVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
use super::Key;
|
use super::{Exec, Key};
|
||||||
use crate::{exec::Exec, MERGED_KEYMAP};
|
use crate::MERGED_KEYMAP;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Control {
|
pub struct Control {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
mod exec;
|
||||||
mod key;
|
mod key;
|
||||||
mod keymap;
|
mod keymap;
|
||||||
|
|
||||||
|
pub use exec::*;
|
||||||
pub use key::*;
|
pub use key::*;
|
||||||
pub use keymap::*;
|
pub use keymap::*;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
mod boot;
|
mod boot;
|
||||||
mod exec;
|
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
mod log;
|
mod log;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Opener {
|
pub struct Opener {
|
||||||
pub cmd: String,
|
pub exec: String,
|
||||||
pub args: Vec<String>,
|
|
||||||
pub block: bool,
|
pub block: bool,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub spread: bool,
|
pub spread: bool,
|
||||||
|
|
@ -16,20 +15,59 @@ impl<'de> Deserialize<'de> for Opener {
|
||||||
{
|
{
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Shadow {
|
pub struct Shadow {
|
||||||
pub cmd: String,
|
// TODO: Deprecate this field in v0.1.5
|
||||||
pub args: Vec<String>,
|
pub cmd: Option<String>,
|
||||||
|
// TODO: Deprecate this field in v0.1.5
|
||||||
|
pub args: Option<Vec<String>>,
|
||||||
|
|
||||||
|
pub exec: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub block: bool,
|
pub block: bool,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
#[serde(skip)]
|
|
||||||
pub spread: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let shadow = Shadow::deserialize(deserializer)?;
|
let mut shadow = Shadow::deserialize(deserializer)?;
|
||||||
|
|
||||||
let display_name = if let Some(s) = shadow.display_name { s } else { shadow.cmd.clone() };
|
// -- TODO: Deprecate this in v0.1.5
|
||||||
let spread = shadow.args.contains(&"$*".to_string());
|
if shadow.exec.is_none() {
|
||||||
|
if shadow.cmd.is_none() {
|
||||||
|
return Err(serde::de::Error::missing_field("exec"));
|
||||||
|
}
|
||||||
|
if shadow.args.is_none() {
|
||||||
|
return Err(serde::de::Error::missing_field("args"));
|
||||||
|
}
|
||||||
|
// Replace the $0 to $1, $1 to $2, and so on
|
||||||
|
shadow.args = Some(
|
||||||
|
shadow
|
||||||
|
.args
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
if !s.starts_with('$') {
|
||||||
|
return shell_words::quote(&s).into();
|
||||||
|
}
|
||||||
|
if let Ok(idx) = s[1..].parse::<usize>() {
|
||||||
|
return format!("${}", idx + 1);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
shadow.exec = Some(format!("{} {}", shadow.cmd.unwrap(), shadow.args.unwrap().join(" ")));
|
||||||
|
}
|
||||||
|
let exec = shadow.exec.unwrap();
|
||||||
|
// TODO: Deprecate this in v0.1.5 --
|
||||||
|
|
||||||
Ok(Self { cmd: shadow.cmd, args: shadow.args, block: shadow.block, display_name, spread })
|
if exec.is_empty() {
|
||||||
|
return Err(serde::de::Error::custom("`exec` cannot be empty"));
|
||||||
|
}
|
||||||
|
let display_name = if let Some(s) = shadow.display_name {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
exec.split_whitespace().next().unwrap().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let spread = exec.contains("$*") || exec.contains("$@");
|
||||||
|
Ok(Self { exec, block: shadow.block, display_name, spread })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
core/src/external/mod.rs
vendored
2
core/src/external/mod.rs
vendored
|
|
@ -7,6 +7,7 @@ mod jq;
|
||||||
mod lsar;
|
mod lsar;
|
||||||
mod pdftoppm;
|
mod pdftoppm;
|
||||||
mod rg;
|
mod rg;
|
||||||
|
mod shell;
|
||||||
mod unar;
|
mod unar;
|
||||||
mod zoxide;
|
mod zoxide;
|
||||||
|
|
||||||
|
|
@ -19,5 +20,6 @@ pub use jq::*;
|
||||||
pub use lsar::*;
|
pub use lsar::*;
|
||||||
pub use pdftoppm::*;
|
pub use pdftoppm::*;
|
||||||
pub use rg::*;
|
pub use rg::*;
|
||||||
|
pub use shell::*;
|
||||||
pub use unar::*;
|
pub use unar::*;
|
||||||
pub use zoxide::*;
|
pub use zoxide::*;
|
||||||
|
|
|
||||||
41
core/src/external/shell.rs
vendored
Normal file
41
core/src/external/shell.rs
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use std::{ffi::OsString, process::Stdio};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::process::{Child, Command};
|
||||||
|
|
||||||
|
pub struct ShellOpt {
|
||||||
|
pub cmd: OsString,
|
||||||
|
pub args: Vec<OsString>,
|
||||||
|
pub piped: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shell(opt: ShellOpt) -> Result<Child> {
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Ok(
|
||||||
|
Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(opt.cmd)
|
||||||
|
.arg("") // $0 is the command name
|
||||||
|
.args(opt.args)
|
||||||
|
.stdout(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
|
.stderr(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
Ok(
|
||||||
|
Command::new("cmd")
|
||||||
|
.arg("/C")
|
||||||
|
.arg(opt.cmd)
|
||||||
|
.args(opt.args)
|
||||||
|
.stdout(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
|
.stderr(if opt.piped { Stdio::piped() } else { Stdio::inherit() })
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -229,16 +229,10 @@ impl Manager {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = emit!(Input(InputOpt::top("Shell:").with_highlight()));
|
let result = emit!(Input(InputOpt::top("Shell:").with_highlight()));
|
||||||
|
|
||||||
if let Ok(cmd) = result.await {
|
if let Ok(exec) = result.await {
|
||||||
emit!(Open(
|
emit!(Open(
|
||||||
vec![(cmd.into(), "".to_string())],
|
Default::default(),
|
||||||
Some(Opener {
|
Some(Opener { exec, block, display_name: Default::default(), spread: true })
|
||||||
cmd: "sh".to_string(),
|
|
||||||
args: vec!["-c".to_string(), "$0".to_string()],
|
|
||||||
block,
|
|
||||||
display_name: Default::default(),
|
|
||||||
spread: false,
|
|
||||||
})
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{ffi::{OsStr, OsString}, path::PathBuf, sync::Arc, time::Duration};
|
use std::{ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use async_channel::{Receiver, Sender};
|
use async_channel::{Receiver, Sender};
|
||||||
use config::open::Opener;
|
use config::open::Opener;
|
||||||
|
|
@ -280,23 +280,12 @@ impl Scheduler {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn process_open(&self, opener: &Opener, args: &[impl AsRef<OsStr>]) {
|
pub(super) fn process_open(&self, opener: &Opener, args: &[impl AsRef<OsStr>]) {
|
||||||
let args: Vec<OsString> = opener
|
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.map_while(|a| {
|
|
||||||
if !a.starts_with('$') {
|
|
||||||
return Some(vec![a.into()]);
|
|
||||||
}
|
|
||||||
if a == "$*" {
|
|
||||||
return Some(args.iter().map(Into::into).collect());
|
|
||||||
}
|
|
||||||
a[1..].parse().ok().and_then(|n: usize| args.get(n)).map(|a| vec![a.into()])
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut running = self.running.write();
|
let mut running = self.running.write();
|
||||||
let name = format!("Exec `{} {}`", opener.cmd, args.join(" ".as_ref()).to_string_lossy());
|
let name = format!(
|
||||||
|
"Exec `{}` with `{}`",
|
||||||
|
opener.exec,
|
||||||
|
args.iter().map(|a| a.as_ref()).collect::<Vec<_>>().join(" ".as_ref()).to_string_lossy()
|
||||||
|
);
|
||||||
let id = running.add(name);
|
let id = running.add(name);
|
||||||
|
|
||||||
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
||||||
|
|
@ -313,12 +302,19 @@ impl Scheduler {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let args = args.into_iter().map(|a| a.as_ref().to_os_string()).collect::<Vec<_>>();
|
||||||
tokio::spawn({
|
tokio::spawn({
|
||||||
let process = self.process.clone();
|
let process = self.process.clone();
|
||||||
let opener = opener.clone();
|
let opener = opener.clone();
|
||||||
async move {
|
async move {
|
||||||
process
|
process
|
||||||
.open(ProcessOpOpen { id, cmd: opener.cmd, args, block: opener.block, cancel: cancel_tx })
|
.open(ProcessOpOpen {
|
||||||
|
id,
|
||||||
|
cmd: opener.exec.into(),
|
||||||
|
args,
|
||||||
|
block: opener.block,
|
||||||
|
cancel: cancel_tx,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ impl Tasks {
|
||||||
let mut openers = BTreeMap::new();
|
let mut openers = BTreeMap::new();
|
||||||
for (path, mime) in targets {
|
for (path, mime) in targets {
|
||||||
if let Some(opener) = OPEN.openers(path, mime).and_then(|o| o.first().cloned()) {
|
if let Some(opener) = OPEN.openers(path, mime).and_then(|o| o.first().cloned()) {
|
||||||
openers.entry(opener).or_insert_with(Vec::new).push(path.as_ref());
|
openers.entry(opener).or_insert_with(Vec::new).push(path.as_ref().as_os_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (opener, args) in openers {
|
for (opener, args) in openers {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use std::{ffi::OsString, process::Stdio};
|
use std::ffi::OsString;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, select, sync::{mpsc, oneshot}};
|
use tokio::{io::{AsyncBufReadExt, BufReader}, select, sync::{mpsc, oneshot}};
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::{emit, tasks::TaskOp, BLOCKER};
|
use crate::{emit, external::{self, ShellOpt}, tasks::TaskOp, BLOCKER};
|
||||||
|
|
||||||
pub(crate) struct Process {
|
pub(crate) struct Process {
|
||||||
sch: mpsc::UnboundedSender<TaskOp>,
|
sch: mpsc::UnboundedSender<TaskOp>,
|
||||||
|
|
@ -13,7 +13,7 @@ pub(crate) struct Process {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ProcessOpOpen {
|
pub(crate) struct ProcessOpOpen {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub cmd: String,
|
pub cmd: OsString,
|
||||||
pub args: Vec<OsString>,
|
pub args: Vec<OsString>,
|
||||||
pub block: bool,
|
pub block: bool,
|
||||||
pub cancel: oneshot::Sender<()>,
|
pub cancel: oneshot::Sender<()>,
|
||||||
|
|
@ -33,12 +33,12 @@ impl Process {
|
||||||
let _guard = BLOCKER.acquire().await.unwrap();
|
let _guard = BLOCKER.acquire().await.unwrap();
|
||||||
emit!(Stop(true)).await;
|
emit!(Stop(true)).await;
|
||||||
|
|
||||||
match Command::new(&task.cmd).args(&task.args).kill_on_drop(true).spawn() {
|
match external::shell(ShellOpt { cmd: task.cmd, args: task.args, piped: false }) {
|
||||||
Ok(mut child) => {
|
Ok(mut child) => {
|
||||||
child.wait().await.ok();
|
child.wait().await.ok();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Failed to spawn {}: {e}", task.cmd);
|
trace!("Failed to spawn process: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit!(Stop(false)).await;
|
emit!(Stop(false)).await;
|
||||||
|
|
@ -48,12 +48,7 @@ impl Process {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sch.send(TaskOp::New(task.id, 0))?;
|
self.sch.send(TaskOp::New(task.id, 0))?;
|
||||||
let mut child = Command::new(&task.cmd)
|
let mut child = external::shell(ShellOpt { cmd: task.cmd, args: task.args, piped: true })?;
|
||||||
.args(&task.args)
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.kill_on_drop(true)
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines();
|
let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines();
|
||||||
let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines();
|
let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue