hyprshell_exec_lib/
util.rs1use anyhow::Context;
2use core_lib::{Active, ClientId};
3use hyprland::ctl::{Color, notify, reload};
4use hyprland::data::{Client, Clients, Monitor, Monitors, Workspace};
5use hyprland::keyword::Keyword;
6use hyprland::prelude::*;
7use semver::Version;
8use std::sync::{Mutex, OnceLock};
9use std::time::Duration;
10use tracing::{debug, info, trace};
11
12pub fn get_clients() -> Vec<Client> {
13 Clients::get().map_or(vec![], hyprland::shared::HyprDataVec::to_vec)
14}
15
16pub fn get_monitors() -> Vec<Monitor> {
17 Monitors::get().map_or(vec![], hyprland::shared::HyprDataVec::to_vec)
18}
19
20#[must_use]
21pub fn get_current_monitor() -> Option<Monitor> {
22 Monitor::get_active().ok()
23}
24
25pub fn reload_hyprland_config() -> anyhow::Result<()> {
26 debug!("Reloading hyprland config");
27 reload::call().context("Failed to reload hyprland config")
28}
29
30pub fn toast(body: &str) {
31 let _ = notify::call(
32 notify::Icon::Warning,
33 Duration::from_secs(10),
34 Color::new(255, 0, 0, 255),
35 format!("hyprshell Error: {body}"),
36 );
37}
38
39pub fn info_toast(body: &str, duration: Duration) {
40 let _ = notify::call(
41 notify::Icon::Info,
42 duration,
43 Color::new(0, 255, 0, 255),
44 format!("hyprshell: {body}"),
45 );
46}
47
48#[must_use]
53pub fn to_client_id(id: &hyprland::shared::Address) -> ClientId {
54 u64::from_str_radix(id.to_string().trim_start_matches("0x"), 16)
55 .expect("Failed to parse client id, this should never happen")
56}
57
58#[must_use]
60pub fn to_client_address(id: ClientId) -> hyprland::shared::Address {
61 hyprland::shared::Address::new(format!("{id:x}"))
62}
63
64fn get_prev_follow_mouse() -> &'static Mutex<Option<String>> {
65 static PREV_FOLLOW_MOUSE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
66 PREV_FOLLOW_MOUSE.get_or_init(|| Mutex::new(None))
67}
68
69fn get_gestures_enabled() -> &'static Mutex<Option<bool>> {
70 static GESTURES_ENABLED: OnceLock<Mutex<Option<bool>>> = OnceLock::new();
71 GESTURES_ENABLED.get_or_init(|| Mutex::new(None))
72}
73
74pub fn set_remain_focused() -> anyhow::Result<()> {
75 let mut lock = get_prev_follow_mouse()
76 .lock()
77 .map_err(|e| anyhow::anyhow!("unable to lock get_prev_follow_mouse mutex: {e:?}"))?;
78 if lock.is_none() {
80 let follow = Keyword::get("input:follow_mouse").context("keyword failed")?;
81 trace!("Storing previous follow_mouse value: {}", follow.value);
82 *lock = Some(follow.value.to_string());
83 }
84 drop(lock);
85 Keyword::set("input:follow_mouse", "3").context("keyword failed")?;
86 trace!("Set follow_mouse to 3");
87
88 let mut lock = get_gestures_enabled()
89 .lock()
90 .map_err(|e| anyhow::anyhow!("unable to lock get_gestures_enabled mutex: {e:?}"))?;
91 if lock.is_none() {
92 let gestures_enabled =
93 Keyword::get("gestures:workspace_swipe").context("keyword failed")?;
94 trace!(
95 "Storing previous gestures_enabled value: {}",
96 gestures_enabled.value
97 );
98 *lock = Some(gestures_enabled.value.to_string() == "1");
99 }
100 drop(lock);
101 Keyword::set("gestures:workspace_swipe", "0").context("keyword failed")?;
102 trace!("Set gestures:workspace_swipe to 0");
103 Ok(())
104}
105
106pub fn reset_remain_focused() -> anyhow::Result<()> {
107 let follow = get_prev_follow_mouse()
108 .lock()
109 .map_err(|e| anyhow::anyhow!("unable to lock get_prev_follow_mouse mutex: {e:?}"))?;
110 if let Some(follow) = follow.as_ref() {
111 Keyword::set("input:follow_mouse", follow.clone()).context("keyword failed")?;
112 trace!("Restored previous follow_mouse value: {follow}");
113 } else {
114 trace!("No previous follow_mouse value stored, skipping reset");
115 }
116 drop(follow);
117
118 let gestures_enabled = get_gestures_enabled()
119 .lock()
120 .map_err(|e| anyhow::anyhow!("unable to lock get_gestures_enabled mutex: {e:?}"))?;
121 if let Some(enabled) = gestures_enabled.as_ref() {
122 Keyword::set("gestures:workspace_swipe", if *enabled { "1" } else { "0" })
123 .context("keyword failed")?;
124 trace!("Restored previous gestures:workspace_swipe value: {enabled}");
125 } else {
126 trace!("No previous gestures:workspace_swipe value stored, skipping reset");
127 }
128 drop(gestures_enabled);
129 Ok(())
130}
131
132pub fn get_initial_active() -> anyhow::Result<Active> {
133 let active_client = Client::get_active()?.map(|c| to_client_id(&c.address));
134 let active_ws = Workspace::get_active()?.id;
135 let active_monitor = Monitor::get_active()?.id;
136 Ok(Active {
137 client: active_client,
138 workspace: active_ws,
139 monitor: active_monitor,
140 })
141}
142
143pub fn check_version() -> anyhow::Result<()> {
144 pub const MIN_VERSION: Version = Version::new(0, 42, 0);
145
146 let version = hyprland::data::Version::get()
147 .context("Failed to get version! (hyprland is probably outdated or too new??)")?;
148 trace!("hyprland {version:?}");
149
150 let version = version
151 .version
152 .unwrap_or_else(|| version.tag.trim_start_matches('v').to_string());
153 info!(
154 "Starting hyprshell {} in {} mode on hyprland {version}",
155 env!("CARGO_PKG_VERSION"),
156 if cfg!(debug_assertions) {
157 "debug"
158 } else {
159 "release"
160 },
161 );
162 let parsed_version = Version::parse(&version).context("Unable to parse hyprland Version")?;
163 if parsed_version.lt(&MIN_VERSION) {
164 toast(&format!(
165 "hyprland version {parsed_version} is too old or unknown, please update to at least {MIN_VERSION}",
166 ));
167 }
168 Ok(())
169}