@@ -7,19 +7,29 @@ use std::collections::HashMap; |
| 7 | 7 | use std::fs; |
| 8 | 8 | use std::os::unix::io::{AsRawFd, BorrowedFd}; |
| 9 | 9 | use std::path::PathBuf; |
| 10 | | -use std::sync::atomic::{AtomicBool, Ordering}; |
| 10 | +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; |
| 11 | 11 | use std::sync::mpsc; |
| 12 | 12 | use std::sync::Arc; |
| 13 | 13 | use std::thread; |
| 14 | 14 | |
| 15 | 15 | use evdev::{Device, InputEventKind}; |
| 16 | +use hyprkvm_common::Direction; |
| 16 | 17 | use rustix::fs::{fcntl_setfl, OFlags}; |
| 17 | 18 | |
| 18 | 19 | use super::grabber::GrabEvent; |
| 19 | 20 | |
| 21 | +// Recovery mode ends when: |
| 22 | +// 1. Super key is released (user stopped trying keybinds or will retry with fresh state) |
| 23 | +// 2. The target key combo is detected |
| 24 | +// 3. A new grab starts |
| 25 | + |
| 20 | 26 | /// Evdev-based input grabber |
| 21 | 27 | pub struct EvdevGrabber { |
| 22 | 28 | active: Arc<AtomicBool>, |
| 29 | + /// Flag indicating recovery mode is active (1 = active, 0 = inactive) |
| 30 | + recovery_active: Arc<AtomicU64>, |
| 31 | + /// The direction to watch for in recovery mode (encoded as u8: 1=Up, 2=Down, 3=Left, 4=Right, 0=none) |
| 32 | + recovery_direction: Arc<AtomicU64>, |
| 23 | 33 | event_rx: mpsc::Receiver<GrabEvent>, |
| 24 | 34 | _thread: thread::JoinHandle<()>, |
| 25 | 35 | } |
@@ -29,13 +39,17 @@ impl EvdevGrabber { |
| 29 | 39 | pub fn new() -> Result<Self, EvdevGrabError> { |
| 30 | 40 | let active = Arc::new(AtomicBool::new(false)); |
| 31 | 41 | let active_clone = active.clone(); |
| 42 | + let recovery_active = Arc::new(AtomicU64::new(0)); |
| 43 | + let recovery_clone = recovery_active.clone(); |
| 44 | + let recovery_direction = Arc::new(AtomicU64::new(0)); |
| 45 | + let recovery_dir_clone = recovery_direction.clone(); |
| 32 | 46 | |
| 33 | 47 | let (event_tx, event_rx) = mpsc::channel(); |
| 34 | 48 | |
| 35 | 49 | let thread = thread::Builder::new() |
| 36 | 50 | .name("evdev-grabber".to_string()) |
| 37 | 51 | .spawn(move || { |
| 38 | | - if let Err(e) = run_evdev_grabber(active_clone, event_tx) { |
| 52 | + if let Err(e) = run_evdev_grabber(active_clone, recovery_clone, recovery_dir_clone, event_tx) { |
| 39 | 53 | tracing::error!("Evdev grabber error: {}", e); |
| 40 | 54 | } |
| 41 | 55 | }) |
@@ -43,6 +57,8 @@ impl EvdevGrabber { |
| 43 | 57 | |
| 44 | 58 | Ok(Self { |
| 45 | 59 | active, |
| 60 | + recovery_active, |
| 61 | + recovery_direction, |
| 46 | 62 | event_rx, |
| 47 | 63 | _thread: thread, |
| 48 | 64 | }) |
@@ -51,12 +67,32 @@ impl EvdevGrabber { |
| 51 | 67 | /// Start grabbing input |
| 52 | 68 | pub fn start(&self) { |
| 53 | 69 | tracing::info!("Starting evdev input grab"); |
| 70 | + // Cancel any recovery mode when starting a new grab |
| 71 | + self.recovery_active.store(0, Ordering::SeqCst); |
| 54 | 72 | self.active.store(true, Ordering::SeqCst); |
| 55 | 73 | } |
| 56 | 74 | |
| 57 | | - /// Stop grabbing input |
| 58 | | - pub fn stop(&self) { |
| 59 | | - tracing::info!("Stopping evdev input grab"); |
| 75 | + /// Stop grabbing input and enter recovery monitoring mode |
| 76 | + /// `stale_direction` is the direction of the outgoing transfer - the key that's stale in libinput |
| 77 | + pub fn stop(&self, stale_direction: Option<Direction>) { |
| 78 | + // Encode direction: 1=Up, 2=Down, 3=Left, 4=Right, 0=none |
| 79 | + let dir_code = match stale_direction { |
| 80 | + Some(Direction::Up) => 1, |
| 81 | + Some(Direction::Down) => 2, |
| 82 | + Some(Direction::Left) => 3, |
| 83 | + Some(Direction::Right) => 4, |
| 84 | + None => 0, |
| 85 | + }; |
| 86 | + self.recovery_direction.store(dir_code, Ordering::SeqCst); |
| 87 | + |
| 88 | + tracing::info!("Stopping evdev input grab, entering recovery mode, watching for {:?}", |
| 89 | + stale_direction); |
| 90 | + |
| 91 | + // Activate recovery mode (will stay active until Super is released or hotkey detected) |
| 92 | + if stale_direction.is_some() { |
| 93 | + self.recovery_active.store(1, Ordering::SeqCst); |
| 94 | + } |
| 95 | + // Deactivate grab |
| 60 | 96 | self.active.store(false, Ordering::SeqCst); |
| 61 | 97 | } |
| 62 | 98 | |
@@ -124,6 +160,8 @@ fn find_input_devices() -> Vec<PathBuf> { |
| 124 | 160 | |
| 125 | 161 | fn run_evdev_grabber( |
| 126 | 162 | active: Arc<AtomicBool>, |
| 163 | + recovery_active: Arc<AtomicU64>, |
| 164 | + recovery_direction: Arc<AtomicU64>, |
| 127 | 165 | event_tx: mpsc::Sender<GrabEvent>, |
| 128 | 166 | ) -> Result<(), EvdevGrabError> { |
| 129 | 167 | let device_paths = find_input_devices(); |
@@ -137,161 +175,261 @@ fn run_evdev_grabber( |
| 137 | 175 | tracing::debug!(" {}", path.display()); |
| 138 | 176 | } |
| 139 | 177 | |
| 140 | | - // We'll open and grab devices fresh each time we activate |
| 178 | + // State machine states |
| 179 | + #[derive(Debug, Clone, Copy, PartialEq)] |
| 180 | + enum State { |
| 181 | + Idle, |
| 182 | + Grabbed, |
| 183 | + Recovery, |
| 184 | + } |
| 185 | + |
| 141 | 186 | let mut devices: HashMap<PathBuf, Device> = HashMap::new(); |
| 142 | | - let mut grabbed = false; |
| 143 | | - let mut last_active = false; |
| 187 | + let mut state = State::Idle; |
| 188 | + |
| 189 | + // Key state tracking for recovery mode (Super + Arrow detection) |
| 190 | + let mut super_held = false; |
| 191 | + let mut super_was_held_at_start = false; // Track if Super was held when recovery started |
| 192 | + let mut recovery_hotkey_sent = false; |
| 193 | + |
| 194 | + // Key codes |
| 195 | + const KEY_LEFTMETA: u16 = 125; |
| 196 | + const KEY_RIGHTMETA: u16 = 126; |
| 197 | + const KEY_UP: u16 = 103; |
| 198 | + const KEY_DOWN: u16 = 108; |
| 199 | + const KEY_LEFT: u16 = 105; |
| 200 | + const KEY_RIGHT: u16 = 106; |
| 144 | 201 | |
| 145 | 202 | loop { |
| 146 | 203 | let is_active = active.load(Ordering::SeqCst); |
| 147 | | - |
| 148 | | - // Handle grab/ungrab transitions |
| 149 | | - if is_active != last_active { |
| 150 | | - last_active = is_active; |
| 151 | | - |
| 152 | | - if is_active { |
| 153 | | - // Open and grab all devices fresh |
| 154 | | - devices.clear(); |
| 155 | | - tracing::info!("Opening and grabbing input devices..."); |
| 156 | | - |
| 157 | | - for path in &device_paths { |
| 158 | | - match Device::open(path) { |
| 159 | | - Ok(mut dev) => { |
| 160 | | - let name = dev.name().unwrap_or("unknown").to_string(); |
| 161 | | - |
| 162 | | - // Set non-blocking mode |
| 163 | | - // SAFETY: dev owns the fd and will outlive this borrow |
| 164 | | - let fd = unsafe { BorrowedFd::borrow_raw(dev.as_raw_fd()) }; |
| 165 | | - if let Err(e) = fcntl_setfl(fd, OFlags::NONBLOCK) { |
| 166 | | - tracing::warn!("Failed to set non-blocking on {}: {}", name, e); |
| 204 | + let in_recovery = recovery_active.load(Ordering::SeqCst) == 1; |
| 205 | + |
| 206 | + // State transitions |
| 207 | + match state { |
| 208 | + State::Idle => { |
| 209 | + if is_active { |
| 210 | + // Transition to Grabbed |
| 211 | + devices.clear(); |
| 212 | + tracing::info!("Opening and grabbing input devices..."); |
| 213 | + |
| 214 | + for path in &device_paths { |
| 215 | + match Device::open(path) { |
| 216 | + Ok(mut dev) => { |
| 217 | + let name = dev.name().unwrap_or("unknown").to_string(); |
| 218 | + let fd = unsafe { BorrowedFd::borrow_raw(dev.as_raw_fd()) }; |
| 219 | + let _ = fcntl_setfl(fd, OFlags::NONBLOCK); |
| 220 | + let _ = dev.fetch_events(); // Drain pending |
| 221 | + |
| 222 | + match dev.grab() { |
| 223 | + Ok(()) => { |
| 224 | + tracing::info!("Grabbed: {} ({})", name, path.display()); |
| 225 | + devices.insert(path.clone(), dev); |
| 226 | + } |
| 227 | + Err(e) => { |
| 228 | + tracing::warn!("Cannot grab {}: {}", name, e); |
| 229 | + } |
| 230 | + } |
| 231 | + } |
| 232 | + Err(e) => { |
| 233 | + tracing::warn!("Failed to open {}: {}", path.display(), e); |
| 167 | 234 | } |
| 235 | + } |
| 236 | + } |
| 168 | 237 | |
| 169 | | - // Drain any pending events before grabbing to start fresh |
| 170 | | - let _ = dev.fetch_events(); |
| 238 | + if devices.is_empty() { |
| 239 | + tracing::error!("Failed to grab any input devices!"); |
| 240 | + } else { |
| 241 | + tracing::info!("Successfully grabbed {} devices", devices.len()); |
| 242 | + state = State::Grabbed; |
| 243 | + } |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + State::Grabbed => { |
| 248 | + if !is_active { |
| 249 | + // Transition to Recovery (ungrab but keep devices open) |
| 250 | + tracing::info!("Releasing grab, entering recovery mode"); |
| 251 | + |
| 252 | + for (_path, dev) in &mut devices { |
| 253 | + if let Err(e) = dev.ungrab() { |
| 254 | + tracing::warn!("Failed to ungrab: {}", e); |
| 255 | + } |
| 256 | + } |
| 171 | 257 | |
| 172 | | - // Try to grab immediately after opening |
| 173 | | - match dev.grab() { |
| 174 | | - Ok(()) => { |
| 175 | | - tracing::info!("Grabbed: {} ({})", name, path.display()); |
| 176 | | - devices.insert(path.clone(), dev); |
| 258 | + // Reset recovery state |
| 259 | + super_held = false; |
| 260 | + super_was_held_at_start = false; |
| 261 | + recovery_hotkey_sent = false; |
| 262 | + |
| 263 | + // Query current Super key state from physical keyboard |
| 264 | + for dev in devices.values() { |
| 265 | + if let Ok(key_state) = dev.get_key_state() { |
| 266 | + if key_state.contains(evdev::Key::new(KEY_LEFTMETA)) |
| 267 | + || key_state.contains(evdev::Key::new(KEY_RIGHTMETA)) |
| 268 | + { |
| 269 | + super_held = true; |
| 270 | + super_was_held_at_start = true; |
| 271 | + tracing::debug!("Super key is physically held at recovery start"); |
| 272 | + } |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + state = State::Recovery; |
| 277 | + tracing::info!("Now in recovery mode, super_held={}, super_was_held_at_start={}", |
| 278 | + super_held, super_was_held_at_start); |
| 279 | + } else { |
| 280 | + // Still grabbed - forward events |
| 281 | + let mut motion_dx: f64 = 0.0; |
| 282 | + let mut motion_dy: f64 = 0.0; |
| 283 | + let mut scroll_h: f64 = 0.0; |
| 284 | + let mut scroll_v: f64 = 0.0; |
| 285 | + |
| 286 | + for dev in devices.values_mut() { |
| 287 | + if let Ok(events) = dev.fetch_events() { |
| 288 | + for ev in events { |
| 289 | + if let InputEventKind::Key(key) = ev.kind() { |
| 290 | + tracing::debug!("RAW EVDEV: key={} value={}", |
| 291 | + key.code(), ev.value()); |
| 177 | 292 | } |
| 178 | | - Err(e) => { |
| 179 | | - tracing::warn!("Cannot grab {} ({}): {}", name, path.display(), e); |
| 180 | | - // Don't add to devices if we can't grab |
| 293 | + match convert_event(&ev) { |
| 294 | + Some(GrabEvent::PointerMotion { dx, dy }) => { |
| 295 | + motion_dx += dx; |
| 296 | + motion_dy += dy; |
| 297 | + } |
| 298 | + Some(GrabEvent::Scroll { horizontal, vertical }) => { |
| 299 | + scroll_h += horizontal; |
| 300 | + scroll_v += vertical; |
| 301 | + } |
| 302 | + Some(other) => { |
| 303 | + if event_tx.send(other).is_err() { |
| 304 | + return Ok(()); |
| 305 | + } |
| 306 | + } |
| 307 | + None => {} |
| 181 | 308 | } |
| 182 | 309 | } |
| 183 | 310 | } |
| 184 | | - Err(e) => { |
| 185 | | - tracing::warn!("Failed to open {}: {}", path.display(), e); |
| 311 | + } |
| 312 | + |
| 313 | + if motion_dx != 0.0 || motion_dy != 0.0 { |
| 314 | + if event_tx.send(GrabEvent::PointerMotion { dx: motion_dx, dy: motion_dy }).is_err() { |
| 315 | + return Ok(()); |
| 316 | + } |
| 317 | + } |
| 318 | + if scroll_h != 0.0 || scroll_v != 0.0 { |
| 319 | + if event_tx.send(GrabEvent::Scroll { horizontal: scroll_h, vertical: scroll_v }).is_err() { |
| 320 | + return Ok(()); |
| 186 | 321 | } |
| 187 | 322 | } |
| 188 | 323 | } |
| 324 | + } |
| 189 | 325 | |
| 190 | | - if devices.is_empty() { |
| 191 | | - tracing::error!("Failed to grab any input devices!"); |
| 192 | | - } else { |
| 193 | | - tracing::info!("Successfully grabbed {} devices", devices.len()); |
| 326 | + State::Recovery => { |
| 327 | + if is_active { |
| 328 | + // New grab starting, go back to grabbed state |
| 329 | + // First close current devices, they'll be reopened fresh |
| 330 | + devices.clear(); |
| 331 | + recovery_active.store(0, Ordering::SeqCst); |
| 332 | + state = State::Idle; |
| 333 | + continue; |
| 194 | 334 | } |
| 195 | | - grabbed = true; |
| 196 | | - } else { |
| 197 | | - // Ungrab and close all devices |
| 198 | | - tracing::info!("Releasing {} input devices", devices.len()); |
| 199 | | - |
| 200 | | - for (path, mut dev) in devices.drain() { |
| 201 | | - // Query physical key state while still grabbed |
| 202 | | - let held_keys: Vec<u16> = if let Ok(state) = dev.get_key_state() { |
| 203 | | - // Check which modifier/arrow keys are physically held |
| 204 | | - let keys_to_check: &[u16] = &[ |
| 205 | | - 125, 126, // Super |
| 206 | | - 42, 54, // Shift |
| 207 | | - 29, 97, // Ctrl |
| 208 | | - 56, 100, // Alt |
| 209 | | - 103, 108, 105, 106, // Arrows |
| 210 | | - ]; |
| 211 | | - keys_to_check.iter() |
| 212 | | - .filter(|&&k| state.contains(evdev::Key::new(k))) |
| 213 | | - .copied() |
| 214 | | - .collect() |
| 215 | | - } else { |
| 216 | | - Vec::new() |
| 217 | | - }; |
| 218 | | - |
| 219 | | - if !held_keys.is_empty() { |
| 220 | | - tracing::debug!("Keys physically held during ungrab: {:?}", held_keys); |
| 221 | | - } |
| 222 | | - |
| 223 | | - // Ungrab the device |
| 224 | | - if let Err(e) = dev.ungrab() { |
| 225 | | - tracing::warn!("Failed to ungrab {}: {}", path.display(), e); |
| 226 | | - continue; |
| 227 | | - } |
| 228 | | - tracing::debug!("Released {}", path.display()); |
| 229 | 335 | |
| 230 | | - // Device is dropped here, closing the fd |
| 336 | + if !in_recovery { |
| 337 | + // Recovery mode was disabled externally |
| 338 | + tracing::info!("Recovery mode ended (disabled)"); |
| 339 | + devices.clear(); |
| 340 | + state = State::Idle; |
| 341 | + continue; |
| 231 | 342 | } |
| 232 | | - grabbed = false; |
| 233 | 343 | |
| 234 | | - // Brief pause to let libinput process the ungrab |
| 235 | | - thread::sleep(std::time::Duration::from_millis(5)); |
| 236 | | - } |
| 237 | | - } |
| 344 | + // In recovery mode - monitor for Super+Arrow |
| 345 | + // Read events WITHOUT grab (we're just observing) |
| 346 | + let mut should_end_recovery = false; |
| 347 | + let mut end_reason = ""; |
| 348 | + |
| 349 | + // Decode the direction we're watching for |
| 350 | + let watch_dir_code = recovery_direction.load(Ordering::SeqCst); |
| 351 | + let watch_direction: Option<Direction> = match watch_dir_code { |
| 352 | + 1 => Some(Direction::Up), |
| 353 | + 2 => Some(Direction::Down), |
| 354 | + 3 => Some(Direction::Left), |
| 355 | + 4 => Some(Direction::Right), |
| 356 | + _ => None, |
| 357 | + }; |
| 358 | + |
| 359 | + for dev in devices.values_mut() { |
| 360 | + if let Ok(events) = dev.fetch_events() { |
| 361 | + for ev in events { |
| 362 | + if let InputEventKind::Key(key) = ev.kind() { |
| 363 | + let keycode = key.code(); |
| 364 | + let pressed = ev.value() == 1; |
| 365 | + let released = ev.value() == 0; |
| 366 | + |
| 367 | + // Track Super key state |
| 368 | + if keycode == KEY_LEFTMETA || keycode == KEY_RIGHTMETA { |
| 369 | + if pressed { |
| 370 | + super_held = true; |
| 371 | + tracing::debug!("RECOVERY: Super pressed"); |
| 372 | + } else if released { |
| 373 | + super_held = false; |
| 374 | + tracing::debug!("RECOVERY: Super released"); |
| 375 | + |
| 376 | + // End recovery when Super is released |
| 377 | + // If Super was held at start, user has finished their keybind attempt |
| 378 | + // If Super wasn't held at start, they did a fresh Super press+release |
| 379 | + if super_was_held_at_start { |
| 380 | + tracing::info!("RECOVERY: Super released (was held at start), ending recovery"); |
| 381 | + should_end_recovery = true; |
| 382 | + end_reason = "Super released"; |
| 383 | + break; |
| 384 | + } |
| 385 | + } |
| 386 | + } |
| 238 | 387 | |
| 239 | | - // Read events if grabbed |
| 240 | | - if grabbed && !devices.is_empty() { |
| 241 | | - // Accumulate motion deltas across all devices and events |
| 242 | | - let mut motion_dx: f64 = 0.0; |
| 243 | | - let mut motion_dy: f64 = 0.0; |
| 244 | | - let mut scroll_h: f64 = 0.0; |
| 245 | | - let mut scroll_v: f64 = 0.0; |
| 246 | | - |
| 247 | | - for (_path, dev) in &mut devices { |
| 248 | | - // Non-blocking read |
| 249 | | - if let Ok(events) = dev.fetch_events() { |
| 250 | | - for ev in events { |
| 251 | | - // Log raw key events from kernel for debugging |
| 252 | | - if let InputEventKind::Key(key) = ev.kind() { |
| 253 | | - tracing::debug!("RAW EVDEV: key={} value={} (1=press, 0=release, 2=repeat)", |
| 254 | | - key.code(), ev.value()); |
| 255 | | - } |
| 256 | | - match convert_event(&ev) { |
| 257 | | - Some(GrabEvent::PointerMotion { dx, dy }) => { |
| 258 | | - // Accumulate motion instead of sending immediately |
| 259 | | - motion_dx += dx; |
| 260 | | - motion_dy += dy; |
| 261 | | - } |
| 262 | | - Some(GrabEvent::Scroll { horizontal, vertical }) => { |
| 263 | | - // Accumulate scroll |
| 264 | | - scroll_h += horizontal; |
| 265 | | - scroll_v += vertical; |
| 266 | | - } |
| 267 | | - Some(other) => { |
| 268 | | - // Key events etc. - send immediately |
| 269 | | - if event_tx.send(other).is_err() { |
| 270 | | - return Ok(()); |
| 388 | + // Check for Super+Arrow, but ONLY for the direction we're watching |
| 389 | + if pressed && super_held && !recovery_hotkey_sent { |
| 390 | + let key_direction = match keycode { |
| 391 | + KEY_UP => Some(Direction::Up), |
| 392 | + KEY_DOWN => Some(Direction::Down), |
| 393 | + KEY_LEFT => Some(Direction::Left), |
| 394 | + KEY_RIGHT => Some(Direction::Right), |
| 395 | + _ => None, |
| 396 | + }; |
| 397 | + |
| 398 | + if let Some(dir) = key_direction { |
| 399 | + // Only trigger if this matches the direction we're watching for |
| 400 | + if watch_direction == Some(dir) { |
| 401 | + tracing::info!("RECOVERY: Detected Super+{:?} (matches watch direction), sending hotkey event", dir); |
| 402 | + if event_tx.send(GrabEvent::RecoveryHotkey { direction: dir }).is_err() { |
| 403 | + return Ok(()); |
| 404 | + } |
| 405 | + recovery_hotkey_sent = true; |
| 406 | + should_end_recovery = true; |
| 407 | + end_reason = "hotkey detected"; |
| 408 | + break; |
| 409 | + } else { |
| 410 | + tracing::debug!("RECOVERY: Ignoring Super+{:?} (watching for {:?})", dir, watch_direction); |
| 411 | + } |
| 412 | + } |
| 271 | 413 | } |
| 272 | 414 | } |
| 273 | | - None => {} |
| 274 | 415 | } |
| 275 | 416 | } |
| 417 | + if should_end_recovery { |
| 418 | + break; |
| 419 | + } |
| 276 | 420 | } |
| 277 | | - } |
| 278 | | - |
| 279 | | - // Send accumulated motion as single event (if any) |
| 280 | | - if motion_dx != 0.0 || motion_dy != 0.0 { |
| 281 | | - if event_tx.send(GrabEvent::PointerMotion { dx: motion_dx, dy: motion_dy }).is_err() { |
| 282 | | - return Ok(()); |
| 283 | | - } |
| 284 | | - } |
| 285 | 421 | |
| 286 | | - // Send accumulated scroll as single event (if any) |
| 287 | | - if scroll_h != 0.0 || scroll_v != 0.0 { |
| 288 | | - if event_tx.send(GrabEvent::Scroll { horizontal: scroll_h, vertical: scroll_v }).is_err() { |
| 289 | | - return Ok(()); |
| 422 | + // End recovery mode if hotkey was detected or Super was released |
| 423 | + if should_end_recovery { |
| 424 | + tracing::info!("Recovery mode ended ({})", end_reason); |
| 425 | + devices.clear(); |
| 426 | + recovery_active.store(0, Ordering::SeqCst); |
| 427 | + state = State::Idle; |
| 290 | 428 | } |
| 291 | 429 | } |
| 292 | 430 | } |
| 293 | 431 | |
| 294 | | - // Minimal sleep to avoid busy-looping while keeping latency low |
| 432 | + // Minimal sleep |
| 295 | 433 | thread::sleep(std::time::Duration::from_micros(100)); |
| 296 | 434 | } |
| 297 | 435 | } |