Skip to main content

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