0.4.0 mostly complete milestone

This commit is contained in:
Brad Stein 2025-06-29 04:54:39 -05:00
parent e93b5a7bde
commit d81eb7c460
6 changed files with 99 additions and 8 deletions

View File

@ -24,6 +24,8 @@ gstreamer-video = "0.23"
gstreamer-gl-wayland = "0.23" gstreamer-gl-wayland = "0.23"
winit = "0.30" winit = "0.30"
raw-window-handle = "0.6" raw-window-handle = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[build-dependencies] [build-dependencies]
prost-build = "0.13" prost-build = "0.13"

View File

@ -1,7 +1,7 @@
// client/src/input/inputs.rs // client/src/input/inputs.rs
use anyhow::{bail, Context, Result}; 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 tokio::{sync::broadcast::Sender, time::{interval, Duration}};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
@ -9,6 +9,7 @@ use lesavka_common::lesavka::{KeyboardReport, MouseReport};
use super::{keyboard::KeyboardAggregator, mouse::MouseAggregator, use super::{keyboard::KeyboardAggregator, mouse::MouseAggregator,
camera::CameraCapture, microphone::MicrophoneCapture}; camera::CameraCapture, microphone::MicrophoneCapture};
use crate::layout::{Layout, apply as apply_layout};
pub struct InputAggregator { pub struct InputAggregator {
kbd_tx: Sender<KeyboardReport>, kbd_tx: Sender<KeyboardReport>,
@ -48,15 +49,11 @@ impl InputAggregator {
continue; continue;
} }
// attempt open // ─── open the event node readwrite *without* unsafe ──────────
let mut dev = match Device::open_with_options( let mut dev = match Device::open(&path) {
&path,
ReadFlag::NORMAL | ReadFlag::BLOCKING,
WriteFlag::NORMAL, // <-- write access needed for EVIOCGRAB
) {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
warn!("❌ open {path}: {e}"); warn!("❌ open {}: {e}", path.display());
continue; continue;
} }
}; };
@ -109,8 +106,11 @@ impl InputAggregator {
pub async fn run(&mut self) -> Result<()> { pub async fn run(&mut self) -> Result<()> {
// Example approach: poll each aggregator in a simple loop // Example approach: poll each aggregator in a simple loop
let mut tick = interval(Duration::from_millis(10)); let mut tick = interval(Duration::from_millis(10));
let mut current = Layout::SideBySide;
loop { loop {
let magic_now = self.keyboards.iter().any(|k| k.magic_grab()); 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; let mut want_kill = false;
for kbd in &mut self.keyboards { for kbd in &mut self.keyboards {
kbd.process_events(); kbd.process_events();
@ -118,6 +118,14 @@ impl InputAggregator {
} }
if magic_now && !self.magic_active { self.toggle_grab(); } 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 { if want_kill {
warn!("🧙 magic chord - killing 🪄 AVADA KEDAVRA!!! 💥💀⚰️"); warn!("🧙 magic chord - killing 🪄 AVADA KEDAVRA!!! 💥💀⚰️");
std::process::exit(0); std::process::exit(0);

View File

@ -89,6 +89,18 @@ impl KeyboardAggregator {
&& self.has_key(KeyCode::KEY_G) && 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 { pub fn magic_kill(&self) -> bool {
self.has_key(KeyCode::KEY_LEFTCTRL) self.has_key(KeyCode::KEY_LEFTCTRL)
&& self.has_key(KeyCode::KEY_ESC) && self.has_key(KeyCode::KEY_ESC)

67
client/src/layout.rs Normal file
View File

@ -0,0 +1,67 @@
// client/src/layout.rs Waylandonly 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 fullscreen, right hidden
FullRight, // right eye fullscreen, left hidden
}
/// Move/resize a window titled “Lesavkaeye{eye}” using `swaymsg`.
fn place_window(eye: u32, x: i32, y: i32, w: i32, h: i32) {
let title = format!("Lesavkaeye{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 **currentlyfocused** 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::<Value>(&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); // offscreen
}
Layout::FullRight => {
place_window(1, x, y, w, h);
place_window(0, x + w * 2, y + h * 2, 1, 1);
}
}
}

View File

@ -5,5 +5,6 @@
pub mod app; pub mod app;
pub mod input; pub mod input;
pub mod output; pub mod output;
pub mod layout;
pub use app::LesavkaClientApp; pub use app::LesavkaClientApp;

View File

@ -45,6 +45,7 @@ pub async fn eye_ball(
gst::init().context("gst init")?; gst::init().context("gst init")?;
let desc = format!( let desc = format!(
"v4l2src device=\"{dev}\" io-mode=mmap ! \ "v4l2src device=\"{dev}\" io-mode=mmap ! \
queue ! tsdemux name=d ! \ queue ! tsdemux name=d ! \
d. ! h264parse disable-passthrough=true config-interval=-1 ! \ d. ! h264parse disable-passthrough=true config-interval=-1 ! \