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        vissue_core::process_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        vissue_core::process_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        vissue_core::process_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        vissue_core::process_env::var(HUD_SOCKET_ENV)
62            .ok()
63            .as_deref(),
64        &default_socket_path(),
65        HUD_SOCK,
66    )
67}
68
69/// `{socket}.lock`, the exclusive flock file. Never unlinked by callers.
70pub fn socket_lock_path(socket: &Path) -> PathBuf {
71    append_suffix(socket, ".lock")
72}
73
74/// `{socket}.pid`, the owner pid file.
75pub fn socket_pid_path(socket: &Path) -> PathBuf {
76    append_suffix(socket, ".pid")
77}
78
79/// `name` in the same directory as `socket`.
80pub fn beside_socket(socket: &Path, name: &str) -> PathBuf {
81    match socket.parent() {
82        Some(dir) if !dir.as_os_str().is_empty() => dir.join(name),
83        _ => PathBuf::from(name),
84    }
85}
86
87fn resolve_socket_path(
88    socket: Option<&str>,
89    xdg_runtime: Option<&str>,
90    home: Option<&Path>,
91) -> PathBuf {
92    if let Some(path) = nonempty(socket) {
93        return PathBuf::from(path);
94    }
95    if let Some(runtime) = nonempty(xdg_runtime) {
96        return PathBuf::from(runtime).join("vissue").join(CONTROL_SOCK);
97    }
98    match home {
99        Some(home) => home.join(".vissue").join("run").join(CONTROL_SOCK),
100        None => PathBuf::from(CONTROL_SOCK),
101    }
102}
103
104fn resolve_named_path(override_path: Option<&str>, socket: &Path, name: &str) -> PathBuf {
105    match nonempty(override_path) {
106        Some(path) => PathBuf::from(path),
107        None => beside_socket(socket, name),
108    }
109}
110
111fn nonempty(value: Option<&str>) -> Option<&str> {
112    value.map(str::trim).filter(|s| !s.is_empty())
113}
114
115fn home_dir() -> Option<PathBuf> {
116    std::env::var_os("HOME")
117        .or_else(|| std::env::var_os("USERPROFILE"))
118        .filter(|v| !v.is_empty())
119        .map(PathBuf::from)
120}
121
122fn append_suffix(path: &Path, suffix: &str) -> PathBuf {
123    let mut raw = path.as_os_str().to_os_string();
124    raw.push(suffix);
125    PathBuf::from(raw)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::path::Path;
132
133    #[test]
134    fn env_override_wins() {
135        let path = resolve_socket_path(
136            Some("/tmp/custom.sock"),
137            Some("/run/user/1000"),
138            Some(Path::new("/home/me")),
139        );
140        assert_eq!(path, PathBuf::from("/tmp/custom.sock"));
141    }
142
143    #[test]
144    fn empty_env_falls_through_to_xdg() {
145        let path = resolve_socket_path(
146            Some(""),
147            Some("/run/user/1000"),
148            Some(Path::new("/home/me")),
149        );
150        assert_eq!(path, PathBuf::from("/run/user/1000/vissue/control.sock"));
151    }
152
153    #[test]
154    fn whitespace_env_falls_through_to_xdg() {
155        let path = resolve_socket_path(
156            Some("   "),
157            Some("/run/user/1000"),
158            Some(Path::new("/home/me")),
159        );
160        assert_eq!(path, PathBuf::from("/run/user/1000/vissue/control.sock"));
161    }
162
163    #[test]
164    fn empty_xdg_falls_through_to_home() {
165        let path = resolve_socket_path(None, Some(""), Some(Path::new("/home/me")));
166        assert_eq!(path, PathBuf::from("/home/me/.vissue/run/control.sock"));
167    }
168
169    #[test]
170    fn missing_home_uses_cwd_name() {
171        let path = resolve_socket_path(None, None, None);
172        assert_eq!(path, PathBuf::from("control.sock"));
173    }
174
175    #[test]
176    fn logs_and_hud_socket_sit_beside_the_socket() {
177        let socket = Path::new("/run/user/1000/vissue/control.sock");
178        assert_eq!(
179            beside_socket(socket, CONTROL_LOG),
180            PathBuf::from("/run/user/1000/vissue/control.log")
181        );
182        assert_eq!(
183            beside_socket(socket, HUD_LOG),
184            PathBuf::from("/run/user/1000/vissue/hud.log")
185        );
186        assert_eq!(
187            beside_socket(socket, HUD_SOCK),
188            PathBuf::from("/run/user/1000/vissue/hud.sock")
189        );
190        assert_eq!(
191            socket_lock_path(socket),
192            PathBuf::from("/run/user/1000/vissue/control.sock.lock")
193        );
194        assert_eq!(
195            socket_pid_path(socket),
196            PathBuf::from("/run/user/1000/vissue/control.sock.pid")
197        );
198    }
199
200    #[test]
201    fn named_path_override_wins() {
202        let socket = Path::new("/run/user/1000/vissue/control.sock");
203        assert_eq!(
204            resolve_named_path(Some("/tmp/serve.log"), socket, CONTROL_LOG),
205            PathBuf::from("/tmp/serve.log")
206        );
207        assert_eq!(
208            resolve_named_path(Some(""), socket, CONTROL_LOG),
209            PathBuf::from("/run/user/1000/vissue/control.log")
210        );
211    }
212
213    #[test]
214    fn beside_socket_on_bare_name_uses_cwd() {
215        assert_eq!(
216            beside_socket(Path::new("control.sock"), "control.log"),
217            PathBuf::from("control.log")
218        );
219    }
220
221    #[test]
222    fn default_socket_path_returns_a_path() {
223        let path = default_socket_path();
224        assert!(path.ends_with("control.sock") || path.file_name().is_some());
225        assert_eq!(runtime_dir(), path.parent().unwrap());
226        assert_eq!(control_log_path().file_name().unwrap(), "control.log");
227        assert_eq!(hud_log_path().file_name().unwrap(), "hud.log");
228        assert_eq!(hud_socket_path().file_name().unwrap(), "hud.sock");
229    }
230}