emacs.rs (4402B)
1 use std::path::{Path, PathBuf}; 2 use std::process::{Command, Stdio}; 3 use crate::graph::Node; 4 5 static EMACSCLIENT_CANDIDATES: &[&str] = &[ 6 "emacsclient", 7 "/opt/homebrew/bin/emacsclient", 8 "/usr/local/bin/emacsclient", 9 "/usr/bin/emacsclient", 10 "/opt/local/bin/emacsclient", 11 "/Applications/Emacs.app/Contents/MacOS/bin/emacsclient", 12 "/Applications/Emacs.app/Contents/MacOS/emacsclient", 13 "/run/current-system/sw/bin/emacsclient", 14 "/snap/bin/emacsclient", 15 "~/.local/bin/emacsclient", 16 "~/.nix-profile/bin/emacsclient", 17 ]; 18 19 pub struct EmacsClient { 20 pub binary: String, 21 pub server_name: String, 22 } 23 24 impl EmacsClient { 25 pub fn new(cfg_binary: &str, server_name: &str) -> Self { 26 let binary = if !cfg_binary.is_empty() && Path::new(cfg_binary).exists() { 27 cfg_binary.to_string() 28 } else { 29 detect_emacsclient() 30 }; 31 EmacsClient { binary, server_name: server_name.to_string() } 32 } 33 34 pub fn open_node(&self, node: &Node) -> Result<(), String> { 35 if node.file.is_empty() || !Path::new(&node.file).exists() { 36 return Err(format!("File not found: {}", node.file)); 37 } 38 39 let sock = find_emacs_socket(&self.server_name); 40 let mut cmd_args: Vec<String> = Vec::new(); 41 if let Some(s) = &sock { 42 cmd_args.push("--socket-name".into()); 43 cmd_args.push(s.clone()); 44 } 45 cmd_args.push("--no-wait".into()); 46 cmd_args.push("--alternate-editor=".into()); 47 cmd_args.push("--eval".into()); 48 49 let path = node.file.replace('\\', "\\\\").replace('"', "\\\""); 50 let goto = if node.level > 0 && node.pos > 0 { 51 format!(" (goto-char {})", node.pos) 52 } else { 53 String::new() 54 }; 55 let elisp = format!( 56 "(progn (find-file \"{path}\"){goto} (delete-other-windows) (when (display-graphic-p) (raise-frame)))" 57 ); 58 cmd_args.push(elisp); 59 60 Command::new(&self.binary) 61 .args(&cmd_args) 62 .stdout(Stdio::null()) 63 .stderr(Stdio::piped()) 64 .spawn() 65 .map(|_| ()) 66 .map_err(|e| format!("emacsclient failed: {e}")) 67 } 68 } 69 70 fn detect_emacsclient() -> String { 71 for cand in EMACSCLIENT_CANDIDATES { 72 let expanded = if let Some(rest) = cand.strip_prefix("~/") { 73 format!("{}/{}", dirs::home_dir().unwrap_or_default().display(), rest) 74 } else { 75 cand.to_string() 76 }; 77 if Path::new(&expanded).exists() { 78 return expanded; 79 } 80 // Try which 81 if let Ok(out) = Command::new("which").arg(cand).output() { 82 let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); 83 if !s.is_empty() && Path::new(&s).exists() { 84 return s; 85 } 86 } 87 } 88 "emacsclient".to_string() 89 } 90 91 fn find_emacs_socket(server_name: &str) -> Option<String> { 92 for key in &["EMACS_SERVER_SOCKET", "EMACS_SERVER_FILE"] { 93 if let Ok(v) = std::env::var(key) { 94 if Path::new(&v).exists() { 95 return Some(v); 96 } 97 } 98 } 99 100 let uid = unsafe { libc_getuid() }; 101 let names: Vec<&str> = if server_name != "server" { 102 vec![server_name, "server"] 103 } else { 104 vec!["server"] 105 }; 106 107 // XDG_RUNTIME_DIR (Linux) 108 if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") { 109 for name in &names { 110 let p = format!("{}/emacs/{}", xdg, name); 111 if Path::new(&p).exists() { return Some(p); } 112 } 113 } 114 115 // macOS temp dirs 116 let bases: Vec<PathBuf> = { 117 let mut b = Vec::new(); 118 for key in &["TMPDIR", "TMP", "TEMP"] { 119 if let Ok(v) = std::env::var(key) { 120 b.push(PathBuf::from(v)); 121 } 122 } 123 b.push(PathBuf::from("/tmp")); 124 b.push(PathBuf::from("/private/tmp")); 125 b 126 }; 127 128 for base in bases { 129 for name in &names { 130 let p = base.join(format!("emacs{}", uid)).join(name); 131 if p.exists() { return Some(p.to_string_lossy().into_owned()); } 132 } 133 } 134 None 135 } 136 137 #[cfg(unix)] 138 fn libc_getuid() -> u32 { 139 unsafe { libc_uid() } 140 } 141 142 #[cfg(not(unix))] 143 fn libc_getuid() -> u32 { 0 } 144 145 #[cfg(unix)] 146 extern "C" { fn getuid() -> u32; } 147 148 #[cfg(unix)] 149 unsafe fn libc_uid() -> u32 { getuid() }