truce_utils/shell_sidecar.rs
1//! Sidecar file that pins the `--shell` logic dylib path at install time.
2//!
3//! `cargo truce install --shell` writes one of these per plugin; the
4//! shell binary loaded by the DAW reads it at first hot-reload to find
5//! the matching logic dylib.
6//!
7//! ## Path layout
8//!
9//! ```text
10//! ~/.truce/shell/<crate_name>.path
11//! ```
12//!
13//! `<crate_name>` matches the consuming crate's `CARGO_PKG_NAME`. The
14//! file content is one line: the absolute path to the logic dylib
15//! (e.g. `/Users/me/projects/my-plugin/target/shell/libmy_plugin.dylib`).
16//! No TOML / no JSON — a single path keeps both writer and reader
17//! trivial and parser-free.
18//!
19//! ## Why `~/.truce/` and not the bundle
20//!
21//! Per-bundle sidecars (e.g. `MyPlugin.clap/Contents/.truce-shell`)
22//! were considered, but the runtime read would need `dladdr` /
23//! `GetModuleFileName` to locate the shell binary's own path on disk.
24//! Putting the sidecar at a `crate_name`-keyed home-relative path
25//! sidesteps that: the shell binary already has `env!("CARGO_PKG_NAME")`
26//! baked at compile time, so the read site needs only `$HOME` plus the
27//! crate name. Trade-off: only one shell install per crate at a time,
28//! which is fine — the only reason to install the same plugin twice is
29//! beta/release coexistence, and shell-mode is a dev-loop feature.
30
31use std::path::PathBuf;
32
33/// Resolve `$HOME/.truce/shell/<crate_name>.path` for a given crate.
34/// `crate_name` is the consuming crate's `CARGO_PKG_NAME` — the
35/// reader passes `env!("CARGO_PKG_NAME")` and the writer passes the
36/// resolved plugin's `crate_name` from `truce.toml`. Returns `None`
37/// when neither `HOME` (Unix) nor `USERPROFILE` (Windows) is set —
38/// the caller should fail loud rather than guess a path.
39#[must_use]
40pub fn sidecar_path(crate_name: &str) -> Option<PathBuf> {
41 Some(
42 home_dir()?
43 .join(".truce")
44 .join("shell")
45 .join(format!("{crate_name}.path")),
46 )
47}
48
49fn home_dir() -> Option<PathBuf> {
50 // Unix: HOME. Windows: USERPROFILE. No external `dirs` dep — both
51 // env vars are set by every shell / login session truce supports.
52 if let Ok(home) = std::env::var("HOME")
53 && !home.is_empty()
54 {
55 return Some(PathBuf::from(home));
56 }
57 if let Ok(profile) = std::env::var("USERPROFILE")
58 && !profile.is_empty()
59 {
60 return Some(PathBuf::from(profile));
61 }
62 None
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn sidecar_path_layout() {
71 // Don't mutate $HOME (truce-utils forbids unsafe blocks; the
72 // 2024-edition `std::env::set_var` is unsafe). Instead, accept
73 // both outcomes: when HOME / USERPROFILE is set the path ends
74 // with the expected suffix; otherwise it's None and the writer
75 // surfaces a clear error.
76 if let Some(p) = sidecar_path("my-plugin") {
77 assert!(p.ends_with(".truce/shell/my-plugin.path"));
78 }
79 }
80}