Skip to main content

vissue_control/
path.rs

1//! Control-socket locations and the sibling files next to them.
2
3use std::path::{Path, PathBuf};
4
5/// Override for [`default_socket_path`]. Empty or unset falls through.
6pub const SOCKET_ENV: &str = "VISSUE_CONTROL_SOCKET";
7/// Override for [`control_log_path`]. Empty or unset falls through.
8pub const SERVE_LOG_ENV: &str = "VISSUE_SERVE_LOG";
9/// Override for [`hud_log_path`]. Empty or unset falls through.
10pub const HUD_LOG_ENV: &str = "VISSUE_HUD_LOG";
11/// Override for [`hud_socket_path`]. Empty or unset falls through.
12pub const HUD_SOCKET_ENV: &str = "VISSUE_HUD_SUMMON_SOCKET";
13
14const CONTROL_SOCK: &str = "control.sock";
15const CONTROL_LOG: &str = "control.log";
16const HUD_LOG: &str = "hud.log";
17const HUD_SOCK: &str = "hud.sock";
18
19/// Per-user runtime directory that holds the control socket.
20pub fn runtime_dir() -> PathBuf {
21    match default_socket_path().parent() {
22        Some(dir) if !dir.as_os_str().is_empty() => dir.to_path_buf(),
23        _ => PathBuf::from("."),
24    }
25}
26
27/// Default control socket: env, then `$XDG_RUNTIME_DIR/vissue/control.sock`,
28/// then `~/.vissue/run/control.sock`.
29pub fn default_socket_path() -> PathBuf {
30    resolve_socket_path(
31        std::env::var(SOCKET_ENV).ok().as_deref(),
32        std::env::var("XDG_RUNTIME_DIR").ok().as_deref(),
33        home_dir().as_deref(),
34    )
35}
36
37/// Detached serve log. `VISSUE_SERVE_LOG` wins; otherwise `control.log` next
38/// to the default socket.
39pub fn control_log_path() -> PathBuf {
40    resolve_named_path(
41        std::env::var(SERVE_LOG_ENV).ok().as_deref(),
42        &default_socket_path(),
43        CONTROL_LOG,
44    )
45}
46
47/// HUD stderr log. `VISSUE_HUD_LOG` wins; otherwise `hud.log` next to the
48/// default socket.
49pub fn hud_log_path() -> PathBuf {
50    resolve_named_path(
51        std::env::var(HUD_LOG_ENV).ok().as_deref(),
52        &default_socket_path(),
53        HUD_LOG,
54    )
55}
56
57/// HUD summon socket. `VISSUE_HUD_SUMMON_SOCKET` wins; otherwise `hud.sock`
58/// next to the default socket.
59pub fn hud_socket_path() -> PathBuf {
60    resolve_named_path(
61        std::env::var(HUD_SOCKET_ENV).ok().as_deref(),
62        &default_socket_path(),
63        HUD_SOCK,
64    )
65}
66
67/// `{socket}.lock`, the exclusive flock file. Never unlinked by callers.
68pub fn socket_lock_path(socket: &Path) -> PathBuf {
69    append_suffix(socket, ".lock")
70}
71
72/// `{socket}.pid`, the owner pid file.
73pub fn socket_pid_path(socket: &Path) -> PathBuf {
74    append_suffix(socket, ".pid")
75}
76
77/// `name` in the same directory as `socket`.
78pub fn beside_socket(socket: &Path, name: &str) -> PathBuf {
79    match socket.parent() {
80        Some(dir) if !dir.as_os_str().is_empty() => dir.join(name),
81        _ => PathBuf::from(name),
82    }
83}
84
85fn resolve_socket_path(
86    socket: Option<&str>,
87    xdg_runtime: Option<&str>,
88    home: Option<&Path>,
89) -> PathBuf {
90    if let Some(path) = nonempty(socket) {
91        return PathBuf::from(path);
92    }
93    if let Some(runtime) = nonempty(xdg_runtime) {
94        return PathBuf::from(runtime).join("vissue").join(CONTROL_SOCK);
95    }
96    match home {
97        Some(home) => home.join(".vissue").join("run").join(CONTROL_SOCK),
98        None => PathBuf::from(CONTROL_SOCK),
99    }
100}
101
102fn resolve_named_path(override_path: Option<&str>, socket: &Path, name: &str) -> PathBuf {
103    match nonempty(override_path) {
104        Some(path) => PathBuf::from(path),
105        None => beside_socket(socket, name),
106    }
107}
108
109fn nonempty(value: Option<&str>) -> Option<&str> {
110    value.map(str::trim).filter(|s| !s.is_empty())
111}
112
113fn home_dir() -> Option<PathBuf> {
114    std::env::var_os("HOME")
115        .or_else(|| std::env::var_os("USERPROFILE"))
116        .filter(|v| !v.is_empty())
117        .map(PathBuf::from)
118}
119
120fn append_suffix(path: &Path, suffix: &str) -> PathBuf {
121    let mut raw = path.as_os_str().to_os_string();
122    raw.push(suffix);
123    PathBuf::from(raw)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::path::Path;
130
131    #[test]
132    fn env_override_wins() {
133        let path = resolve_socket_path(
134            Some("/tmp/custom.sock"),
135            Some("/run/user/1000"),
136            Some(Path::new("/home/me")),
137        );
138        assert_eq!(path, PathBuf::from("/tmp/custom.sock"));
139    }
140
141    #[test]
142    fn empty_env_falls_through_to_xdg() {
143        let path = resolve_socket_path(
144            Some(""),
145            Some("/run/user/1000"),
146            Some(Path::new("/home/me")),
147        );
148        assert_eq!(path, PathBuf::from("/run/user/1000/vissue/control.sock"));
149    }
150
151    #[test]
152    fn whitespace_env_falls_through_to_xdg() {
153        let path = resolve_socket_path(
154            Some("   "),
155            Some("/run/user/1000"),
156            Some(Path::new("/home/me")),
157        );
158        assert_eq!(path, PathBuf::from("/run/user/1000/vissue/control.sock"));
159    }
160
161    #[test]
162    fn empty_xdg_falls_through_to_home() {
163        let path = resolve_socket_path(None, Some(""), Some(Path::new("/home/me")));
164        assert_eq!(path, PathBuf::from("/home/me/.vissue/run/control.sock"));
165    }
166
167    #[test]
168    fn missing_home_uses_cwd_name() {
169        let path = resolve_socket_path(None, None, None);
170        assert_eq!(path, PathBuf::from("control.sock"));
171    }
172
173    #[test]
174    fn logs_and_hud_socket_sit_beside_the_socket() {
175        let socket = Path::new("/run/user/1000/vissue/control.sock");
176        assert_eq!(
177            beside_socket(socket, CONTROL_LOG),
178            PathBuf::from("/run/user/1000/vissue/control.log")
179        );
180        assert_eq!(
181            beside_socket(socket, HUD_LOG),
182            PathBuf::from("/run/user/1000/vissue/hud.log")
183        );
184        assert_eq!(
185            beside_socket(socket, HUD_SOCK),
186            PathBuf::from("/run/user/1000/vissue/hud.sock")
187        );
188        assert_eq!(
189            socket_lock_path(socket),
190            PathBuf::from("/run/user/1000/vissue/control.sock.lock")
191        );
192        assert_eq!(
193            socket_pid_path(socket),
194            PathBuf::from("/run/user/1000/vissue/control.sock.pid")
195        );
196    }
197
198    #[test]
199    fn named_path_override_wins() {
200        let socket = Path::new("/run/user/1000/vissue/control.sock");
201        assert_eq!(
202            resolve_named_path(Some("/tmp/serve.log"), socket, CONTROL_LOG),
203            PathBuf::from("/tmp/serve.log")
204        );
205        assert_eq!(
206            resolve_named_path(Some(""), socket, CONTROL_LOG),
207            PathBuf::from("/run/user/1000/vissue/control.log")
208        );
209    }
210
211    #[test]
212    fn beside_socket_on_bare_name_uses_cwd() {
213        assert_eq!(
214            beside_socket(Path::new("control.sock"), "control.log"),
215            PathBuf::from("control.log")
216        );
217    }
218
219    #[test]
220    fn default_socket_path_returns_a_path() {
221        let path = default_socket_path();
222        assert!(path.ends_with("control.sock") || path.file_name().is_some());
223        assert_eq!(runtime_dir(), path.parent().unwrap());
224        assert_eq!(control_log_path().file_name().unwrap(), "control.log");
225        assert_eq!(hud_log_path().file_name().unwrap(), "hud.log");
226        assert_eq!(hud_socket_path().file_name().unwrap(), "hud.sock");
227    }
228}