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
138fn 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 generate_socat(echo: &str) -> String {
147    format!(r#"{} socat '{}'"#, get_hyprshell_path(), echo)
148}
149
150#[derive(Debug, Clone)]
151pub enum ExecType {
152    Flatpak(Box<str>, Box<str>),
153    PWA(Box<str>, Box<str>),
154    FlatpakPWA(Box<str>, Box<str>),
155    Absolute(Box<str>, Box<str>),
156    AppImage(Box<str>, Box<str>),
157    Relative(Box<str>),
158}
159
160const UNKNOWN_EXEC: &str = "unknown";
161
162pub fn analyse_exec(exec: &str) -> ExecType {
163    let exec_trim = exec.replace("'", "").replace("\"", "");
164    // pwa detection
165    if exec.contains("--app-id=") && exec.contains("--profile-directory=") {
166        // "flatpak 'run'" = pwa from browser inside flatpak
167        if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
168            let browser_exec_in_flatpak = exec_trim
169                .split_whitespace()
170                .find(|s| s.contains("--command="))
171                .and_then(|s| {
172                    s.split('=')
173                        .next_back()
174                        .and_then(|s| s.split('/').next_back())
175                })
176                .unwrap_or(UNKNOWN_EXEC);
177            let flatpak_identifier = exec_trim
178                .split_whitespace()
179                .skip(2)
180                .find(|arg| !arg.starts_with("--"))
181                .unwrap_or(UNKNOWN_EXEC);
182            ExecType::FlatpakPWA(
183                Box::from(flatpak_identifier),
184                Box::from(browser_exec_in_flatpak),
185            )
186        } else {
187            // normal PWA
188            let browser_exec = exec
189                .split_whitespace()
190                .next()
191                .and_then(|s| s.split('/').next_back())
192                .unwrap_or(UNKNOWN_EXEC);
193            let browser_full_exec = exec.split_whitespace().next().unwrap_or(UNKNOWN_EXEC);
194            ExecType::PWA(Box::from(browser_exec), Box::from(browser_full_exec))
195        }
196        // flatpak detection
197    } else if exec.contains("flatpak run") || exec.contains("flatpak 'run'") {
198        let command_in_flatpak = exec_trim
199            .split_whitespace()
200            .find(|s| s.contains("--command="))
201            .and_then(|s| {
202                s.split('=')
203                    .next_back()
204                    .and_then(|s| s.split('/').next_back())
205            })
206            .unwrap_or(UNKNOWN_EXEC);
207        let flatpak_identifier = exec_trim
208            .split_whitespace()
209            .skip(2)
210            .find(|arg| !arg.starts_with("--"))
211            .unwrap_or(UNKNOWN_EXEC);
212        ExecType::Flatpak(Box::from(flatpak_identifier), Box::from(command_in_flatpak))
213    } else if exec_trim. contains(".AppImage"){
214        // AppImage detection
215        let appimage_name = exec_trim
216            .split_whitespace()
217            .next()
218            .and_then(|s| s.split('/').next_back())
219            .and_then(|s| s.split('_').next())
220            .unwrap_or(UNKNOWN_EXEC);
221        ExecType::AppImage(Box::from(appimage_name), Box::from(exec))
222    } else if exec_trim.starts_with("/") {
223        let exec_name = exec_trim
224            .split_whitespace()
225            .next()
226            .and_then(|s| s.split('/').next_back())
227            .unwrap_or(UNKNOWN_EXEC);
228        ExecType::Absolute(Box::from(exec_name), Box::from(exec))
229    } else {
230        ExecType::Relative(Box::from(exec_trim))
231    }
232}