hyprshell_core_lib/
util.rs

1use crate::find_application_dirs;
2use anyhow::Context;
3use semver::Version;
4use std::fs::DirEntry;
5use std::os::unix::net::UnixStream;
6use std::path::PathBuf;
7use std::{env, fmt};
8use tracing::{debug, info, warn};
9
10pub const MIN_VERSION: Version = Version::new(0, 42, 0);
11
12pub const OVERVIEW_NAMESPACE: &str = "hyprshell_overview";
13pub const LAUNCHER_NAMESPACE: &str = "hyprshell_launcher";
14
15pub trait Warn<A> {
16    fn warn(self, msg: &str) -> Option<A>;
17}
18
19impl<A> Warn<A> for Option<A> {
20    fn warn(self, msg: &str) -> Option<A> {
21        match self {
22            Some(o) => Some(o),
23            None => {
24                warn!("{}", msg);
25                None
26            }
27        }
28    }
29}
30
31impl<A, E: fmt::Debug + fmt::Display> Warn<A> for Result<A, E> {
32    fn warn(self, msg: &str) -> Option<A> {
33        match self {
34            Ok(o) => Some(o),
35            Err(e) => {
36                warn!("{}: {}", msg, e);
37                debug!("{e:?}");
38                None
39            }
40        }
41    }
42}
43
44// from https://github.com/i3/i3/blob/next/i3-sensible-terminal
45// shorted to only the most common ones that I know support -e option
46pub const TERMINALS: [&str; 9] = [
47    "alacritty",
48    "kitty",
49    "wezterm",
50    "foot",
51    "qterminal",
52    "lilyterm",
53    "tilix",
54    "terminix",
55    "konsole",
56];
57
58pub fn get_daemon_socket_path_buff() -> PathBuf {
59    let mut buf = if let Some(runtime_path) = env::var_os("XDG_RUNTIME_DIR") {
60        std::path::PathBuf::from(runtime_path)
61    } else if let Ok(uid) = env::var("UID") {
62        std::path::PathBuf::from("/run/user/".to_owned() + &uid)
63    } else {
64        std::path::PathBuf::from("/tmp")
65    };
66    #[cfg(debug_assertions)]
67    buf.push("hyprshell.debug.sock");
68    #[cfg(not(debug_assertions))]
69    buf.push("hyprshell.sock");
70    buf
71}
72
73pub fn daemon_running() -> bool {
74    // check if socket exists and socket is open
75    let buf = get_daemon_socket_path_buff();
76    if buf.exists() {
77        debug!("Checking if daemon is running");
78        UnixStream::connect(buf).is_ok()
79    } else {
80        debug!("Daemon not running");
81        false
82    }
83}
84
85pub fn check_version(version: anyhow::Result<String>) -> anyhow::Result<()> {
86    if let Ok(version) = version {
87        info!(
88            "Starting hyprshell {} in {} mode on hyprland {}",
89            env!("CARGO_PKG_VERSION"),
90            if cfg!(debug_assertions) {
91                "debug"
92            } else {
93                "release"
94            },
95            version,
96        );
97        let parsed_version =
98            Version::parse(&version).context("Unable to parse hyprland Version")?;
99        if parsed_version.lt(&MIN_VERSION) {
100            Err(anyhow::anyhow!(
101                "hyprland version {} is too old or unknown, please update to at least {}",
102                parsed_version,
103                MIN_VERSION
104            ))
105        } else {
106            Ok(())
107        }
108    } else {
109        Err(anyhow::anyhow!("Unable to get hyprland version"))
110    }
111}
112
113pub fn collect_desktop_files() -> Vec<DirEntry> {
114    let mut res = Vec::new();
115    for dir in find_application_dirs() {
116        if !dir.exists() {
117            continue;
118        }
119        match dir.read_dir() {
120            Ok(dir) => {
121                for entry in dir.flatten() {
122                    let path = entry.path();
123                    if path.is_file() && path.extension().is_some_and(|e| e == "desktop") {
124                        res.push(entry);
125                    }
126                }
127            }
128            Err(e) => {
129                warn!("Failed to read dir {dir:?}: {e}");
130                continue;
131            }
132        }
133    }
134    debug!("found {} desktop files", res.len());
135    res
136}
137
138pub fn get_hyprshell_path() -> String {
139    env::current_exe()
140        .expect("Current executable not found")
141        .display()
142        .to_string()
143        .replace("(deleted)", "")
144}
145
146pub fn get_hyprctl_path() -> String {
147    env::var("PATH")
148        .unwrap_or_else(|_| String::from("/usr/bin:/bin:/usr/local/bin"))
149        .split(':')
150        .find_map(|dir| {
151            let path = PathBuf::from(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#"{} dispatch submap {} && {} socat '{}'"#,
168        get_hyprctl_path(),
169        submap,
170        get_hyprshell_path(),
171        echo
172    )
173}
174
175#[derive(Debug, Clone)]
176pub enum ExecType {
177    Flatpak(Box<str>, Box<str>),
178    PWA(Box<str>, Box<str>),
179    FlatpakPWA(Box<str>, Box<str>),
180    Absolute(Box<str>, Box<str>),
181    AppImage(Box<str>, Box<str>),
182    Relative(Box<str>),
183}
184
185const UNKNOWN_EXEC: &str = "unknown";
186
187pub fn analyse_exec(exec: &str) -> ExecType {
188    let exec_trim = exec.replace("'", "").replace("\"", "");
189    // pwa detection
190    if exec.contains("--app-id=") && exec.contains("--profile-directory=") {
191        // "flatpak 'run'" = pwa from browser inside flatpak
192        if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
193            let browser_exec_in_flatpak = exec_trim
194                .split_whitespace()
195                .find(|s| s.contains("--command="))
196                .and_then(|s| {
197                    s.split('=')
198                        .next_back()
199                        .and_then(|s| s.split('/').next_back())
200                })
201                .unwrap_or(UNKNOWN_EXEC);
202            let flatpak_identifier = exec_trim
203                .split_whitespace()
204                .skip(2)
205                .find(|arg| !arg.starts_with("--"))
206                .unwrap_or(UNKNOWN_EXEC);
207            ExecType::FlatpakPWA(
208                Box::from(flatpak_identifier),
209                Box::from(browser_exec_in_flatpak),
210            )
211        } else {
212            // normal PWA
213            let browser_exec = exec
214                .split_whitespace()
215                .next()
216                .and_then(|s| s.split('/').next_back())
217                .unwrap_or(UNKNOWN_EXEC);
218            let browser_full_exec = exec.split_whitespace().next().unwrap_or(UNKNOWN_EXEC);
219            ExecType::PWA(Box::from(browser_exec), Box::from(browser_full_exec))
220        }
221        // flatpak detection
222    } else if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
223        let command_in_flatpak = exec_trim
224            .split_whitespace()
225            .find(|s| s.contains("--command="))
226            .and_then(|s| {
227                s.split('=')
228                    .next_back()
229                    .and_then(|s| s.split('/').next_back())
230            })
231            .unwrap_or(UNKNOWN_EXEC);
232        let flatpak_identifier = exec_trim
233            .split_whitespace()
234            .skip(2)
235            .find(|arg| !arg.starts_with("--"))
236            .unwrap_or(UNKNOWN_EXEC);
237        ExecType::Flatpak(Box::from(flatpak_identifier), Box::from(command_in_flatpak))
238    } else if exec_trim.contains(".AppImage") {
239        // AppImage detection
240        let appimage_name = exec_trim
241            .split_whitespace()
242            .next()
243            .and_then(|s| s.split('/').next_back())
244            .and_then(|s| s.split('_').next())
245            .unwrap_or(UNKNOWN_EXEC);
246        ExecType::AppImage(Box::from(appimage_name), Box::from(exec))
247    } else if exec_trim.starts_with("/") {
248        let exec_name = exec_trim
249            .split_whitespace()
250            .next()
251            .and_then(|s| s.split('/').next_back())
252            .unwrap_or(UNKNOWN_EXEC);
253        ExecType::Absolute(Box::from(exec_name), Box::from(exec))
254    } else {
255        ExecType::Relative(Box::from(exec_trim))
256    }
257}