navi

Obsidian-style interactive graph viewer for org-roam — native window, no Emacs package required.
Log | Files | Refs | README

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() }