use std::process::{Command, Stdio};
pub fn exe(name: &str) -> String {
if cfg!(windows) {
format!("{name}.exe")
} else {
name.to_string()
}
}
pub fn find_on_path(name: &str) -> Option<std::path::PathBuf> {
let path = std::env::var_os("PATH")?;
let names: Vec<String> = if cfg!(windows) {
let ext = std::env::var("PATHEXT").unwrap_or_else(|_| ".EXE;.CMD;.BAT".into());
let mut v = vec![name.to_string()];
v.extend(
ext.split(';')
.filter(|e| !e.is_empty())
.map(|e| format!("{name}{}", e.to_ascii_lowercase())),
);
v
} else {
vec![name.to_string()]
};
std::env::split_paths(&path)
.filter(|d| !d.as_os_str().is_empty())
.flat_map(|d| names.iter().map(move |n| d.join(n)))
.find(|p| p.is_file())
}
pub fn has_display() -> bool {
if cfg!(not(target_os = "linux")) {
return true;
}
std::env::var_os("DISPLAY").is_some() || std::env::var_os("WAYLAND_DISPLAY").is_some()
}
pub fn open_url(url: &str) -> bool {
#[cfg(target_os = "windows")]
{
let safe: String = url
.chars()
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
.collect();
return Command::new("cmd")
.arg("/C")
.raw_arg(format!("start \"\" \"{safe}\""))
.creation_flags(CREATE_NO_WINDOW)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
}
#[cfg(not(target_os = "windows"))]
{
for opener in ["xdg-open", "open"] {
if Command::new(opener)
.arg(url)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.is_ok()
{
return true;
}
}
false
}
}
pub fn open_folder(dir: &std::path::Path) -> bool {
let dir = dunce::simplified(dir);
#[cfg(target_os = "windows")]
{
return Command::new("explorer")
.arg(dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.is_ok();
}
#[cfg(not(target_os = "windows"))]
{
open_url(&dir.to_string_lossy())
}
}
pub fn app_mode_browsers() -> Vec<String> {
if cfg!(target_os = "windows") {
let mut out = vec![];
for base in ["PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA"] {
let Some(dir) = std::env::var_os(base) else {
continue;
};
let dir = std::path::PathBuf::from(dir);
for rel in [
r"Microsoft\Edge\Application\msedge.exe",
r"Google\Chrome\Application\chrome.exe",
r"BraveSoftware\Brave-Browser\Application\brave.exe",
] {
let p = dir.join(rel);
if p.is_file() {
out.push(p.to_string_lossy().into_owned());
}
}
}
out
} else if cfg!(target_os = "macos") {
let mut out = vec![];
for (bundle, exe) in [
("Google Chrome.app", "Google Chrome"),
("Chromium.app", "Chromium"),
("Brave Browser.app", "Brave Browser"),
("Microsoft Edge.app", "Microsoft Edge"),
] {
for app in app_bundles(bundle) {
let p = app.join("Contents/MacOS").join(exe);
if p.is_file() {
out.push(p.to_string_lossy().into_owned());
}
}
}
out
} else {
[
"chromium",
"chromium-browser",
"google-chrome",
"brave-browser",
"microsoft-edge",
]
.iter()
.map(|s| s.to_string())
.collect()
}
}
pub fn app_bundles(name: &str) -> Vec<std::path::PathBuf> {
if cfg!(not(target_os = "macos")) {
return vec![];
}
let mut out = vec![std::path::PathBuf::from("/Applications").join(name)];
if let Some(home) = dirs::home_dir() {
out.push(home.join("Applications").join(name));
}
out.into_iter().filter(|p| p.is_dir()).collect()
}
pub async fn pick_folder(
cancel: &tokio::sync::Notify,
) -> Result<Option<std::path::PathBuf>, String> {
let title = "Open a folder in snyvi";
#[cfg(target_os = "macos")]
let tries: Vec<Vec<String>> = vec![vec![
"osascript".into(),
"-e".into(),
format!("POSIX path of (choose folder with prompt \"{title}\")"),
]];
#[cfg(target_os = "windows")]
let tries: Vec<Vec<String>> = vec![vec![
"powershell".into(),
"-NoProfile".into(),
"-NonInteractive".into(),
"-STA".into(),
"-EncodedCommand".into(),
encoded_command(&PICK_FOLDER_PS1.replace("@TITLE@", &ps_quote(title))),
]];
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let tries: Vec<Vec<String>> = {
if !has_display() {
return Err("there is no display to show a folder dialog on".into());
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/".into());
vec![
vec![
"zenity".into(),
"--file-selection".into(),
"--directory".into(),
format!("--filename={home}/.snyvi-opens-here"),
format!("--title={title}"),
],
vec![
"kdialog".into(),
"--getexistingdirectory".into(),
home,
"--title".into(),
title.into(),
],
vec![
"yad".into(),
"--file".into(),
"--directory".into(),
format!("--title={title}"),
],
]
};
if let Some(answer) = ask_folder(tries, cancel, PICK_TIMEOUT, |_| {}).await {
return answer;
}
Err(if cfg!(target_os = "linux") {
"no folder dialog is installed: zenity or kdialog would give one".into()
} else {
"the desktop's folder dialog could not be started".into()
})
}
#[cfg(target_os = "windows")]
const PICK_FOLDER_PS1: &str = include_str!("pick_folder.ps1");
#[cfg_attr(not(windows), allow(dead_code))]
fn encoded_command(script: &str) -> String {
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect();
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let n = chunk
.iter()
.enumerate()
.fold(0u32, |n, (i, &b)| n | u32::from(b) << (16 - 8 * i));
for i in 0..4 {
out.push(if i <= chunk.len() {
B64[(n >> (18 - 6 * i) & 63) as usize] as char
} else {
'='
});
}
}
out
}
pub const PICK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15 * 60);
async fn ask_folder(
tries: Vec<Vec<String>>,
cancel: &tokio::sync::Notify,
timeout: std::time::Duration,
spawned: impl FnOnce(u32),
) -> Option<Result<Option<std::path::PathBuf>, String>> {
let mut spawned = Some(spawned);
for args in tries {
let Some((program, rest)) = args.split_first() else {
continue;
};
let mut cmd = tokio::process::Command::new(program);
cmd.args(rest)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
#[cfg(target_os = "windows")]
cmd.creation_flags(CREATE_NO_WINDOW);
let Ok(child) = cmd.spawn() else {
continue;
};
if let (Some(f), Some(pid)) = (spawned.take(), child.id()) {
f(pid);
}
let out = tokio::select! {
r = child.wait_with_output() => r,
_ = tokio::time::sleep(timeout) => return Some(Ok(None)),
_ = cancel.notified() => return Some(Ok(None)),
};
let Ok(out) = out else {
return Some(Ok(None));
};
let picked = String::from_utf8_lossy(&out.stdout).trim().to_string();
return Some(Ok(
(out.status.success() && !picked.is_empty()).then(|| picked.into())
));
}
None
}
pub fn shim(name: &str) -> Command {
#[cfg(target_os = "windows")]
{
let mut cmd = Command::new("cmd");
cmd.arg("/C").arg(name);
cmd
}
#[cfg(not(target_os = "windows"))]
Command::new(name)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sound {
Asked,
Declined,
Unsaid,
}
const BURST: std::time::Duration = std::time::Duration::from_secs(2);
fn sound() -> Sound {
static LAST: std::sync::Mutex<Option<std::time::Instant>> = std::sync::Mutex::new(None);
let want = match std::env::var("SNYVI_SOUND").as_deref() {
Ok("1") | Ok("true") | Ok("yes") => Some(true),
Ok("0") | Ok("false") | Ok("no") | Ok("") => Some(false),
_ => None,
};
let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());
sound_for(want, &mut last, std::time::Instant::now())
}
fn sound_for(
want: Option<bool>,
last: &mut Option<std::time::Instant>,
now: std::time::Instant,
) -> Sound {
match want {
None => Sound::Unsaid,
Some(false) => Sound::Declined,
Some(true) => {
if last.is_some_and(|t| now.duration_since(t) < BURST) {
Sound::Declined
} else {
*last = Some(now);
Sound::Asked
}
}
}
}
#[allow(unused_variables)]
fn notify_with(title: &str, body: &str, sound: Sound) {
#[cfg(target_os = "windows")]
{
let script = format!(
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null;\
$x = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent(5);\
$t = $x.GetElementsByTagName('text');\
$t.Item(0).AppendChild($x.CreateTextNode('{}')) > $null;\
$t.Item(1).AppendChild($x.CreateTextNode('{}')) > $null;{}\
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}}\\WindowsPowerShell\\v1.0\\powershell.exe').Show([Windows.UI.Notifications.ToastNotification]::new($x))",
ps_quote(title),
ps_quote(body),
if sound == Sound::Declined {
"$a = $x.CreateElement('audio'); $a.SetAttribute('silent', 'true'); $x.DocumentElement.AppendChild($a) > $null;"
} else {
""
},
);
let _ = Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &script])
.creation_flags(CREATE_NO_WINDOW)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
#[cfg(target_os = "macos")]
{
let script = format!(
"display notification \"{}\" with title \"{}\"{}",
body.replace('\\', "\\\\").replace('"', "\\\""),
title.replace('\\', "\\\\").replace('"', "\\\""),
if sound == Sound::Asked {
" sound name \"Glass\""
} else {
""
},
);
let _ = Command::new("osascript")
.args(["-e", &script])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
{
let _ = Command::new("notify-send")
.args(["-a", "snyvi", "-i", "snyvi"])
.args(sound_hint(sound))
.args([title, body])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
}
pub fn notify_open(title: &str, body: &str, open: impl FnOnce() + Send + 'static) {
let sound = sound();
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
{
if notify_send_takes_actions() {
let mut child = match Command::new("notify-send")
.args([
"-a",
"snyvi",
"-i",
"snyvi",
"-A",
"default=Open",
"-t",
"20000",
])
.args(sound_hint(sound))
.args([title, body])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(c) => c,
Err(_) => {
notify_with(title, body, sound);
return;
}
};
let out = child.stdout.take();
let id = child.id();
if let Some(mut old) = replace_clickable(Some(child)) {
let _ = old.kill();
let _ = old.wait();
}
std::thread::spawn(move || {
use std::io::Read;
let mut said = String::new();
if let Some(mut out) = out {
let _ = out.read_to_string(&mut said);
}
if let Some(mut mine) = take_clickable(id) {
let _ = mine.wait();
}
if said.trim() == "default" {
open();
}
});
return;
}
}
let _ = &open;
notify_with(title, body, sound);
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
fn sound_hint(sound: Sound) -> &'static [&'static str] {
if sound == Sound::Asked {
&["-h", "string:sound-name:message-new-instant"]
} else {
&[]
}
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
static CLICKABLE: std::sync::Mutex<Option<std::process::Child>> = std::sync::Mutex::new(None);
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
fn replace_clickable(next: Option<std::process::Child>) -> Option<std::process::Child> {
let mut slot = CLICKABLE.lock().unwrap_or_else(|e| e.into_inner());
std::mem::replace(&mut slot, next)
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
fn take_clickable(id: u32) -> Option<std::process::Child> {
let mut slot = CLICKABLE.lock().unwrap_or_else(|e| e.into_inner());
if slot.as_ref().map(std::process::Child::id) == Some(id) {
slot.take()
} else {
None
}
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
fn notify_send_takes_actions() -> bool {
static OK: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*OK.get_or_init(|| {
Command::new("notify-send")
.arg("--help")
.output()
.map(|o| {
let text = String::from_utf8_lossy(&o.stdout).into_owned()
+ &String::from_utf8_lossy(&o.stderr);
text.contains("--action")
})
.unwrap_or(false)
})
}
#[cfg(target_os = "windows")]
fn ps_quote(s: &str) -> String {
s.replace('\'', "''").replace(['\n', '\r'], " ")
}
pub fn terminate(pid: u32, force: bool) {
#[cfg(target_os = "windows")]
{
let mut cmd = Command::new("taskkill");
cmd.arg("/PID").arg(pid.to_string());
if force {
cmd.arg("/F");
}
let _ = cmd
.creation_flags(CREATE_NO_WINDOW)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(not(target_os = "windows"))]
{
let _ = Command::new("kill")
.arg(if force { "-KILL" } else { "-TERM" })
.arg(pid.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
pub fn spawn_daemon(exe: &std::path::Path) -> std::io::Result<()> {
#[cfg(not(windows))]
{
if let Some(log) = open_daemon_log(&crate::config::paths().data_dir) {
let err = log.try_clone()?;
let mut cmd = Command::new(exe);
cmd.arg("serve")
.stdin(Stdio::null())
.stdout(Stdio::from(log))
.stderr(Stdio::from(err));
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
cmd.spawn()?;
return Ok(());
}
}
spawn_detached(exe, &["serve"])
}
#[cfg(not(windows))]
const DAEMON_LOG_MAX: u64 = 1 << 20;
pub fn daemon_log(data_dir: &std::path::Path) -> std::path::PathBuf {
data_dir.join("daemon.log")
}
#[cfg(not(windows))]
fn open_daemon_log(data_dir: &std::path::Path) -> Option<std::fs::File> {
let path = daemon_log(data_dir);
std::fs::create_dir_all(data_dir).ok()?;
if std::fs::metadata(&path).is_ok_and(|m| m.len() > DAEMON_LOG_MAX) {
let _ = std::fs::rename(&path, data_dir.join("daemon.log.1"));
}
let mut o = std::fs::OpenOptions::new();
o.create(true).append(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
o.mode(0o600);
}
o.open(&path).ok()
}
pub fn spawn_detached(exe: &std::path::Path, args: &[&str]) -> std::io::Result<()> {
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
CreateProcessW, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS,
PROCESS_INFORMATION, STARTUPINFOW,
};
let mut line: Vec<u16> = vec![b'"' as u16];
line.extend(exe.as_os_str().encode_wide().filter(|c| *c != b'"' as u16));
line.push(b'"' as u16);
for a in args {
line.push(b' ' as u16);
line.extend(a.encode_utf16());
}
line.push(0);
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let started = unsafe {
CreateProcessW(
std::ptr::null(),
line.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW,
std::ptr::null(),
std::ptr::null(),
&si,
&mut pi,
)
};
if started == 0 {
return Err(std::io::Error::last_os_error());
}
unsafe {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
return Ok(());
}
#[cfg(not(windows))]
{
let mut cmd = Command::new(exe);
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
cmd.spawn()?;
Ok(())
}
}
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
pub(crate) const CREATE_NO_WINDOW: u32 = 0x0800_0000;
pub fn release_thread_memory() {
unsafe { libmimalloc_sys::mi_collect(true) }
}
pub fn release_freed_memory() {
#[cfg(all(target_os = "linux", target_env = "gnu"))]
{
extern "C" {
fn malloc_trim(pad: usize) -> std::os::raw::c_int;
}
unsafe {
malloc_trim(0);
}
}
}
#[cfg(test)]
mod tests {
use super::{sound_for, Sound};
#[test]
fn a_burst_sounds_once_and_only_when_asked() {
use std::time::{Duration, Instant};
let t0 = Instant::now();
let mut last = None;
assert_eq!(sound_for(None, &mut last, t0), Sound::Unsaid);
assert_eq!(sound_for(Some(false), &mut last, t0), Sound::Declined);
assert!(
last.is_none(),
"declining or saying nothing spent the burst"
);
assert_eq!(sound_for(Some(true), &mut last, t0), Sound::Asked);
for ms in [1, 500, 1999] {
assert_eq!(
sound_for(Some(true), &mut last, t0 + Duration::from_millis(ms)),
Sound::Declined,
"a second sound {ms} ms into the burst"
);
}
assert_eq!(
sound_for(Some(true), &mut last, t0 + Duration::from_millis(2000)),
Sound::Asked
);
}
}
#[cfg(test)]
mod pick_tests {
use super::ask_folder;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::Notify;
fn stays_open() -> Vec<Vec<String>> {
let argv: &[&str] = if cfg!(windows) {
&["powershell", "-NoProfile", "-Command", "Start-Sleep 600"]
} else {
&["sleep", "600"]
};
vec![argv.iter().map(|s| s.to_string()).collect()]
}
fn answers(path: &str) -> Vec<Vec<String>> {
let argv: Vec<String> = if cfg!(windows) {
vec!["cmd".into(), "/C".into(), format!("echo {path}")]
} else {
vec!["echo".into(), path.into()]
};
vec![argv]
}
fn alive(pid: u32) -> bool {
if cfg!(windows) {
let out = std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
.output()
.expect("tasklist runs");
String::from_utf8_lossy(&out.stdout).contains(&format!("\"{pid}\""))
} else {
std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.status()
.is_ok_and(|s| s.success())
}
}
async fn gone_soon(pid: u32) -> bool {
let until = Instant::now() + Duration::from_secs(5);
while Instant::now() < until {
if !alive(pid) {
return true;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
false
}
#[tokio::test]
async fn a_dialog_nobody_waits_on_is_closed() {
let pid = AtomicU32::new(0);
let cancel = Notify::new();
let asking = ask_folder(stays_open(), &cancel, Duration::from_secs(600), |p| {
pid.store(p, Ordering::SeqCst)
});
let _ = tokio::time::timeout(Duration::from_millis(1500), asking).await;
let pid = pid.load(Ordering::SeqCst);
assert_ne!(pid, 0, "the dialog started");
assert!(
gone_soon(pid).await,
"the dialog's process outlived the asking"
);
}
#[tokio::test]
async fn cancel_closes_the_dialog() {
let pid = AtomicU32::new(0);
let cancel = Notify::new();
let asking = ask_folder(stays_open(), &cancel, Duration::from_secs(600), |p| {
pid.store(p, Ordering::SeqCst)
});
let started = Instant::now();
let (answer, ()) = tokio::join!(asking, async {
tokio::time::sleep(Duration::from_millis(1000)).await;
cancel.notify_waiters();
});
assert!(matches!(answer, Some(Ok(None))), "a cancel is no choice");
assert!(
started.elapsed() < Duration::from_secs(2),
"cancel ends it at once"
);
assert!(gone_soon(pid.load(Ordering::SeqCst)).await);
}
#[tokio::test]
async fn a_dialog_left_open_is_given_up_on() {
let pid = AtomicU32::new(0);
let cancel = Notify::new();
let answer = ask_folder(stays_open(), &cancel, Duration::from_millis(1500), |p| {
pid.store(p, Ordering::SeqCst)
})
.await;
assert!(matches!(answer, Some(Ok(None))));
assert!(gone_soon(pid.load(Ordering::SeqCst)).await);
}
#[tokio::test]
async fn a_cancel_with_no_dialog_open_is_not_kept_for_the_next() {
let cancel = Notify::new();
cancel.notify_waiters();
let picked = if cfg!(windows) {
r"C:\picked"
} else {
"/picked"
};
let answer = ask_folder(answers(picked), &cancel, Duration::from_secs(60), |_| {}).await;
assert_eq!(
answer.unwrap().unwrap().as_deref(),
Some(std::path::Path::new(picked))
);
}
#[tokio::test]
async fn a_dialog_that_is_not_installed_is_the_next_ones_turn() {
let cancel = Notify::new();
let mut tries = vec![vec!["snyvi-no-such-dialog".to_string()]];
tries.extend(answers(if cfg!(windows) { r"C:\b" } else { "/b" }));
let answer = ask_folder(tries, &cancel, Duration::from_secs(60), |_| {}).await;
assert!(matches!(answer, Some(Ok(Some(_)))));
let none = ask_folder(
vec![vec!["snyvi-no-such-dialog".into()]],
&cancel,
Duration::from_secs(60),
|_| {},
)
.await;
assert!(none.is_none());
}
}
#[cfg(test)]
mod encoded_tests {
use super::encoded_command;
#[test]
fn a_script_is_base64_of_its_utf16() {
assert_eq!(encoded_command(""), "");
assert_eq!(encoded_command("a"), "YQA=");
assert_eq!(encoded_command("Hi"), "SABpAA==");
assert_eq!(encoded_command("abc"), "YQBiAGMA");
}
#[cfg(windows)]
#[test]
fn powershell_runs_the_script_and_answers_in_utf8() {
let script = r"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; 'D:\प्रोजेक्ट\café'";
let out = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-EncodedCommand"])
.arg(encoded_command(script))
.output()
.expect("powershell runs");
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
r"D:\प्रोजेक्ट\café"
);
}
#[cfg(windows)]
#[test]
fn the_folder_dialogs_csharp_compiles() {
let ps1 = super::PICK_FOLDER_PS1;
let from = ps1
.find("Add-Type -TypeDefinition @'")
.expect("the C# is there");
let to = ps1[from..].find("\n'@").expect("and ends") + from + 3;
let script = format!(
"$ErrorActionPreference = 'Stop'; {}; [SnyviFolderDialog].Name",
&ps1[from..to]
);
let out = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-EncodedCommand"])
.arg(encoded_command(&script))
.output()
.expect("powershell runs");
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"SnyviFolderDialog",
"{}",
String::from_utf8_lossy(&out.stderr)
);
}
}