#[cfg(all(unix, not(target_os = "macos")))]
use std::path::Path;
#[cfg(all(unix, not(target_os = "macos")))]
pub(super) fn expand_exec_template(exec: &str, target: &Path) -> Option<(String, Vec<String>)> {
let target_str = target.to_str()?;
let tokens = tokenize_exec(exec);
let mut expanded: Vec<String> = Vec::new();
for token in tokens {
match token.as_str() {
"%i" | "%c" | "%k" => {}
"%f" | "%F" | "%u" | "%U" => expanded.push(target_str.to_string()),
other => {
let replaced = other
.replace("%f", target_str)
.replace("%F", target_str)
.replace("%u", target_str)
.replace("%U", target_str)
.replace("%i", "")
.replace("%c", "")
.replace("%k", "");
let clean = strip_unknown_field_codes(&replaced);
if !clean.is_empty() {
expanded.push(clean);
}
}
}
}
if expanded.is_empty() {
return None;
}
let program = expanded.remove(0);
Some((program, expanded))
}
#[cfg(all(unix, not(target_os = "macos")))]
fn strip_unknown_field_codes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
match chars.peek() {
Some('%') => {
chars.next();
result.push('%');
}
Some(_) => {
chars.next(); }
None => {} }
} else {
result.push(ch);
}
}
result
}
#[cfg(any(target_os = "macos", all(unix, not(target_os = "macos"))))]
pub(super) fn tokenize_exec(exec: &str) -> Vec<String> {
let mut tokens: Vec<String> = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut chars = exec.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'"' => in_quotes = !in_quotes,
'\\' => {
if let Some(next) = chars.next() {
current.push(next);
}
}
' ' | '\t' if !in_quotes => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
#[cfg(all(test, unix, not(target_os = "macos")))]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn expand_exec_template_supports_percent_f_and_percent_u() {
let path = Path::new("/home/user/doc.txt");
let (prog, args) = expand_exec_template("gedit %f", path).expect("should expand");
assert_eq!(prog, "gedit");
assert_eq!(args, vec!["/home/user/doc.txt"]);
let (prog, args) = expand_exec_template("vlc %u", path).expect("should expand");
assert_eq!(prog, "vlc");
assert_eq!(args, vec!["/home/user/doc.txt"]);
}
#[test]
fn expand_exec_template_supports_uppercase_percent_f_and_percent_u() {
let path = Path::new("/tmp/file.png");
let (prog, args) = expand_exec_template("eog %F", path).expect("should expand");
assert_eq!(prog, "eog");
assert_eq!(args, vec!["/tmp/file.png"]);
let (prog, args) = expand_exec_template("vlc %U", path).expect("should expand");
assert_eq!(prog, "vlc");
assert_eq!(args, vec!["/tmp/file.png"]);
}
#[test]
fn expand_exec_template_strips_percent_i_percent_c_percent_k() {
let path = Path::new("/tmp/x.txt");
let (prog, args) = expand_exec_template("nano %i %c %k %f", path).expect("should expand");
assert_eq!(prog, "nano");
assert_eq!(args, vec!["/tmp/x.txt"]);
}
#[test]
fn expand_exec_template_handles_embedded_placeholder() {
let path = Path::new("/tmp/image.png");
let (prog, args) =
expand_exec_template("viewer --file=%f --quality=90", path).expect("should expand");
assert_eq!(prog, "viewer");
assert_eq!(args, vec!["--file=/tmp/image.png", "--quality=90"]);
}
#[test]
fn expand_exec_template_handles_quoted_program() {
let path = Path::new("/tmp/doc.txt");
let (prog, args) = expand_exec_template(r#""my editor" %f"#, path).expect("should expand");
assert_eq!(prog, "my editor");
assert_eq!(args, vec!["/tmp/doc.txt"]);
}
#[test]
fn expand_exec_template_returns_none_for_empty_after_strip() {
let path = Path::new("/tmp/x");
let result = expand_exec_template("%i %c %k", path);
assert!(result.is_none());
}
#[test]
fn expand_exec_template_drops_unknown_placeholders() {
let path = Path::new("/tmp/doc.txt");
let (prog, args) =
expand_exec_template("app %d %n %f", path).expect("should expand with file arg");
assert_eq!(prog, "app");
assert_eq!(args, vec!["/tmp/doc.txt"]);
}
#[test]
fn expand_exec_template_handles_embedded_unknown_placeholder() {
let path = Path::new("/tmp/img.png");
let (prog, args) = expand_exec_template("viewer --opt=%v %f", path).expect("should expand");
assert_eq!(prog, "viewer");
assert_eq!(args, vec!["--opt=", "/tmp/img.png"]);
}
#[test]
fn expand_exec_template_converts_double_percent_to_literal() {
let path = Path::new("/tmp/file");
let (prog, args) =
expand_exec_template("app --label=100%% %f", path).expect("should expand");
assert_eq!(prog, "app");
assert_eq!(args, vec!["--label=100%", "/tmp/file"]);
}
}