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