hyprshell_core_lib/
util.rs

1use crate::transfer::TransferType;
2use anyhow::Context;
3use ron::extensions::Extensions;
4use semver::Version;
5use std::fs::DirEntry;
6use std::os::unix::net::UnixStream;
7use std::path::PathBuf;
8use std::sync::OnceLock;
9use std::{env, fmt};
10use tracing::{debug, trace, 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 Ok(runtime_path) = env::var("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        let parsed_version =
90            Version::parse(&version).context("Unable to parse hyprland Version")?;
91        if parsed_version.lt(&MIN_VERSION) {
92            Err(anyhow::anyhow!(
93                "hyprland version {} is too old or unknown, please update to at least {}",
94                parsed_version,
95                MIN_VERSION
96            ))
97        } else {
98            Ok(())
99        }
100    } else {
101        Err(anyhow::anyhow!("Unable to get hyprland version"))
102    }
103}
104
105pub fn collect_desktop_files() -> Vec<DirEntry> {
106    let mut res = Vec::new();
107    for dir in find_application_dirs() {
108        if !dir.exists() {
109            continue;
110        }
111        match dir.read_dir() {
112            Ok(dir) => {
113                for entry in dir.flatten() {
114                    let path = entry.path();
115                    if path.is_file() && path.extension().is_some_and(|e| e == "desktop") {
116                        res.push(entry);
117                    }
118                }
119            }
120            Err(e) => {
121                warn!("Failed to read dir {dir:?}: {e}");
122                continue;
123            }
124        }
125    }
126    debug!("found {} desktop files", res.len());
127    res
128}
129
130fn find_application_dirs() -> Vec<PathBuf> {
131    let mut dirs = env::var_os("XDG_DATA_DIRS")
132        .map(|val| env::split_paths(&val).collect())
133        .unwrap_or_else(|| {
134            vec![
135                PathBuf::from("/usr/local/share"),
136                PathBuf::from("/usr/share"),
137            ]
138        });
139
140    if let Some(data_home) = env::var_os("XDG_DATA_HOME").map(PathBuf::from).map_or_else(
141        || {
142            env::var_os("HOME")
143                .map(|p| PathBuf::from(p).join(".local/share"))
144                .or_else(|| {
145                    warn!("No XDG_DATA_HOME and HOME environment variable found");
146                    None
147                })
148        },
149        Some,
150    ) {
151        dirs.push(data_home)
152    }
153
154    let dirs = dirs
155        .into_iter()
156        .map(|dir| dir.join("applications"))
157        .collect();
158    trace!("searching for icons in dirs: {:?}", dirs);
159    dirs
160}
161
162static RON_OPTIONS: OnceLock<ron::Options> = OnceLock::new();
163
164fn get_ron_options() -> ron::Options {
165    ron::Options::default()
166        .with_default_extension(Extensions::IMPLICIT_SOME)
167        .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
168        .with_default_extension(Extensions::EXPLICIT_STRUCT_NAMES)
169}
170
171pub fn to_ron_string(transfer: &TransferType) -> anyhow::Result<String> {
172    RON_OPTIONS
173        .get_or_init(get_ron_options)
174        .to_string(transfer)
175        .context("Failed to serialize ron transfer data")
176}
177
178pub fn from_ron_string(transfer: &str) -> anyhow::Result<TransferType> {
179    RON_OPTIONS
180        .get_or_init(get_ron_options)
181        .from_str(transfer)
182        .context("Failed to deserialize ron transfer data")
183}
184
185static SOCAT_PATH: OnceLock<String> = OnceLock::new();
186
187fn get_socat_path() -> String {
188    env::var("HYPRSHELL_SOCAT_PATH")
189        .or_else(|_| which::which("socat").map(|path| path.to_string_lossy().to_string()))
190        .expect("`socat` command not found. Please ensure it is installed and available in PATH.")
191}
192
193pub fn generate_socat(echo: &str) -> String {
194    format!(
195        r#"echo '{}' | {} - UNIX-CONNECT:{}"#,
196        echo,
197        SOCAT_PATH.get_or_init(get_socat_path),
198        get_daemon_socket_path_buff().to_string_lossy()
199    )
200}