1use std::path::{Path, PathBuf};
4
5pub const SOCKET_ENV: &str = "VISSUE_CONTROL_SOCKET";
7pub const SERVE_LOG_ENV: &str = "VISSUE_SERVE_LOG";
9pub const HUD_LOG_ENV: &str = "VISSUE_HUD_LOG";
11pub 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
19pub 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
27pub 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
37pub 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
47pub 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
57pub 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
69pub fn socket_lock_path(socket: &Path) -> PathBuf {
71 append_suffix(socket, ".lock")
72}
73
74pub fn socket_pid_path(socket: &Path) -> PathBuf {
76 append_suffix(socket, ".pid")
77}
78
79pub 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}