1use anyhow::Result;
2use clap::Parser;
3
4use crate::args::Args;
5use crate::connection::HyprlandClient;
6use crate::domain::{Direction, OwnedMonitor, OwnedWorkspace};
7
8pub struct HyprCycle {
12 connection: Box<dyn HyprlandClient>,
13}
14
15impl HyprCycle {
16 pub fn parse_args() -> Args {
17 Args::parse()
18 }
19
20 pub fn new(connection: Box<dyn HyprlandClient>) -> HyprCycle {
23 HyprCycle { connection }
24 }
25
26 pub fn real() -> Result<HyprCycle> {
29 let conn = hyprrust::HyprlandConnection::current().map_err(anyhow::Error::new)?;
30 let client = crate::connection::RealHyprlandClient::new(conn);
31 Ok(HyprCycle::new(Box::new(client)))
32 }
33
34 pub fn get_focused_monitor(&self) -> Result<OwnedMonitor> {
37 let monitors = self.connection.get_monitors()?;
38 let monitor = monitors
39 .into_iter()
40 .find(|m| m.focused())
41 .ok_or_else(|| anyhow::anyhow!("No focused monitor found"))?;
42 Ok(monitor)
43 }
44
45 pub fn get_workspaces_for_monitor(
49 &self,
50 monitor: &OwnedMonitor,
51 ) -> Result<Vec<OwnedWorkspace>> {
52 let workspaces = self.connection.get_workspaces()?;
53 let mut workspaces_for_monitor: Vec<OwnedWorkspace> = workspaces
54 .into_iter()
55 .filter(|w| w.monitor_name().eq(monitor.name()) && w.visible())
56 .collect();
57 if workspaces_for_monitor.is_empty() {
58 return Err(anyhow::anyhow!(
59 "No workspaces found for monitor: {}",
60 monitor.name()
61 ));
62 }
63 workspaces_for_monitor.sort();
64 Ok(workspaces_for_monitor)
65 }
66
67 pub fn get_current_workspace(&self) -> Result<OwnedWorkspace> {
69 let focused_monitor = self.get_focused_monitor()?;
70 let active_workspace = focused_monitor.active_workspace();
71 Ok(active_workspace)
72 }
73
74 pub fn get_target_workspace(&self, direction: Direction) -> Result<OwnedWorkspace> {
77 let monitor = &self.get_focused_monitor()?;
78 let workspaces = &self.get_workspaces_for_monitor(monitor)?;
79 let current_ws = &self.get_current_workspace()?;
80
81 let idx = workspaces
82 .iter()
83 .position(|w| w == current_ws)
84 .ok_or_else(|| anyhow::anyhow!("Current workspace not found"))?;
85 let len = workspaces.len();
86
87 let next_idx = match direction {
88 Direction::Next => (idx + 1) % len,
89 Direction::Previous => (idx + len - 1) % len,
90 };
91 Ok(workspaces[next_idx].to_owned())
92 }
93
94 pub fn switch_to_workspace(&self, target: &OwnedWorkspace) -> Result<()> {
95 self.connection.go_to_workspace(target.id())?;
96 Ok(())
97 }
98}
99
100#[cfg(test)]
101pub mod fixtures {
102 use crate::domain::{OwnedMonitor, OwnedWorkspace};
103
104 pub fn ws(id: i64, mon: &str) -> OwnedWorkspace {
105 OwnedWorkspace::new(id, mon.to_string())
106 }
107
108 pub fn mon(name: &str, id: i64, focused: bool, active_id: i64) -> OwnedMonitor {
109 OwnedMonitor::new(name.to_string(), id, focused, ws(active_id, name))
110 }
111
112 pub fn monitors() -> Vec<OwnedMonitor> {
113 vec![
114 mon("eDP-1", 1, true, 1), mon("HDMI-1", 2, false, 3),
116 ]
117 }
118
119 pub fn workspaces() -> Vec<OwnedWorkspace> {
120 vec![
121 ws(-97, "eDP-1"), ws(1, "eDP-1"),
123 ws(2, "eDP-1"),
124 ws(3, "HDMI-1"),
125 ]
126 }
127}
128
129#[cfg(test)]
130mod tests {
131
132 use super::*;
133 use crate::connection::MockHyprlandClient;
134
135 mod helpers {
136 use super::super::*;
137 use crate::connection::MockHyprlandClient;
138 use anyhow::Context;
139
140 pub fn mock_service_with(conn: MockHyprlandClient) -> HyprCycle {
141 HyprCycle::new(Box::new(conn))
142 }
143
144 pub fn mock_service() -> HyprCycle {
145 let mut conn = MockHyprlandClient::new();
146 conn.expect_get_monitors()
147 .returning(|| Ok(fixtures::monitors()));
148 conn.expect_get_workspaces()
149 .returning(|| Ok(fixtures::workspaces()));
150 mock_service_with(conn)
151 }
152
153 pub fn visible_for_monitor(
154 ws: Vec<OwnedWorkspace>,
155 monitor: &OwnedMonitor,
156 ) -> Vec<OwnedWorkspace> {
157 ws.into_iter()
158 .filter(|w| w.visible() && w.monitor_name() == monitor.name())
159 .collect()
160 }
161
162 pub fn focused_monitor(monitors: Vec<OwnedMonitor>) -> Result<OwnedMonitor> {
163 monitors
164 .into_iter()
165 .find(|m| m.focused())
166 .context("No focused monitor found")
167 }
168 }
169
170 #[test]
173 fn test_get_focused_monitor() -> Result<()> {
174 let expected = helpers::focused_monitor(fixtures::monitors())?;
175 let returned = helpers::mock_service().get_focused_monitor()?;
176 assert_eq!(returned.name(), expected.name());
177 Ok(())
178 }
179
180 #[test]
184 fn test_get_workspaces_for_monitor() -> Result<()> {
185 let target_monitor = &fixtures::monitors()[0];
186 let returned_workspaces =
187 helpers::mock_service().get_workspaces_for_monitor(target_monitor)?;
188 assert!(returned_workspaces.iter().all(|w| w.visible()));
190 assert!(returned_workspaces
192 .iter()
193 .all(|w| w.monitor_name() == target_monitor.name()));
194 let expected_workspaces =
196 helpers::visible_for_monitor(fixtures::workspaces(), target_monitor);
197 assert_eq!(expected_workspaces, returned_workspaces);
198 Ok(())
199 }
200
201 #[test]
206 fn test_get_current_workspace() -> Result<()> {
207 let expected = helpers::focused_monitor(fixtures::monitors())?;
208 let returned = helpers::mock_service().get_current_workspace()?;
209 assert_eq!(returned.id(), expected.active_workspace().id());
210 Ok(())
211 }
212
213 #[test]
216 fn test_switch_to_workspace() -> Result<()> {
217 let mut conn = MockHyprlandClient::new();
218 conn.expect_go_to_workspace().times(1).returning(|_| Ok(()));
219 helpers::mock_service_with(conn).switch_to_workspace(&fixtures::workspaces()[0])
220 }
221}