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.
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/` (the directory the per-crate sidecar
34/// files live in). Returns `None` when neither `HOME` (Unix) nor
35/// `USERPROFILE` (Windows) is set — the caller should fail loud
36/// rather than guess a path.
37#[must_use]
38pub fn shell_dir() -> Option<PathBuf> {
39    let home = home_dir()?;
40    Some(home.join(".truce").join("shell"))
41}
42
43/// Resolve `$HOME/.truce/shell/<crate_name>.path` for a given crate.
44/// `crate_name` is the consuming crate's `CARGO_PKG_NAME` — the
45/// reader passes `env!("CARGO_PKG_NAME")` and the writer passes the
46/// resolved plugin's `crate_name` from `truce.toml`.
47#[must_use]
48pub fn sidecar_path(crate_name: &str) -> Option<PathBuf> {
49    Some(shell_dir()?.join(format!("{crate_name}.path")))
50}
51
52fn home_dir() -> Option<PathBuf> {
53    // Unix: HOME. Windows: USERPROFILE. No external `dirs` dep — both
54    // env vars are set by every shell / login session truce supports.
55    if let Ok(home) = std::env::var("HOME")
56        && !home.is_empty()
57    {
58        return Some(PathBuf::from(home));
59    }
60    if let Ok(profile) = std::env::var("USERPROFILE")
61        && !profile.is_empty()
62    {
63        return Some(PathBuf::from(profile));
64    }
65    None
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn sidecar_path_layout() {
74        // Don't mutate $HOME (truce-utils forbids unsafe blocks; the
75        // 2024-edition `std::env::set_var` is unsafe). Instead, accept
76        // both outcomes: when HOME / USERPROFILE is set the path ends
77        // with the expected suffix; otherwise it's None and the writer
78        // surfaces a clear error.
79        if let Some(p) = sidecar_path("my-plugin") {
80            assert!(p.ends_with(".truce/shell/my-plugin.path"));
81        }
82    }
83}