Skip to main content

hyprshell_exec_lib/
util.rs

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