Rust · 7335 bytes Raw Blame History
1 //! State manager module
2 //!
3 //! Single source of truth for all runtime state.
4
5 use std::net::SocketAddr;
6 use std::sync::atomic::{AtomicBool, Ordering};
7
8 use tokio::sync::RwLock;
9
10 use hyprkvm_common::Direction;
11
12 use crate::config::Config;
13 use crate::hyprland::ipc::HyprlandClient;
14 use crate::hyprland::layout::MonitorLayout;
15 use crate::input::EdgeEvent;
16
17 /// The current control state
18 #[derive(Debug, Clone, PartialEq, Eq)]
19 pub enum ControlState {
20 /// This machine has keyboard/mouse control
21 Local,
22 /// Control transferred to another machine
23 Remote { machine: String },
24 }
25
26 /// What triggered an edge detection
27 #[derive(Debug, Clone)]
28 pub enum EdgeTrigger {
29 /// Mouse moved to screen edge
30 Mouse {
31 direction: Direction,
32 position: (i32, i32),
33 },
34 /// Keyboard navigation at workspace boundary
35 Keyboard { direction: Direction },
36 }
37
38 /// Information about a neighbor machine
39 #[derive(Debug, Clone)]
40 pub struct NeighborInfo {
41 pub name: String,
42 pub address: SocketAddr,
43 pub direction: Direction,
44 }
45
46 /// Unified state manager for the daemon
47 pub struct StateManager {
48 /// Hyprland client for queries
49 hyprland: HyprlandClient,
50 /// Current monitor layout
51 layout: RwLock<MonitorLayout>,
52 /// Daemon configuration
53 config: Config,
54 /// Whether we have local control
55 has_local_control: AtomicBool,
56 /// Current remote machine (if control is remote)
57 current_remote: RwLock<Option<String>>,
58 }
59
60 impl StateManager {
61 /// Create a new state manager
62 pub async fn new(config: Config, hyprland: HyprlandClient) -> Result<Self, StateError> {
63 // Get initial monitor layout
64 let monitors = hyprland
65 .monitors()
66 .await
67 .map_err(|e| StateError::Hyprland(e.to_string()))?;
68
69 let layout = MonitorLayout::from_monitors(&monitors);
70
71 tracing::info!(
72 "Initialized state manager with {} monitors",
73 monitors.len()
74 );
75
76 Ok(Self {
77 hyprland,
78 layout: RwLock::new(layout),
79 config,
80 has_local_control: AtomicBool::new(true),
81 current_remote: RwLock::new(None),
82 })
83 }
84
85 /// Get current control state
86 pub async fn control_state(&self) -> ControlState {
87 if self.has_local_control.load(Ordering::Relaxed) {
88 ControlState::Local
89 } else {
90 let remote = self.current_remote.read().await;
91 match remote.as_ref() {
92 Some(name) => ControlState::Remote {
93 machine: name.clone(),
94 },
95 None => ControlState::Local,
96 }
97 }
98 }
99
100 /// Check if an edge trigger should cause a network switch
101 pub async fn should_network_switch(&self, trigger: &EdgeTrigger) -> Option<NeighborInfo> {
102 let direction = match trigger {
103 EdgeTrigger::Mouse { direction, .. } => direction,
104 EdgeTrigger::Keyboard { direction } => direction,
105 };
106
107 // Check if we have a neighbor in this direction
108 self.get_neighbor(*direction).await
109 }
110
111 /// Get neighbor machine in the given direction
112 pub async fn get_neighbor(&self, direction: Direction) -> Option<NeighborInfo> {
113 let layout = self.layout.read().await;
114
115 // First check if we're at the edge of our local monitors
116 if !layout.is_at_edge(direction) {
117 tracing::debug!(
118 "Not at local edge for direction {:?}, no network switch",
119 direction
120 );
121 return None;
122 }
123
124 // Check if we have a configured neighbor in this direction
125 self.config
126 .machines
127 .neighbors
128 .iter()
129 .find(|n| n.direction == direction)
130 .map(|n| NeighborInfo {
131 name: n.name.clone(),
132 address: n.address,
133 direction: n.direction,
134 })
135 }
136
137 /// Set control as transferred to a remote machine
138 pub async fn set_control_remote(&self, machine: &str) {
139 tracing::info!("Control transferred to: {}", machine);
140 self.has_local_control.store(false, Ordering::Relaxed);
141 let mut remote = self.current_remote.write().await;
142 *remote = Some(machine.to_string());
143 }
144
145 /// Set control as returned to local
146 pub async fn set_control_local(&self) {
147 tracing::info!("Control returned to local");
148 self.has_local_control.store(true, Ordering::Relaxed);
149 let mut remote = self.current_remote.write().await;
150 *remote = None;
151 }
152
153 /// Refresh monitor layout from Hyprland
154 pub async fn refresh_layout(&self) -> Result<(), StateError> {
155 let monitors = self
156 .hyprland
157 .monitors()
158 .await
159 .map_err(|e| StateError::Hyprland(e.to_string()))?;
160
161 let new_layout = MonitorLayout::from_monitors(&monitors);
162
163 let mut layout = self.layout.write().await;
164 *layout = new_layout;
165
166 tracing::debug!("Refreshed monitor layout: {} monitors", monitors.len());
167 Ok(())
168 }
169
170 /// Get a read lock on the current layout
171 pub async fn layout(&self) -> tokio::sync::RwLockReadGuard<'_, MonitorLayout> {
172 self.layout.read().await
173 }
174
175 /// Get the configured edges that have network neighbors
176 pub fn network_edge_directions(&self) -> Vec<Direction> {
177 self.config
178 .machines
179 .neighbors
180 .iter()
181 .map(|n| n.direction)
182 .collect()
183 }
184
185 /// Process an edge event and determine if action needed
186 pub async fn process_edge_event(&self, event: EdgeEvent) -> Option<NeighborInfo> {
187 let trigger = EdgeTrigger::Mouse {
188 direction: event.direction,
189 position: event.position,
190 };
191
192 if let Some(neighbor) = self.should_network_switch(&trigger).await {
193 tracing::info!(
194 "Edge event {:?} should switch to {}",
195 event.direction,
196 neighbor.name
197 );
198 Some(neighbor)
199 } else {
200 tracing::debug!(
201 "Edge event {:?} has no network neighbor",
202 event.direction
203 );
204 None
205 }
206 }
207
208 /// Process a keyboard move and determine if action needed
209 pub async fn process_keyboard_move(&self, direction: Direction) -> Option<NeighborInfo> {
210 let trigger = EdgeTrigger::Keyboard { direction };
211
212 if let Some(neighbor) = self.should_network_switch(&trigger).await {
213 tracing::info!(
214 "Keyboard move {:?} should switch to {}",
215 direction,
216 neighbor.name
217 );
218 Some(neighbor)
219 } else {
220 tracing::debug!("Keyboard move {:?} has no network neighbor", direction);
221 None
222 }
223 }
224
225 /// Get machine name
226 pub fn machine_name(&self) -> &str {
227 &self.config.machines.self_name
228 }
229
230 /// Get the config reference
231 pub fn config(&self) -> &Config {
232 &self.config
233 }
234 }
235
236 #[derive(Debug, thiserror::Error)]
237 pub enum StateError {
238 #[error("Hyprland error: {0}")]
239 Hyprland(String),
240
241 #[error("Configuration error: {0}")]
242 Config(String),
243 }
244