hyprshell_core_lib/
util.rs

1use 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
46// from https://github.com/i3/i3/blob/next/i3-sensible-terminal
47// shorted to only the most common ones that I know support -e option
48pub 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    // check if socket exists and socket is open
77    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    // pwa detection
192    if exec.contains("--app-id=") && exec.contains("--profile-directory=") {
193        // "flatpak 'run'" = pwa from browser inside flatpak
194        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            // normal PWA
215            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        // flatpak detection
224    } 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        // AppImage detection
242        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                // skip to the last element
280                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}