datui_lib/inspector/
external_open.rs1use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ExternalOpen {
16 pub path: PathBuf,
17 pub document: bool,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Program {
24 Wait(Vec<String>),
26 Opener(Vec<String>),
28}
29
30fn opener() -> Vec<String> {
32 if cfg!(windows) {
33 ["cmd", "/c", "start", ""].map(String::from).to_vec()
35 } else if cfg!(target_os = "macos") {
36 vec!["open".to_string()]
37 } else {
38 vec!["xdg-open".to_string()]
39 }
40}
41
42pub fn program_for(document: bool, env: impl Fn(&str) -> Option<String>) -> Program {
47 if document {
48 return Program::Opener(opener());
49 }
50 for name in ["VISUAL", "EDITOR", "PAGER"] {
51 if let Some(value) = env(name).filter(|v| !v.trim().is_empty()) {
52 return Program::Wait(value.split_whitespace().map(String::from).collect());
53 }
54 }
55 if cfg!(windows) {
56 Program::Opener(opener())
57 } else {
58 Program::Wait(vec!["less".to_string()])
59 }
60}
61
62pub fn file_name(field: &str, row: usize, extension: &str) -> String {
65 let safe: String = field
66 .chars()
67 .map(|c| {
68 if c.is_alphanumeric() || matches!(c, '-' | '_' | '.') {
69 c
70 } else {
71 '_'
72 }
73 })
74 .take(60)
75 .collect();
76 let safe = safe.trim_matches('.');
77 let safe = if safe.is_empty() { "value" } else { safe };
78 format!("{safe}-row{row}.{extension}")
79}
80
81pub fn write_value(dir: &Path, name: &str, bytes: &[u8]) -> std::io::Result<PathBuf> {
84 let mut path = dir.join(name);
85 let mut n = 1;
86 while path.exists() {
87 n += 1;
88 path = dir.join(format!("{n}-{name}"));
89 }
90 std::fs::write(&path, bytes)?;
91 if cfg!(unix) {
93 let mut perms = std::fs::metadata(&path)?.permissions();
94 perms.set_readonly(true);
95 std::fs::set_permissions(&path, perms)?;
96 }
97 Ok(path)
98}
99
100pub fn run(program: &Program, path: &Path) -> std::io::Result<()> {
103 let (argv, wait) = match program {
104 Program::Wait(argv) => (argv, true),
105 Program::Opener(argv) => (argv, false),
106 };
107 let Some((first, rest)) = argv.split_first() else {
108 return Err(std::io::Error::other("no program to open the value with"));
109 };
110 if !wait {
111 let mut argv: Vec<&std::ffi::OsStr> = argv.iter().map(|a| a.as_ref()).collect();
112 argv.push(path.as_os_str());
113 return start(&argv);
114 }
115 let status = std::process::Command::new(first)
116 .args(rest)
117 .arg(path)
118 .status()?;
119 if !status.success() {
120 return Err(std::io::Error::other(format!(
121 "{first} exited with {status}"
122 )));
123 }
124 Ok(())
125}
126
127pub fn start<S: AsRef<std::ffi::OsStr>>(argv: &[S]) -> std::io::Result<()> {
130 let Some((first, rest)) = argv.split_first() else {
131 return Err(std::io::Error::other("no program to open with"));
132 };
133 let mut child = std::process::Command::new(first)
134 .args(rest)
135 .stdin(std::process::Stdio::null())
136 .stdout(std::process::Stdio::null())
137 .stderr(std::process::Stdio::null())
138 .spawn()?;
139 std::thread::spawn(move || child.wait());
140 Ok(())
141}
142
143#[cfg(test)]
144mod tests;