hyprshell_core_lib/
util.rs1use crate::find_application_dirs;
2use anyhow::Context;
3use semver::Version;
4use std::env::split_paths;
5use std::ffi::OsString;
6use std::fs::DirEntry;
7use std::os::unix::net::UnixStream;
8use std::path::PathBuf;
9use std::{env, fmt};
10use tracing::{debug, info, warn};
11
12pub const MIN_VERSION: Version = Version::new(0, 42, 0);
13
14pub const OVERVIEW_NAMESPACE: &str = "hyprshell_overview";
15pub const LAUNCHER_NAMESPACE: &str = "hyprshell_launcher";
16
17pub trait Warn<A> {
18 fn warn(self, msg: &str) -> Option<A>;
19}
20
21impl<A> Warn<A> for Option<A> {
22 fn warn(self, msg: &str) -> Option<A> {
23 match self {
24 Some(o) => Some(o),
25 None => {
26 warn!("{}", msg);
27 None
28 }
29 }
30 }
31}
32
33impl<A, E: fmt::Debug + fmt::Display> Warn<A> for Result<A, E> {
34 fn warn(self, msg: &str) -> Option<A> {
35 match self {
36 Ok(o) => Some(o),
37 Err(e) => {
38 warn!("{}: {}", msg, e);
39 debug!("{e:?}");
40 None
41 }
42 }
43 }
44}
45
46pub const TERMINALS: [&str; 9] = [
49 "alacritty",
50 "kitty",
51 "wezterm",
52 "foot",
53 "qterminal",
54 "lilyterm",
55 "tilix",
56 "terminix",
57 "konsole",
58];
59
60pub fn get_daemon_socket_path_buff() -> PathBuf {
61 let mut buf = if let Some(runtime_path) = env::var_os("XDG_RUNTIME_DIR") {
62 std::path::PathBuf::from(runtime_path)
63 } else if let Ok(uid) = env::var("UID") {
64 std::path::PathBuf::from("/run/user/".to_owned() + &uid)
65 } else {
66 std::path::PathBuf::from("/tmp")
67 };
68 #[cfg(debug_assertions)]
69 buf.push("hyprshell.debug.sock");
70 #[cfg(not(debug_assertions))]
71 buf.push("hyprshell.sock");
72 buf
73}
74
75pub fn daemon_running() -> bool {
76 let buf = get_daemon_socket_path_buff();
78 if buf.exists() {
79 debug!("Checking if daemon is running");
80 UnixStream::connect(buf).is_ok()
81 } else {
82 debug!("Daemon not running");
83 false
84 }
85}
86
87pub fn check_version(version: anyhow::Result<String>) -> anyhow::Result<()> {
88 if let Ok(version) = version {
89 info!(
90 "Starting hyprshell {} in {} mode on hyprland {}",
91 env!("CARGO_PKG_VERSION"),
92 if cfg!(debug_assertions) {
93 "debug"
94 } else {
95 "release"
96 },
97 version,
98 );
99 let parsed_version =
100 Version::parse(&version).context("Unable to parse hyprland Version")?;
101 if parsed_version.lt(&MIN_VERSION) {
102 Err(anyhow::anyhow!(
103 "hyprland version {} is too old or unknown, please update to at least {}",
104 parsed_version,
105 MIN_VERSION
106 ))
107 } else {
108 Ok(())
109 }
110 } else {
111 Err(anyhow::anyhow!("Unable to get hyprland version"))
112 }
113}
114
115pub fn collect_desktop_files() -> Vec<DirEntry> {
116 let mut res = Vec::new();
117 for dir in find_application_dirs() {
118 if !dir.exists() {
119 continue;
120 }
121 match dir.read_dir() {
122 Ok(dir) => {
123 for entry in dir.flatten() {
124 let path = entry.path();
125 if path.is_file() && path.extension().is_some_and(|e| e == "desktop") {
126 res.push(entry);
127 }
128 }
129 }
130 Err(e) => {
131 warn!("Failed to read dir {dir:?}: {e}");
132 continue;
133 }
134 }
135 }
136 debug!("found {} desktop files", res.len());
137 res
138}
139
140pub fn get_hyprshell_path() -> String {
141 env::current_exe()
142 .expect("Current executable not found")
143 .display()
144 .to_string()
145 .replace("(deleted)", "")
146}
147
148pub fn get_hyprctl_path() -> String {
149 let path =
150 env::var_os("PATH").unwrap_or_else(|| OsString::from("/usr/bin:/bin:/usr/local/bin"));
151
152 split_paths(&path)
153 .find_map(|dir| {
154 let path = dir.join("hyprctl");
155 if path.exists() {
156 Some(path.display().to_string())
157 } else {
158 None
159 }
160 })
161 .unwrap_or_else(|| String::from("hyprctl"))
162}
163
164pub fn generate_socat(echo: &str) -> String {
165 format!(r#"{} socat '{}'"#, get_hyprshell_path(), echo)
166}
167
168pub fn generate_socat_and_activate_submap(echo: &str, submap: &str) -> String {
169 format!(
170 r#"{} socat --submap {} '{}'"#,
171 get_hyprshell_path(),
172 submap,
173 echo
174 )
175}
176
177#[derive(Debug, Clone)]
178pub enum ExecType {
179 Flatpak(Box<str>, Box<str>),
180 PWA(Box<str>, Box<str>),
181 FlatpakPWA(Box<str>, Box<str>),
182 Absolute(Box<str>, Box<str>),
183 AppImage(Box<str>, Box<str>),
184 Relative(Box<str>),
185}
186
187const UNKNOWN_EXEC: &str = "unknown";
188
189pub fn analyse_exec(exec: &str) -> ExecType {
190 let exec_trim = exec.replace("'", "").replace("\"", "");
191 if exec.contains("--app-id=") && exec.contains("--profile-directory=") {
193 if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
195 let browser_exec_in_flatpak = exec_trim
196 .split_whitespace()
197 .find(|s| s.contains("--command="))
198 .and_then(|s| {
199 s.split('=')
200 .next_back()
201 .and_then(|s| s.split('/').next_back())
202 })
203 .unwrap_or(UNKNOWN_EXEC);
204 let flatpak_identifier = exec_trim
205 .split_whitespace()
206 .skip(2)
207 .find(|arg| !arg.starts_with("--"))
208 .unwrap_or(UNKNOWN_EXEC);
209 ExecType::FlatpakPWA(
210 Box::from(flatpak_identifier),
211 Box::from(browser_exec_in_flatpak),
212 )
213 } else {
214 let browser_exec = exec
216 .split_whitespace()
217 .next()
218 .and_then(|s| s.split('/').next_back())
219 .unwrap_or(UNKNOWN_EXEC);
220 let browser_full_exec = exec.split_whitespace().next().unwrap_or(UNKNOWN_EXEC);
221 ExecType::PWA(Box::from(browser_exec), Box::from(browser_full_exec))
222 }
223 } else if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
225 let command_in_flatpak = exec_trim
226 .split_whitespace()
227 .find(|s| s.contains("--command="))
228 .and_then(|s| {
229 s.split('=')
230 .next_back()
231 .and_then(|s| s.split('/').next_back())
232 })
233 .unwrap_or(UNKNOWN_EXEC);
234 let flatpak_identifier = exec_trim
235 .split_whitespace()
236 .skip(2)
237 .find(|arg| !arg.starts_with("--"))
238 .unwrap_or(UNKNOWN_EXEC);
239 ExecType::Flatpak(Box::from(flatpak_identifier), Box::from(command_in_flatpak))
240 } else if exec_trim.contains(".AppImage") {
241 let appimage_name = exec_trim
243 .split_whitespace()
244 .next()
245 .and_then(|s| s.split('/').next_back())
246 .and_then(|s| s.split('_').next())
247 .unwrap_or(UNKNOWN_EXEC);
248 ExecType::AppImage(Box::from(appimage_name), Box::from(exec))
249 } else if exec_trim.starts_with("/") {
250 let exec_name = exec_trim
251 .split_whitespace()
252 .next()
253 .and_then(|s| s.split('/').next_back())
254 .unwrap_or(UNKNOWN_EXEC);
255 ExecType::Absolute(Box::from(exec_name), Box::from(exec))
256 } else {
257 ExecType::Relative(Box::from(exec_trim))
258 }
259}
260
261pub trait GetFirstOrLast: Iterator + Sized {
262 fn get_first_or_last(self, last: bool) -> Option<Self::Item>;
263}
264impl<I: Iterator> GetFirstOrLast for I {
265 fn get_first_or_last(mut self, last: bool) -> Option<Self::Item> {
266 if last { self.last() } else { self.next() }
267 }
268}
269
270pub trait GetNextOrPrev: Iterator + Sized {
271 fn get_next_or_prev(self, last: bool, len: usize) -> Option<Self::Item>;
272}
273impl<I: Iterator> GetNextOrPrev for I {
274 fn get_next_or_prev(mut self, last: bool, len: usize) -> Option<Self::Item> {
275 if last {
276 if len == 0 {
277 None
278 } else {
279 self.nth(len - 1)
281 }
282 } else {
283 self.next()
284 }
285 }
286}
287
288pub trait RevIf<'a>: Iterator + Sized + 'a {
289 fn rev_if(self, cond: bool) -> Box<dyn Iterator<Item = Self::Item> + 'a>;
290}
291
292impl<'a, I: DoubleEndedIterator + 'a> RevIf<'a> for I {
293 fn rev_if(self, cond: bool) -> Box<dyn Iterator<Item = Self::Item> + 'a> {
294 if cond {
295 Box::new(self.rev())
296 } else {
297 Box::new(self)
298 }
299 }
300}