use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub fn display_path(path: &Path) -> String {
let raw = path.to_string_lossy();
if let Some(rest) = raw.strip_prefix(r"\\?\UNC\") {
return format!(r"\\{rest}");
}
if let Some(rest) = raw.strip_prefix(r"\\?\") {
return rest.to_string();
}
raw.into_owned()
}
pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
path.canonicalize().map(|c| PathBuf::from(display_path(&c)))
}
pub fn find_executable(command: &str) -> Option<PathBuf> {
let paths = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&paths) {
let direct = dir.join(command);
if direct.is_file() {
return Some(direct);
}
if cfg!(windows) {
for ext in windows_path_extensions() {
let candidate = dir.join(format!("{command}{ext}"));
if candidate.is_file() {
return Some(candidate);
}
}
}
}
None
}
pub fn executable_exists(command: &str) -> bool {
find_executable(command).is_some()
}
fn windows_path_extensions() -> Vec<String> {
std::env::var("PATHEXT")
.unwrap_or_else(|_| ".EXE;.CMD;.BAT;.COM".to_string())
.split(';')
.filter(|ext| !ext.is_empty())
.map(|ext| ext.to_string())
.collect()
}
pub fn default_editor() -> String {
for var in ["VISUAL", "EDITOR"] {
if let Ok(value) = std::env::var(var) {
let trimmed = value.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
}
if cfg!(windows) {
"notepad".to_string()
} else {
"vi".to_string()
}
}
pub fn notify(title: &str, message: &str) -> bool {
let title = sanitize(title);
let message = sanitize(message);
if cfg!(target_os = "macos") {
let script = format!("display notification \"{message}\" with title \"{title}\"");
return spawn("osascript", &["-e", &script]);
}
if cfg!(target_os = "windows") {
let script = format!(
"Add-Type -AssemblyName System.Windows.Forms; \
Add-Type -AssemblyName System.Drawing; \
$n = New-Object System.Windows.Forms.NotifyIcon; \
$n.Icon = [System.Drawing.SystemIcons]::Information; \
$n.Visible = $true; \
$n.ShowBalloonTip(10000, '{title}', '{message}', \
[System.Windows.Forms.ToolTipIcon]::Info); \
Start-Sleep -Seconds 1; $n.Dispose()"
);
return spawn(
"powershell",
&["-NoProfile", "-WindowStyle", "Hidden", "-Command", &script],
);
}
spawn("notify-send", &[title.as_str(), message.as_str()])
}
fn sanitize(input: &str) -> String {
input
.chars()
.map(|c| match c {
'"' | '\'' | '`' => ' ',
'\n' | '\r' => ' ',
other => other,
})
.collect()
}
fn spawn(program: &str, args: &[&str]) -> bool {
Command::new(program)
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
#[cfg(test)]
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn display_path_strips_verbatim_prefix() {
assert_eq!(
display_path(&PathBuf::from(r"\\?\D:\proj\cijira")),
r"D:\proj\cijira"
);
}
#[test]
fn display_path_strips_verbatim_unc_prefix() {
assert_eq!(
display_path(&PathBuf::from(r"\\?\UNC\server\share\x")),
r"\\server\share\x"
);
}
#[test]
fn display_path_passthrough_plain() {
assert_eq!(
display_path(&PathBuf::from("/home/user/proj")),
"/home/user/proj"
);
assert_eq!(display_path(&PathBuf::from(r"D:\proj")), r"D:\proj");
}
#[test]
fn default_editor_prefers_env() {
let _env = lock_env();
let saved_visual = std::env::var("VISUAL").ok();
let saved_editor = std::env::var("EDITOR").ok();
std::env::remove_var("VISUAL");
std::env::set_var("EDITOR", "nano");
assert_eq!(default_editor(), "nano");
std::env::set_var("VISUAL", "code --wait");
assert_eq!(default_editor(), "code --wait");
std::env::remove_var("VISUAL");
std::env::remove_var("EDITOR");
let fallback = default_editor();
if cfg!(windows) {
assert_eq!(fallback, "notepad");
} else {
assert_eq!(fallback, "vi");
}
match saved_visual {
Some(v) => std::env::set_var("VISUAL", v),
None => std::env::remove_var("VISUAL"),
}
match saved_editor {
Some(v) => std::env::set_var("EDITOR", v),
None => std::env::remove_var("EDITOR"),
}
}
}