hyprshell_core_lib/
util.rs1use 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
46pub 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 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}