hyprshell_exec_lib/
util.rs

1use 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/// trim 0x from hexadecimal (base-16) string and convert to id
49///
50/// # Panics
51/// Panics if the id cannot be parsed, this should never happen as the id is always a valid hexadecimal string
52#[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/// convert id to hexadecimal (base-16) string
59#[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
69pub fn set_no_follow_mouse() -> anyhow::Result<()> {
70    Keyword::set("input:follow_mouse", "3").context("keyword failed")?;
71    trace!("Set follow_mouse to 3");
72    Ok(())
73}
74
75pub fn reset_no_follow_mouse() -> anyhow::Result<()> {
76    let follow = get_prev_follow_mouse()
77        .lock()
78        .map_err(|e| anyhow::anyhow!("unable to lock get_prev_follow_mouse mutex: {e:?}"))?;
79    if let Some(follow) = follow.as_ref() {
80        Keyword::set("input:follow_mouse", follow.clone()).context("keyword failed")?;
81        trace!("Restored previous follow_mouse value: {follow}");
82    } else {
83        trace!("No previous follow_mouse value stored, skipping reset");
84    }
85    drop(follow);
86    Ok(())
87}
88
89pub fn set_follow_mouse_default() -> anyhow::Result<()> {
90    let mut lock = get_prev_follow_mouse()
91        .lock()
92        .map_err(|e| anyhow::anyhow!("unable to lock get_prev_follow_mouse mutex: {e:?}"))?;
93    let follow = Keyword::get("input:follow_mouse").context("keyword failed")?;
94    trace!("Storing previous follow_mouse value: {}", follow.value);
95    *lock = Some(follow.value.to_string());
96    drop(lock);
97    Ok(())
98}
99
100pub fn get_initial_active() -> anyhow::Result<Active> {
101    let active_client = Client::get_active()
102        .ok()
103        .flatten()
104        .map(|c| to_client_id(&c.address));
105    let active_ws = Workspace::get_active()
106        .context("unable to get initial workspace")?
107        .id;
108    let active_monitor = Monitor::get_active()
109        .context("unable to get initial monitor")?
110        .id;
111    Ok(Active {
112        client: active_client,
113        workspace: active_ws,
114        monitor: active_monitor,
115    })
116}
117
118pub fn check_version() -> anyhow::Result<()> {
119    pub const MIN_VERSION: Version = Version::new(0, 42, 0);
120
121    let version = hyprland::data::Version::get()
122        .context("Failed to get version! (hyprland is probably outdated or too new??)")?;
123    trace!("hyprland {version:?}");
124
125    let version = version
126        .version
127        .unwrap_or_else(|| version.tag.trim_start_matches('v').to_string());
128    info!(
129        "Starting hyprshell {} in {} mode on hyprland {version}",
130        env!("CARGO_PKG_VERSION"),
131        if cfg!(debug_assertions) {
132            "debug"
133        } else {
134            "release"
135        },
136    );
137    let parsed_version = Version::parse(&version).context("Unable to parse hyprland Version")?;
138    if parsed_version.lt(&MIN_VERSION) {
139        toast(&format!(
140            "hyprland version {parsed_version} is too old or unknown, please update to at least {MIN_VERSION}",
141        ));
142    }
143    Ok(())
144}