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    buf.push("hyprshell.sock");
69    buf
70}
71
72pub fn daemon_running() -> bool {
73    // check if socket exists and socket is open
74    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    // pwa detection
189    if exec.contains("--app-id=") && exec.contains("--profile-directory=") {
190        // "flatpak 'run'" = pwa from browser inside flatpak
191        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            // normal PWA
212            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        // flatpak detection
221    } 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        // AppImage detection
239        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                // skip to the last element
277                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}