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 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
37pub 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
47pub 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
57pub 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
67pub fn socket_lock_path(socket: &Path) -> PathBuf {
69 append_suffix(socket, ".lock")
70}
71
72pub fn socket_pid_path(socket: &Path) -> PathBuf {
74 append_suffix(socket, ".pid")
75}
76
77pub 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}