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