From d81eb7c460c399cb7f97d079ecaae2a7d05ed3ca Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 29 Jun 2025 04:54:39 -0500 Subject: [PATCH] 0.4.0 mostly complete milestone --- client/Cargo.toml | 2 ++ client/src/input/inputs.rs | 24 ++++++++----- client/src/input/keyboard.rs | 12 +++++++ client/src/layout.rs | 67 ++++++++++++++++++++++++++++++++++++ client/src/lib.rs | 1 + server/src/video.rs | 1 + 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 client/src/layout.rs diff --git a/client/Cargo.toml b/client/Cargo.toml index e1a0fda..75238f5 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -24,6 +24,8 @@ gstreamer-video = "0.23" gstreamer-gl-wayland = "0.23" winit = "0.30" raw-window-handle = "0.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [build-dependencies] prost-build = "0.13" diff --git a/client/src/input/inputs.rs b/client/src/input/inputs.rs index a1adcce..e0f2a91 100644 --- a/client/src/input/inputs.rs +++ b/client/src/input/inputs.rs @@ -1,7 +1,7 @@ // client/src/input/inputs.rs use anyhow::{bail, Context, Result}; -use evdev::{Device, EventType, KeyCode, RelativeAxisCode, ReadFlag, WriteFlag}; +use evdev::{Device, EventType, KeyCode, RelativeAxisCode}; use tokio::{sync::broadcast::Sender, time::{interval, Duration}}; use tracing::{debug, info, warn}; @@ -9,6 +9,7 @@ use lesavka_common::lesavka::{KeyboardReport, MouseReport}; use super::{keyboard::KeyboardAggregator, mouse::MouseAggregator, camera::CameraCapture, microphone::MicrophoneCapture}; +use crate::layout::{Layout, apply as apply_layout}; pub struct InputAggregator { kbd_tx: Sender, @@ -48,15 +49,11 @@ impl InputAggregator { continue; } - // attempt open - let mut dev = match Device::open_with_options( - &path, - ReadFlag::NORMAL | ReadFlag::BLOCKING, - WriteFlag::NORMAL, // <-- write access needed for EVIOCGRAB - ) { + // ─── open the event node read‑write *without* unsafe ────────── + let mut dev = match Device::open(&path) { Ok(d) => d, Err(e) => { - warn!("❌ open {path}: {e}"); + warn!("❌ open {}: {e}", path.display()); continue; } }; @@ -109,8 +106,11 @@ impl InputAggregator { pub async fn run(&mut self) -> Result<()> { // Example approach: poll each aggregator in a simple loop let mut tick = interval(Duration::from_millis(10)); + let mut current = Layout::SideBySide; loop { let magic_now = self.keyboards.iter().any(|k| k.magic_grab()); + let magic_left = self.keyboards.iter().any(|k| k.magic_left()); + let magic_right = self.keyboards.iter().any(|k| k.magic_right()); let mut want_kill = false; for kbd in &mut self.keyboards { kbd.process_events(); @@ -118,6 +118,14 @@ impl InputAggregator { } if magic_now && !self.magic_active { self.toggle_grab(); } + if (magic_left || magic_right) && self.magic_active { + current = match current { + Layout::SideBySide => Layout::FullLeft, + Layout::FullLeft => Layout::FullRight, + Layout::FullRight => Layout::SideBySide, + }; + apply_layout(current); + } if want_kill { warn!("🧙 magic chord - killing 🪄 AVADA KEDAVRA!!! 💥💀⚰️"); std::process::exit(0); diff --git a/client/src/input/keyboard.rs b/client/src/input/keyboard.rs index 489b8d7..4c92b34 100644 --- a/client/src/input/keyboard.rs +++ b/client/src/input/keyboard.rs @@ -89,6 +89,18 @@ impl KeyboardAggregator { && self.has_key(KeyCode::KEY_G) } + pub fn magic_left(&self) -> bool { + self.has_key(KeyCode::KEY_LEFTCTRL) + && self.has_key(KeyCode::KEY_LEFTSHIFT) + && self.has_key(KeyCode::KEY_LEFT) + } + + pub fn magic_right(&self) -> bool { + self.has_key(KeyCode::KEY_LEFTCTRL) + && self.has_key(KeyCode::KEY_LEFTSHIFT) + && self.has_key(KeyCode::KEY_RIGHT) + } + pub fn magic_kill(&self) -> bool { self.has_key(KeyCode::KEY_LEFTCTRL) && self.has_key(KeyCode::KEY_ESC) diff --git a/client/src/layout.rs b/client/src/layout.rs new file mode 100644 index 0000000..d3e50d3 --- /dev/null +++ b/client/src/layout.rs @@ -0,0 +1,67 @@ +// client/src/layout.rs – Wayland‑only window placement utilities +#![forbid(unsafe_code)] + +use std::process::Command; +use tracing::{info, warn}; +use serde_json::Value; + +/// The three layouts we cycle through. +#[derive(Clone, Copy)] +pub enum Layout { + SideBySide, // two halves on the current monitor + FullLeft, // left eye full‑screen, right hidden + FullRight, // right eye full‑screen, left hidden +} + +/// Move/resize a window titled “Lesavka‑eye‑{eye}” using `swaymsg`. +fn place_window(eye: u32, x: i32, y: i32, w: i32, h: i32) { + let title = format!("Lesavka‑eye‑{eye}"); + let cmd = format!( + r#"[title="^{title}$"] resize set {w} {h}; move position {x} {y}"# + ); + + match Command::new("swaymsg").arg(cmd).status() { + Ok(st) if st.success() => info!("✅ placed eye{eye} {w}×{h}@{x},{y}"), + Ok(st) => warn!("⚠️ swaymsg exited with {st}"), + Err(e) => warn!("⚠️ swaymsg failed: {e}"), + } +} + +/// Apply a layout on the **currently‑focused** output. +/// No `?` operators → the function can stay `-> ()`. +pub fn apply(layout: Layout) { + let out = match Command::new("swaymsg") + .args(["-t", "get_outputs", "-r"]) + .output() + { + Ok(o) => o.stdout, + Err(e) => { warn!("get_outputs failed: {e}"); return; } + }; + + let Ok(Value::Array(outputs)) = serde_json::from_slice::(&out) else { + warn!("unexpected JSON from swaymsg"); return; + }; + let Some(rect) = outputs.iter() + .find(|o| o.get("focused").and_then(Value::as_bool) == Some(true)) + .and_then(|o| o.get("rect")) else { return; }; + + // helper to read an i64 → i32 with defaults + let g = |k: &str| rect.get(k).and_then(Value::as_i64).unwrap_or(0) as i32; + let (x, y, w, h) = (g("x"), g("y"), g("width"), g("height")); + + match layout { + Layout::SideBySide => { + let w_half = w / 2; + place_window(0, x, y, w_half, h); + place_window(1, x + w_half, y, w_half, h); + } + Layout::FullLeft => { + place_window(0, x, y, w, h); + place_window(1, x + w * 2, y + h * 2, 1, 1); // off‑screen + } + Layout::FullRight => { + place_window(1, x, y, w, h); + place_window(0, x + w * 2, y + h * 2, 1, 1); + } + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 542036b..0db2943 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -5,5 +5,6 @@ pub mod app; pub mod input; pub mod output; +pub mod layout; pub use app::LesavkaClientApp; diff --git a/server/src/video.rs b/server/src/video.rs index ba20c50..95c484b 100644 --- a/server/src/video.rs +++ b/server/src/video.rs @@ -45,6 +45,7 @@ pub async fn eye_ball( gst::init().context("gst init")?; let desc = format!( + "v4l2src device=\"{dev}\" io-mode=mmap ! \ queue ! tsdemux name=d ! \ d. ! h264parse disable-passthrough=true config-interval=-1 ! \