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 buf.push("hyprshell.sock");
69 buf
70}
71
72pub fn daemon_running() -> bool {
73 let buf = get_daemon_socket_path_buff();
75 if buf.exists() {
76 debug!("Checking if daemon is running");
77 UnixStream::connect(buf).is_ok()
78 } else {
79 debug!("Daemon not running");
80 false
81 }
82}
83
84pub fn check_version(version: anyhow::Result<String>) -> anyhow::Result<()> {
85 if let Ok(version) = version {
86 info!(
87 "Starting hyprshell {} in {} mode on hyprland {}",
88 env!("CARGO_PKG_VERSION"),
89 if cfg!(debug_assertions) {
90 "debug"
91 } else {
92 "release"
93 },
94 version,
95 );
96 let parsed_version =
97 Version::parse(&version).context("Unable to parse hyprland Version")?;
98 if parsed_version.lt(&MIN_VERSION) {
99 Err(anyhow::anyhow!(
100 "hyprland version {} is too old or unknown, please update to at least {}",
101 parsed_version,
102 MIN_VERSION
103 ))
104 } else {
105 Ok(())
106 }
107 } else {
108 Err(anyhow::anyhow!("Unable to get hyprland version"))
109 }
110}
111
112pub fn collect_desktop_files() -> Vec<DirEntry> {
113 let mut res = Vec::new();
114 for dir in find_application_dirs() {
115 if !dir.exists() {
116 continue;
117 }
118 match dir.read_dir() {
119 Ok(dir) => {
120 for entry in dir.flatten() {
121 let path = entry.path();
122 if path.is_file() && path.extension().is_some_and(|e| e == "desktop") {
123 res.push(entry);
124 }
125 }
126 }
127 Err(e) => {
128 warn!("Failed to read dir {dir:?}: {e}");
129 continue;
130 }
131 }
132 }
133 debug!("found {} desktop files", res.len());
134 res
135}
136
137pub fn get_hyprshell_path() -> String {
138 env::current_exe()
139 .expect("Current executable not found")
140 .display()
141 .to_string()
142 .replace("(deleted)", "")
143}
144
145pub fn get_hyprctl_path() -> String {
146 let path =
147 env::var_os("PATH").unwrap_or_else(|| OsString::from("/usr/bin:/bin:/usr/local/bin"));
148
149 split_paths(&path)
150 .find_map(|dir| {
151 let path = dir.join("hyprctl");
152 if path.exists() {
153 Some(path.display().to_string())
154 } else {
155 None
156 }
157 })
158 .unwrap_or_else(|| String::from("hyprctl"))
159}
160
161pub fn generate_socat(echo: &str) -> String {
162 format!(r#"{} socat '{}'"#, get_hyprshell_path(), echo)
163}
164
165pub fn generate_socat_and_activate_submap(echo: &str, submap: &str) -> String {
166 format!(
167 r#"{} socat --submap {} '{}'"#,
168 get_hyprshell_path(),
169 submap,
170 echo
171 )
172}
173
174#[derive(Debug, Clone)]
175pub enum ExecType {
176 Flatpak(Box<str>, Box<str>),
177 PWA(Box<str>, Box<str>),
178 FlatpakPWA(Box<str>, Box<str>),
179 Absolute(Box<str>, Box<str>),
180 AppImage(Box<str>, Box<str>),
181 Relative(Box<str>),
182}
183
184const UNKNOWN_EXEC: &str = "unknown";
185
186pub fn analyse_exec(exec: &str) -> ExecType {
187 let exec_trim = exec.replace("'", "").replace("\"", "");
188 if exec.contains("--app-id=") && exec.contains("--profile-directory=") {
190 if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
192 let browser_exec_in_flatpak = exec_trim
193 .split_whitespace()
194 .find(|s| s.contains("--command="))
195 .and_then(|s| {
196 s.split('=')
197 .next_back()
198 .and_then(|s| s.split('/').next_back())
199 })
200 .unwrap_or(UNKNOWN_EXEC);
201 let flatpak_identifier = exec_trim
202 .split_whitespace()
203 .skip(2)
204 .find(|arg| !arg.starts_with("--"))
205 .unwrap_or(UNKNOWN_EXEC);
206 ExecType::FlatpakPWA(
207 Box::from(flatpak_identifier),
208 Box::from(browser_exec_in_flatpak),
209 )
210 } else {
211 let browser_exec = exec
213 .split_whitespace()
214 .next()
215 .and_then(|s| s.split('/').next_back())
216 .unwrap_or(UNKNOWN_EXEC);
217 let browser_full_exec = exec.split_whitespace().next().unwrap_or(UNKNOWN_EXEC);
218 ExecType::PWA(Box::from(browser_exec), Box::from(browser_full_exec))
219 }
220 } else if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
222 let command_in_flatpak = exec_trim
223 .split_whitespace()
224 .find(|s| s.contains("--command="))
225 .and_then(|s| {
226 s.split('=')
227 .next_back()
228 .and_then(|s| s.split('/').next_back())
229 })
230 .unwrap_or(UNKNOWN_EXEC);
231 let flatpak_identifier = exec_trim
232 .split_whitespace()
233 .skip(2)
234 .find(|arg| !arg.starts_with("--"))
235 .unwrap_or(UNKNOWN_EXEC);
236 ExecType::Flatpak(Box::from(flatpak_identifier), Box::from(command_in_flatpak))
237 } else if exec_trim.contains(".AppImage") {
238 let appimage_name = exec_trim
240 .split_whitespace()
241 .next()
242 .and_then(|s| s.split('/').next_back())
243 .and_then(|s| s.split('_').next())
244 .unwrap_or(UNKNOWN_EXEC);
245 ExecType::AppImage(Box::from(appimage_name), Box::from(exec))
246 } else if exec_trim.starts_with("/") {
247 let exec_name = exec_trim
248 .split_whitespace()
249 .next()
250 .and_then(|s| s.split('/').next_back())
251 .unwrap_or(UNKNOWN_EXEC);
252 ExecType::Absolute(Box::from(exec_name), Box::from(exec))
253 } else {
254 ExecType::Relative(Box::from(exec_trim))
255 }
256}
257
258pub trait GetFirstOrLast: Iterator + Sized {
259 fn get_first_or_last(self, last: bool) -> Option<Self::Item>;
260}
261impl<I: Iterator> GetFirstOrLast for I {
262 fn get_first_or_last(mut self, last: bool) -> Option<Self::Item> {
263 if last { self.last() } else { self.next() }
264 }
265}
266
267pub trait GetNextOrPrev: Iterator + Sized {
268 fn get_next_or_prev(self, last: bool, len: usize) -> Option<Self::Item>;
269}
270impl<I: Iterator> GetNextOrPrev for I {
271 fn get_next_or_prev(mut self, last: bool, len: usize) -> Option<Self::Item> {
272 if last {
273 if len == 0 {
274 None
275 } else {
276 self.nth(len - 1)
278 }
279 } else {
280 self.next()
281 }
282 }
283}
284
285pub trait RevIf<'a>: Iterator + Sized + 'a {
286 fn rev_if(self, cond: bool) -> Box<dyn Iterator<Item = Self::Item> + 'a>;
287}
288
289impl<'a, I: DoubleEndedIterator + 'a> RevIf<'a> for I {
290 fn rev_if(self, cond: bool) -> Box<dyn Iterator<Item = Self::Item> + 'a> {
291 if cond {
292 Box::new(self.rev())
293 } else {
294 Box::new(self)
295 }
296 }
297}