Skip to main content

hyprshell_exec_lib/
switch.rs

1use crate::util::to_client_address;
2use anyhow::Context;
3use core_lib::{ClientId, Warn};
4use hyprland::data::{Client, Monitors, Workspace, Workspaces};
5use hyprland::dispatch::{
6    Dispatch, DispatchType, WindowIdentifier, WorkspaceIdentifierWithSpecial,
7};
8use hyprland::prelude::*;
9use hyprland::shared::WorkspaceId;
10use tracing::{debug, instrument, trace, warn};
11
12#[instrument(level = "debug", ret(level = "trace"))]
13pub fn switch_client(address: ClientId) -> anyhow::Result<()> {
14    match switch_client_lua(address) {
15        Err(e) => {
16            warn!("Failed to switch to client: {}, trying legacy syntax", e);
17            switch_client_legacy(address)
18        }
19        Ok(r) => Ok(r),
20    }
21}
22
23fn switch_client_legacy(address: ClientId) -> anyhow::Result<()> {
24    debug!("execute switch to client: {address}");
25    deactivate_special_workspace_if_needed().warn();
26    Dispatch::call(DispatchType::FocusWindow(WindowIdentifier::Address(
27        to_client_address(address),
28    )))
29    .context("failed to execute dispatch")?;
30
31    Dispatch::call(DispatchType::BringActiveToTop).context("failed to execute dispatch2")?;
32    Ok(())
33}
34
35fn switch_client_lua(address: ClientId) -> anyhow::Result<()> {
36    debug!("execute switch to client: {address}");
37    deactivate_special_workspace_if_needed().warn();
38    let disp = hyprland::dispatch_new::Dispatch::FocusWindow(
39        hyprland::dispatch_new::WindowIdentifier::Address(to_client_address(address)),
40    );
41    disp.apply().context("failed to execute dispatch")?;
42    let disp2 = hyprland::dispatch_new::Dispatch::WindowAlterZ(
43        hyprland::dispatch_new::ZOption::Top,
44        Some(hyprland::dispatch_new::WindowIdentifier::Address(
45            to_client_address(address),
46        )),
47    );
48    disp2.apply().context("failed to execute dispatch2")?;
49    Ok(())
50}
51
52#[instrument(level = "debug", ret(level = "trace"))]
53pub fn switch_client_by_initial_class(class: &str) -> anyhow::Result<()> {
54    match switch_client_by_initial_class_lua(class) {
55        Err(e) => {
56            warn!("Failed to switch to client: {}, trying legacy syntax", e);
57            switch_client_by_initial_class_legacy(class)
58        }
59        Ok(r) => Ok(r),
60    }
61}
62
63fn switch_client_by_initial_class_lua(class: &str) -> anyhow::Result<()> {
64    debug!("execute switch to client: {class} by initial_class");
65    deactivate_special_workspace_if_needed().warn();
66
67    let disp = hyprland::dispatch_new::Dispatch::FocusWindow(
68        hyprland::dispatch_new::WindowIdentifier::InitialClassRegularExpression(
69            class.to_ascii_lowercase(),
70        ),
71    );
72    disp.apply().context("failed to execute dispatch")?;
73    let disp2 = hyprland::dispatch_new::Dispatch::WindowAlterZ(
74        hyprland::dispatch_new::ZOption::Top,
75        Some(
76            hyprland::dispatch_new::WindowIdentifier::InitialClassRegularExpression(
77                class.to_ascii_lowercase(),
78            ),
79        ),
80    );
81    disp2.apply().context("failed to execute dispatch2")?;
82    Ok(())
83}
84
85fn switch_client_by_initial_class_legacy(class: &str) -> anyhow::Result<()> {
86    debug!("execute switch to client: {class} by initial_class");
87    deactivate_special_workspace_if_needed().warn();
88    Dispatch::call(DispatchType::FocusWindow(
89        WindowIdentifier::ClassRegularExpression(&format!(
90            "initialclass:{}",
91            class.to_ascii_lowercase()
92        )),
93    ))?;
94    Dispatch::call(DispatchType::BringActiveToTop)?;
95    Ok(())
96}
97
98#[instrument(level = "debug", ret(level = "trace"))]
99pub fn switch_workspace(workspace_id: WorkspaceId) -> anyhow::Result<()> {
100    deactivate_special_workspace_if_needed().warn();
101
102    // check if already on workspace (if so, don't switch because it throws an error `Previous workspace doesn't exist`)
103    let current_workspace = Workspace::get_active();
104    if let Ok(workspace) = current_workspace
105        && workspace_id == workspace.id
106    {
107        trace!("Already on workspace {}", workspace_id);
108        return Ok(());
109    }
110
111    if workspace_id < 0 {
112        switch_special_workspace(workspace_id).with_context(|| {
113            format!("Failed to execute switch special workspace with id {workspace_id}")
114        })?;
115    } else {
116        switch_normal_workspace(workspace_id).with_context(|| {
117            format!("Failed to execute switch workspace with id {workspace_id}")
118        })?;
119    }
120    Ok(())
121}
122
123#[instrument(level = "debug", ret(level = "trace"))]
124fn switch_special_workspace(workspace_id: WorkspaceId) -> anyhow::Result<()> {
125    match switch_special_workspace_lua(workspace_id) {
126        Err(e) => {
127            warn!(
128                "Failed to switch special workspace: {}, trying legacy syntax",
129                e
130            );
131            switch_special_workspace_legacy(workspace_id)
132        }
133        Ok(r) => Ok(r),
134    }
135}
136
137fn switch_special_workspace_legacy(workspace_id: WorkspaceId) -> anyhow::Result<()> {
138    let special = Monitors::get()?
139        .into_iter()
140        .find(|m| m.special_workspace.id == workspace_id);
141    if let Some(special) = special {
142        trace!("Special workspace already toggled: {special:?}");
143        return Ok(());
144    }
145    let ws = Workspaces::get()?
146        .into_iter()
147        .find(|w| w.id == workspace_id)
148        .context("workspace not found")?;
149
150    Dispatch::call(DispatchType::ToggleSpecialWorkspace(Some(
151        ws.name.trim_start_matches("special:").to_string(),
152    )))
153    .context("failed to execute dispatch")?;
154    Ok(())
155}
156
157fn switch_special_workspace_lua(workspace_id: WorkspaceId) -> anyhow::Result<()> {
158    let special = Monitors::get()?
159        .into_iter()
160        .find(|m| m.special_workspace.id == workspace_id);
161    if let Some(special) = special {
162        trace!("Special workspace already toggled: {special:?}");
163        return Ok(());
164    }
165    let ws = Workspaces::get()?
166        .into_iter()
167        .find(|w| w.id == workspace_id)
168        .context("workspace not found")?;
169
170    let disp = hyprland::dispatch_new::Dispatch::FocusWorkspace(
171        hyprland::dispatch_new::WorkspaceIdentifier::Special(Some(
172            ws.name.trim_start_matches("special:").to_string(),
173        )),
174        false,
175    );
176    disp.apply().context("failed to execute dispatch")?;
177    Ok(())
178}
179
180#[instrument(level = "debug", ret(level = "trace"))]
181fn switch_normal_workspace(workspace_id: WorkspaceId) -> anyhow::Result<()> {
182    match switch_normal_workspace_lua(workspace_id) {
183        Err(e) => {
184            warn!("Failed to switch workspace: {}, trying legacy syntax", e);
185            switch_normal_workspace_legacy(workspace_id)
186        }
187        Ok(r) => Ok(r),
188    }
189}
190
191fn switch_normal_workspace_lua(workspace_id: WorkspaceId) -> anyhow::Result<()> {
192    debug!("execute switch to workspace {workspace_id}");
193    let disp = hyprland::dispatch_new::Dispatch::FocusWorkspace(
194        hyprland::dispatch_new::WorkspaceIdentifier::Id(workspace_id),
195        false,
196    );
197    disp.apply().context("failed to execute dispatch")?;
198    Ok(())
199}
200
201fn switch_normal_workspace_legacy(workspace_id: WorkspaceId) -> anyhow::Result<()> {
202    debug!("execute switch to workspace {workspace_id}");
203    Dispatch::call(DispatchType::Workspace(WorkspaceIdentifierWithSpecial::Id(
204        workspace_id,
205    )))
206    .context("failed to execute dispatch")?;
207    Ok(())
208}
209
210/// always run when changing client or workspace
211///
212/// if client on special workspace is opened the workspace is activated
213#[instrument(level = "debug", ret(level = "trace"))]
214fn deactivate_special_workspace_if_needed() -> anyhow::Result<()> {
215    let active_ws = Workspace::get_active()
216        .map(|w| w.name)
217        .context("active workspace failed")?;
218    let active_ws = Client::get_active()
219        .context("active client failed")?
220        .map_or(active_ws, |a| a.workspace.name);
221    trace!("current workspace: {active_ws}");
222    if active_ws.starts_with("special:") {
223        debug!("current client is on special workspace, deactivating special workspace");
224        // current client is on special workspace
225        match deactivate_special_workspace_if_needed_lua(&active_ws) {
226            Err(e) => {
227                warn!(
228                    "Failed to deactivate special workspace: {}, trying legacy syntax",
229                    e
230                );
231                deactivate_special_workspace_if_needed_legacy(&active_ws)
232            }
233            Ok(r) => Ok(r),
234        }?;
235    }
236    Ok(())
237}
238
239fn deactivate_special_workspace_if_needed_lua(name: &str) -> anyhow::Result<()> {
240    debug!("execute switch to workspace {name}");
241    let disp = hyprland::dispatch_new::Dispatch::FocusWorkspace(
242        hyprland::dispatch_new::WorkspaceIdentifier::Special(Some(
243            name.trim_start_matches("special:").to_string(),
244        )),
245        false,
246    );
247    disp.apply().context("failed to execute dispatch")?;
248    Ok(())
249}
250
251fn deactivate_special_workspace_if_needed_legacy(name: &str) -> anyhow::Result<()> {
252    debug!("execute switch to workspace {name}");
253    Dispatch::call(DispatchType::ToggleSpecialWorkspace(Some(
254        name.trim_start_matches("special:").to_string(),
255    )))?;
256    Ok(())
257}