keyboard progress & refactors
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
|
||||
use strum::EnumCount;
|
||||
|
||||
use crate::config::GeneralConfig;
|
||||
|
||||
#[derive(Debug, Clone, Copy, EnumCount)]
|
||||
#[repr(usize)]
|
||||
pub enum AudioRole {
|
||||
Notification,
|
||||
Keyboard,
|
||||
}
|
||||
|
||||
pub struct AudioOutput {
|
||||
muted_roles: [bool; AudioRole::COUNT],
|
||||
audio_stream: Option<(OutputStream, OutputStreamHandle)>,
|
||||
first_try: bool,
|
||||
}
|
||||
|
||||
impl AudioOutput {
|
||||
pub const fn new(config: &GeneralConfig) -> Self {
|
||||
Self {
|
||||
muted_roles: [
|
||||
//TODO: improve this
|
||||
!config.keyboard_sound_enabled,
|
||||
!config.notifications_sound_enabled,
|
||||
],
|
||||
audio_stream: None,
|
||||
first_try: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
|
||||
if self.audio_stream.is_none() && self.first_try {
|
||||
self.first_try = false;
|
||||
if let Ok((stream, handle)) = OutputStream::try_default() {
|
||||
self.audio_stream = Some((stream, handle));
|
||||
} else {
|
||||
log::error!("Failed to open audio stream. Audio will not work.");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.audio_stream.as_ref().map(|(_, h)| h)
|
||||
}
|
||||
|
||||
pub fn play(&mut self, role: AudioRole, wav_bytes: &'static [u8]) {
|
||||
if self.muted_roles[role as usize] {
|
||||
return;
|
||||
}
|
||||
let Some(handle) = self.get_handle() else {
|
||||
return;
|
||||
};
|
||||
let cursor = Cursor::new(wav_bytes);
|
||||
let source = match Decoder::new_wav(cursor) {
|
||||
Ok(source) => source,
|
||||
Err(e) => {
|
||||
log::error!("Failed to play sound: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = handle.play_raw(source.convert_samples());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
use glam::{IVec2, Vec2};
|
||||
use idmap::{IdMap, idmap};
|
||||
use idmap_derive::IntegerId;
|
||||
use input_linux::{
|
||||
AbsoluteAxis, AbsoluteInfo, AbsoluteInfoSetup, EventKind, InputId, Key, RelativeAxis,
|
||||
UInputHandle,
|
||||
};
|
||||
use libc::{input_event, timeval};
|
||||
use serde::Deserialize;
|
||||
use std::mem::transmute;
|
||||
use std::sync::LazyLock;
|
||||
use std::{fs::File, sync::atomic::AtomicBool};
|
||||
use strum::{EnumIter, EnumString, IntoEnumIterator};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
mod wayland;
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
mod x11;
|
||||
|
||||
pub static USE_UINPUT: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
pub(super) fn initialize() -> Box<dyn HidProvider> {
|
||||
if !USE_UINPUT.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
log::info!("Uinput disabled by user.");
|
||||
return Box::new(DummyProvider {});
|
||||
}
|
||||
|
||||
if let Some(uinput) = UInputProvider::try_new() {
|
||||
log::info!("Initialized uinput.");
|
||||
return Box::new(uinput);
|
||||
}
|
||||
log::error!("Could not create uinput provider. Keyboard/Mouse input will not work!");
|
||||
log::error!("To check if you're in input group, run: id -nG");
|
||||
if let Ok(user) = std::env::var("USER") {
|
||||
log::error!("To add yourself to the input group, run: sudo usermod -aG input {user}");
|
||||
log::error!("After adding yourself to the input group, you will need to reboot.");
|
||||
}
|
||||
Box::new(DummyProvider {})
|
||||
}
|
||||
|
||||
pub trait HidProvider: Sync + Send {
|
||||
fn mouse_move(&mut self, pos: Vec2);
|
||||
fn send_button(&mut self, button: u16, down: bool);
|
||||
fn wheel(&mut self, delta_y: i32, delta_x: i32);
|
||||
fn set_modifiers(&mut self, mods: u8);
|
||||
fn send_key(&self, key: VirtualKey, down: bool);
|
||||
fn set_desktop_extent(&mut self, extent: Vec2);
|
||||
fn set_desktop_origin(&mut self, origin: Vec2);
|
||||
fn commit(&mut self);
|
||||
}
|
||||
|
||||
struct MouseButtonAction {
|
||||
button: u16,
|
||||
down: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseAction {
|
||||
last_requested_pos: Option<Vec2>,
|
||||
pos: Option<Vec2>,
|
||||
button: Option<MouseButtonAction>,
|
||||
scroll: Option<IVec2>,
|
||||
}
|
||||
|
||||
pub struct UInputProvider {
|
||||
keyboard_handle: UInputHandle<File>,
|
||||
mouse_handle: UInputHandle<File>,
|
||||
desktop_extent: Vec2,
|
||||
desktop_origin: Vec2,
|
||||
cur_modifiers: u8,
|
||||
current_action: MouseAction,
|
||||
}
|
||||
|
||||
pub struct DummyProvider;
|
||||
|
||||
pub const MOUSE_LEFT: u16 = 0x110;
|
||||
pub const MOUSE_RIGHT: u16 = 0x111;
|
||||
pub const MOUSE_MIDDLE: u16 = 0x112;
|
||||
|
||||
const MOUSE_EXTENT: f32 = 32768.;
|
||||
|
||||
const EV_SYN: u16 = 0x0;
|
||||
const EV_KEY: u16 = 0x1;
|
||||
const EV_REL: u16 = 0x2;
|
||||
const EV_ABS: u16 = 0x3;
|
||||
|
||||
impl UInputProvider {
|
||||
fn try_new() -> Option<Self> {
|
||||
let keyboard_file = File::create("/dev/uinput").ok()?;
|
||||
let keyboard_handle = UInputHandle::new(keyboard_file);
|
||||
|
||||
let mouse_file = File::create("/dev/uinput").ok()?;
|
||||
let mouse_handle = UInputHandle::new(mouse_file);
|
||||
|
||||
let kbd_id = InputId {
|
||||
bustype: 0x03,
|
||||
vendor: 0x4711,
|
||||
product: 0x0829,
|
||||
version: 5,
|
||||
};
|
||||
let mouse_id = InputId {
|
||||
bustype: 0x03,
|
||||
vendor: 0x4711,
|
||||
product: 0x0830,
|
||||
version: 5,
|
||||
};
|
||||
let kbd_name = b"WlxOverlay-S Keyboard\0";
|
||||
let mouse_name = b"WlxOverlay-S Mouse\0";
|
||||
|
||||
let abs_info = vec![
|
||||
AbsoluteInfoSetup {
|
||||
axis: input_linux::AbsoluteAxis::X,
|
||||
info: AbsoluteInfo {
|
||||
value: 0,
|
||||
minimum: 0,
|
||||
maximum: MOUSE_EXTENT as _,
|
||||
fuzz: 0,
|
||||
flat: 0,
|
||||
resolution: 10,
|
||||
},
|
||||
},
|
||||
AbsoluteInfoSetup {
|
||||
axis: input_linux::AbsoluteAxis::Y,
|
||||
info: AbsoluteInfo {
|
||||
value: 0,
|
||||
minimum: 0,
|
||||
maximum: MOUSE_EXTENT as _,
|
||||
fuzz: 0,
|
||||
flat: 0,
|
||||
resolution: 10,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
keyboard_handle.set_evbit(EventKind::Key).ok()?;
|
||||
for key in VirtualKey::iter() {
|
||||
let mapped_key: Key = unsafe { std::mem::transmute((key as u16) - 8) };
|
||||
keyboard_handle.set_keybit(mapped_key).ok()?;
|
||||
}
|
||||
|
||||
keyboard_handle.create(&kbd_id, kbd_name, 0, &[]).ok()?;
|
||||
|
||||
mouse_handle.set_evbit(EventKind::Absolute).ok()?;
|
||||
mouse_handle.set_evbit(EventKind::Relative).ok()?;
|
||||
mouse_handle.set_absbit(AbsoluteAxis::X).ok()?;
|
||||
mouse_handle.set_absbit(AbsoluteAxis::Y).ok()?;
|
||||
mouse_handle.set_relbit(RelativeAxis::WheelHiRes).ok()?;
|
||||
mouse_handle
|
||||
.set_relbit(RelativeAxis::HorizontalWheelHiRes)
|
||||
.ok()?;
|
||||
mouse_handle.set_evbit(EventKind::Key).ok()?;
|
||||
|
||||
for btn in MOUSE_LEFT..=MOUSE_MIDDLE {
|
||||
let mouse_btn: Key = unsafe { transmute(btn) };
|
||||
mouse_handle.set_keybit(mouse_btn).ok()?;
|
||||
}
|
||||
mouse_handle
|
||||
.create(&mouse_id, mouse_name, 0, &abs_info)
|
||||
.ok()?;
|
||||
|
||||
Some(Self {
|
||||
keyboard_handle,
|
||||
mouse_handle,
|
||||
desktop_extent: Vec2::ZERO,
|
||||
desktop_origin: Vec2::ZERO,
|
||||
current_action: MouseAction::default(),
|
||||
cur_modifiers: 0,
|
||||
})
|
||||
}
|
||||
fn send_button_internal(&self, button: u16, down: bool) {
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_KEY, button, down.into()),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.mouse_handle.write(&events) {
|
||||
log::error!("send_button: {res}");
|
||||
}
|
||||
}
|
||||
fn mouse_move_internal(&mut self, pos: Vec2) {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("Mouse move: {pos:?}");
|
||||
|
||||
let pos = (pos - self.desktop_origin) * (MOUSE_EXTENT / self.desktop_extent);
|
||||
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_ABS, AbsoluteAxis::X as _, pos.x as i32),
|
||||
new_event(time, EV_ABS, AbsoluteAxis::Y as _, pos.y as i32),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.mouse_handle.write(&events) {
|
||||
log::error!("{res}");
|
||||
}
|
||||
}
|
||||
fn wheel_internal(&self, delta_y: i32, delta_x: i32) {
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_REL, RelativeAxis::WheelHiRes as _, delta_y),
|
||||
new_event(
|
||||
time,
|
||||
EV_REL,
|
||||
RelativeAxis::HorizontalWheelHiRes as _,
|
||||
delta_x,
|
||||
),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.mouse_handle.write(&events) {
|
||||
log::error!("wheel: {res}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HidProvider for UInputProvider {
|
||||
fn set_modifiers(&mut self, modifiers: u8) {
|
||||
let changed = self.cur_modifiers ^ modifiers;
|
||||
for i in 0..8 {
|
||||
let m = 1 << i;
|
||||
if changed & m != 0 {
|
||||
if let Some(vk) = MODS_TO_KEYS.get(m).into_iter().flatten().next() {
|
||||
self.send_key(*vk, modifiers & m != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cur_modifiers = modifiers;
|
||||
}
|
||||
fn send_key(&self, key: VirtualKey, down: bool) {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("send_key: {key:?} {down}");
|
||||
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_KEY, (key as u16) - 8, down.into()),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.keyboard_handle.write(&events) {
|
||||
log::error!("send_key: {res}");
|
||||
}
|
||||
}
|
||||
fn set_desktop_extent(&mut self, extent: Vec2) {
|
||||
self.desktop_extent = extent;
|
||||
}
|
||||
fn set_desktop_origin(&mut self, origin: Vec2) {
|
||||
self.desktop_origin = origin;
|
||||
}
|
||||
fn mouse_move(&mut self, pos: Vec2) {
|
||||
if self.current_action.pos.is_none() && self.current_action.scroll.is_none() {
|
||||
self.current_action.pos = Some(pos);
|
||||
}
|
||||
self.current_action.last_requested_pos = Some(pos);
|
||||
}
|
||||
fn send_button(&mut self, button: u16, down: bool) {
|
||||
if self.current_action.button.is_none() {
|
||||
self.current_action.button = Some(MouseButtonAction { button, down });
|
||||
self.current_action.pos = self.current_action.last_requested_pos;
|
||||
}
|
||||
}
|
||||
fn wheel(&mut self, delta_y: i32, delta_x: i32) {
|
||||
if self.current_action.scroll.is_none() {
|
||||
self.current_action.scroll = Some(IVec2::new(delta_x, delta_y));
|
||||
// Pass mouse motion events only if not scrolling
|
||||
// (allows scrolling on all Chromium-based applications)
|
||||
self.current_action.pos = None;
|
||||
}
|
||||
}
|
||||
fn commit(&mut self) {
|
||||
if let Some(pos) = self.current_action.pos.take() {
|
||||
self.mouse_move_internal(pos);
|
||||
}
|
||||
if let Some(button) = self.current_action.button.take() {
|
||||
self.send_button_internal(button.button, button.down);
|
||||
}
|
||||
if let Some(scroll) = self.current_action.scroll.take() {
|
||||
self.wheel_internal(scroll.y, scroll.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HidProvider for DummyProvider {
|
||||
fn mouse_move(&mut self, _pos: Vec2) {}
|
||||
fn send_button(&mut self, _button: u16, _down: bool) {}
|
||||
fn wheel(&mut self, _delta_y: i32, _delta_x: i32) {}
|
||||
fn set_modifiers(&mut self, _modifiers: u8) {}
|
||||
fn send_key(&self, _key: VirtualKey, _down: bool) {}
|
||||
fn set_desktop_extent(&mut self, _extent: Vec2) {}
|
||||
fn set_desktop_origin(&mut self, _origin: Vec2) {}
|
||||
fn commit(&mut self) {}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_time() -> timeval {
|
||||
let mut time = timeval {
|
||||
tv_sec: 0,
|
||||
tv_usec: 0,
|
||||
};
|
||||
unsafe { libc::gettimeofday(&mut time, std::ptr::null_mut()) };
|
||||
time
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn new_event(time: timeval, type_: u16, code: u16, value: i32) -> input_event {
|
||||
input_event {
|
||||
time,
|
||||
type_,
|
||||
code,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
pub type KeyModifier = u8;
|
||||
pub const SHIFT: KeyModifier = 0x01;
|
||||
pub const CAPS_LOCK: KeyModifier = 0x02;
|
||||
pub const CTRL: KeyModifier = 0x04;
|
||||
pub const ALT: KeyModifier = 0x08;
|
||||
pub const NUM_LOCK: KeyModifier = 0x10;
|
||||
pub const SUPER: KeyModifier = 0x40;
|
||||
pub const META: KeyModifier = 0x80;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter)]
|
||||
pub enum VirtualKey {
|
||||
Escape = 9,
|
||||
N1, // number row
|
||||
N2,
|
||||
N3,
|
||||
N4,
|
||||
N5,
|
||||
N6,
|
||||
N7,
|
||||
N8,
|
||||
N9,
|
||||
N0,
|
||||
Minus,
|
||||
Plus,
|
||||
BackSpace,
|
||||
Tab,
|
||||
Q,
|
||||
W,
|
||||
E,
|
||||
R,
|
||||
T,
|
||||
Y,
|
||||
U,
|
||||
I,
|
||||
O,
|
||||
P,
|
||||
Oem4, // [ {
|
||||
Oem6, // ] }
|
||||
Return,
|
||||
LCtrl,
|
||||
A,
|
||||
S,
|
||||
D,
|
||||
F,
|
||||
G,
|
||||
H,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
Oem1, // ; :
|
||||
Oem7, // ' "
|
||||
Oem3, // ` ~
|
||||
LShift,
|
||||
Oem5, // \ |
|
||||
Z,
|
||||
X,
|
||||
C,
|
||||
V,
|
||||
B,
|
||||
N,
|
||||
M,
|
||||
Comma, // , <
|
||||
Period, // . >
|
||||
Oem2, // / ?
|
||||
RShift,
|
||||
KP_Multiply,
|
||||
LAlt,
|
||||
Space,
|
||||
Caps,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
NumLock,
|
||||
Scroll,
|
||||
KP_7, // KeyPad
|
||||
KP_8,
|
||||
KP_9,
|
||||
KP_Subtract,
|
||||
KP_4,
|
||||
KP_5,
|
||||
KP_6,
|
||||
KP_Add,
|
||||
KP_1,
|
||||
KP_2,
|
||||
KP_3,
|
||||
KP_0,
|
||||
KP_Decimal,
|
||||
Oem102 = 94, // Optional key usually between LShift and Z
|
||||
F11,
|
||||
F12,
|
||||
AbntC1,
|
||||
Katakana,
|
||||
Hiragana,
|
||||
Henkan,
|
||||
Kana,
|
||||
Muhenkan,
|
||||
KP_Enter = 104,
|
||||
RCtrl,
|
||||
KP_Divide,
|
||||
Print,
|
||||
Meta, // Right Alt aka AltGr
|
||||
Home = 110,
|
||||
Up,
|
||||
Prior,
|
||||
Left,
|
||||
Right,
|
||||
End,
|
||||
Down,
|
||||
Next,
|
||||
Insert,
|
||||
Delete,
|
||||
XF86AudioMute = 121,
|
||||
XF86AudioLowerVolume,
|
||||
XF86AudioRaiseVolume,
|
||||
Pause = 127,
|
||||
AbntC2 = 129,
|
||||
Hangul,
|
||||
Hanja,
|
||||
LSuper = 133,
|
||||
RSuper,
|
||||
Menu,
|
||||
Help = 146,
|
||||
XF86MenuKB,
|
||||
XF86Sleep = 150,
|
||||
XF86Xfer = 155,
|
||||
XF86Launch1,
|
||||
XF86Launch2,
|
||||
XF86WWW,
|
||||
XF86Mail = 163,
|
||||
XF86Favorites,
|
||||
XF86MyComputer,
|
||||
XF86Back,
|
||||
XF86Forward,
|
||||
XF86AudioNext = 171,
|
||||
XF86AudioPlay,
|
||||
XF86AudioPrev,
|
||||
XF86AudioStop,
|
||||
XF86HomePage = 180,
|
||||
XF86Reload,
|
||||
F13 = 191,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
F21,
|
||||
F22,
|
||||
F23,
|
||||
F24,
|
||||
Hyper = 207,
|
||||
XF86Launch3,
|
||||
XF86Launch4,
|
||||
XF86LaunchB,
|
||||
XF86Search = 225,
|
||||
}
|
||||
|
||||
pub static KEYS_TO_MODS: LazyLock<IdMap<VirtualKey, KeyModifier>> = LazyLock::new(|| {
|
||||
idmap! {
|
||||
VirtualKey::LShift => SHIFT,
|
||||
VirtualKey::RShift => SHIFT,
|
||||
VirtualKey::Caps => CAPS_LOCK,
|
||||
VirtualKey::LCtrl => CTRL,
|
||||
VirtualKey::RCtrl => CTRL,
|
||||
VirtualKey::LAlt => ALT,
|
||||
VirtualKey::NumLock => NUM_LOCK,
|
||||
VirtualKey::LSuper => SUPER,
|
||||
VirtualKey::RSuper => SUPER,
|
||||
VirtualKey::Meta => META,
|
||||
}
|
||||
});
|
||||
|
||||
pub static MODS_TO_KEYS: LazyLock<IdMap<KeyModifier, Vec<VirtualKey>>> = LazyLock::new(|| {
|
||||
idmap! {
|
||||
SHIFT => vec![VirtualKey::LShift, VirtualKey::RShift],
|
||||
CAPS_LOCK => vec![VirtualKey::Caps],
|
||||
CTRL => vec![VirtualKey::LCtrl, VirtualKey::RCtrl],
|
||||
ALT => vec![VirtualKey::LAlt],
|
||||
NUM_LOCK => vec![VirtualKey::NumLock],
|
||||
SUPER => vec![VirtualKey::LSuper, VirtualKey::RSuper],
|
||||
META => vec![VirtualKey::Meta],
|
||||
}
|
||||
});
|
||||
|
||||
pub enum KeyType {
|
||||
Symbol,
|
||||
NumPad,
|
||||
Special,
|
||||
Other,
|
||||
}
|
||||
|
||||
macro_rules! key_between {
|
||||
($key:expr, $start:expr, $end:expr) => {
|
||||
$key as u32 >= $start as u32 && $key as u32 <= $end as u32
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! key_is {
|
||||
($key:expr, $val:expr) => {
|
||||
$key as u32 == $val as u32
|
||||
};
|
||||
}
|
||||
|
||||
pub const fn get_key_type(key: VirtualKey) -> KeyType {
|
||||
if key_between!(key, VirtualKey::N1, VirtualKey::Plus)
|
||||
|| key_between!(key, VirtualKey::Q, VirtualKey::Oem6)
|
||||
|| key_between!(key, VirtualKey::A, VirtualKey::Oem3)
|
||||
|| key_between!(key, VirtualKey::Oem5, VirtualKey::Oem2)
|
||||
|| key_is!(key, VirtualKey::Oem102)
|
||||
{
|
||||
KeyType::Symbol
|
||||
} else if key_between!(key, VirtualKey::KP_7, VirtualKey::KP_0)
|
||||
&& !key_is!(key, VirtualKey::KP_Subtract)
|
||||
&& !key_is!(key, VirtualKey::KP_Add)
|
||||
{
|
||||
KeyType::NumPad
|
||||
} else if matches!(
|
||||
key,
|
||||
VirtualKey::BackSpace
|
||||
| VirtualKey::Down
|
||||
| VirtualKey::Left
|
||||
| VirtualKey::Menu
|
||||
| VirtualKey::Return
|
||||
| VirtualKey::KP_Enter
|
||||
| VirtualKey::Right
|
||||
| VirtualKey::LShift
|
||||
| VirtualKey::RShift
|
||||
| VirtualKey::LSuper
|
||||
| VirtualKey::RSuper
|
||||
| VirtualKey::Tab
|
||||
| VirtualKey::Up
|
||||
) {
|
||||
KeyType::Special
|
||||
} else {
|
||||
KeyType::Other
|
||||
}
|
||||
}
|
||||
|
||||
pub struct XkbKeymap {
|
||||
pub keymap: xkb::Keymap,
|
||||
}
|
||||
|
||||
impl XkbKeymap {
|
||||
pub fn label_for_key(&self, key: VirtualKey, modifier: KeyModifier) -> String {
|
||||
let mut state = xkb::State::new(&self.keymap);
|
||||
if modifier > 0 {
|
||||
if let Some(mod_key) = MODS_TO_KEYS.get(modifier) {
|
||||
state.update_key(
|
||||
xkb::Keycode::from(mod_key[0] as u32),
|
||||
xkb::KeyDirection::Down,
|
||||
);
|
||||
}
|
||||
}
|
||||
state.key_get_utf8(xkb::Keycode::from(key as u32))
|
||||
}
|
||||
|
||||
pub fn has_altgr(&self) -> bool {
|
||||
let state0 = xkb::State::new(&self.keymap);
|
||||
let mut state1 = xkb::State::new(&self.keymap);
|
||||
state1.update_key(
|
||||
xkb::Keycode::from(VirtualKey::Meta as u32),
|
||||
xkb::KeyDirection::Down,
|
||||
);
|
||||
|
||||
for key in [
|
||||
VirtualKey::N0,
|
||||
VirtualKey::N1,
|
||||
VirtualKey::N2,
|
||||
VirtualKey::N3,
|
||||
VirtualKey::N4,
|
||||
VirtualKey::N5,
|
||||
VirtualKey::N6,
|
||||
VirtualKey::N7,
|
||||
VirtualKey::N8,
|
||||
VirtualKey::N9,
|
||||
] {
|
||||
let sym0 = state0.key_get_one_sym(xkb::Keycode::from(key as u32));
|
||||
let sym1 = state1.key_get_one_sym(xkb::Keycode::from(key as u32));
|
||||
if sym0 != sym1 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use wayland::get_keymap_wl;
|
||||
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {
|
||||
anyhow::bail!("Wayland support not enabled.")
|
||||
}
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
pub use x11::get_keymap_x11;
|
||||
|
||||
#[cfg(not(feature = "x11"))]
|
||||
pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
|
||||
anyhow::bail!("X11 support not enabled.")
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use wlx_capture::wayland::wayland_client::{
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
protocol::{
|
||||
wl_keyboard::{self, WlKeyboard},
|
||||
wl_registry::WlRegistry,
|
||||
wl_seat::{self, Capability, WlSeat},
|
||||
},
|
||||
Connection, Dispatch, Proxy, QueueHandle,
|
||||
};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use super::XkbKeymap;
|
||||
|
||||
struct WlKeymapHandler {
|
||||
seat: WlSeat,
|
||||
keyboard: Option<WlKeyboard>,
|
||||
keymap: Option<XkbKeymap>,
|
||||
}
|
||||
|
||||
impl Drop for WlKeymapHandler {
|
||||
fn drop(&mut self) {
|
||||
if let Some(keyboard) = &self.keyboard {
|
||||
keyboard.release();
|
||||
}
|
||||
self.seat.release();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {
|
||||
let connection = Connection::connect_to_env()?;
|
||||
let (globals, mut queue) = registry_queue_init::<WlKeymapHandler>(&connection)?;
|
||||
let qh = queue.handle();
|
||||
let seat: WlSeat = globals
|
||||
.bind(&qh, 4..=9, ())
|
||||
.expect(WlSeat::interface().name);
|
||||
|
||||
let mut me = WlKeymapHandler {
|
||||
seat,
|
||||
keyboard: None,
|
||||
keymap: None,
|
||||
};
|
||||
|
||||
// this gets us the wl_seat
|
||||
let _ = queue.blocking_dispatch(&mut me);
|
||||
|
||||
// this gets us the wl_keyboard
|
||||
let _ = queue.blocking_dispatch(&mut me);
|
||||
|
||||
me.keymap
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not load keymap"))
|
||||
}
|
||||
|
||||
impl Dispatch<WlRegistry, GlobalListContents> for WlKeymapHandler {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlRegistry,
|
||||
_event: <WlRegistry as Proxy>::Event,
|
||||
_data: &GlobalListContents,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlSeat, ()> for WlKeymapHandler {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
proxy: &WlSeat,
|
||||
event: <WlSeat as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_seat::Event::Capabilities { capabilities } => {
|
||||
let capability = capabilities
|
||||
.into_result()
|
||||
.unwrap_or(wl_seat::Capability::empty());
|
||||
if capability.contains(Capability::Keyboard) {
|
||||
state.keyboard = Some(proxy.get_keyboard(qhandle, ()));
|
||||
}
|
||||
}
|
||||
wl_seat::Event::Name { name } => {
|
||||
log::debug!("Using WlSeat: {name}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlKeyboard, ()> for WlKeymapHandler {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_proxy: &WlKeyboard,
|
||||
event: <WlKeyboard as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_keyboard::Event::Keymap { format, fd, size } => {
|
||||
let format = format
|
||||
.into_result()
|
||||
.unwrap_or(wl_keyboard::KeymapFormat::NoKeymap);
|
||||
|
||||
if matches!(format, wl_keyboard::KeymapFormat::XkbV1) {
|
||||
let context = xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES);
|
||||
let maybe_keymap = unsafe {
|
||||
xkb::Keymap::new_from_fd(
|
||||
&context,
|
||||
fd,
|
||||
size as _,
|
||||
xkb::KEYMAP_FORMAT_TEXT_V1,
|
||||
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||
)
|
||||
};
|
||||
|
||||
match maybe_keymap {
|
||||
Ok(Some(keymap)) => {
|
||||
state.keymap = Some(XkbKeymap { keymap });
|
||||
}
|
||||
Ok(None) => {
|
||||
log::error!("Could not load keymap: no keymap");
|
||||
log::error!("Default layout will be used.");
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Could not load keymap: {err}");
|
||||
log::error!("Default layout will be used.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::RepeatInfo { rate, delay } => {
|
||||
log::debug!("WlKeyboard RepeatInfo rate: {rate}, delay: {delay}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use xkbcommon::xkb::{
|
||||
self,
|
||||
x11::{
|
||||
get_core_keyboard_device_id, keymap_new_from_device, setup_xkb_extension,
|
||||
SetupXkbExtensionFlags, MIN_MAJOR_XKB_VERSION, MIN_MINOR_XKB_VERSION,
|
||||
},
|
||||
};
|
||||
|
||||
use super::XkbKeymap;
|
||||
|
||||
pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
|
||||
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||
|
||||
let (conn, _) = xcb::Connection::connect(None)?;
|
||||
setup_xkb_extension(
|
||||
&conn,
|
||||
MIN_MAJOR_XKB_VERSION,
|
||||
MIN_MINOR_XKB_VERSION,
|
||||
SetupXkbExtensionFlags::NoFlags,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
);
|
||||
|
||||
let device_id = get_core_keyboard_device_id(&conn);
|
||||
if device_id == -1 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"get_core_keyboard_device_id returned -1. Check your XKB installation."
|
||||
));
|
||||
}
|
||||
let keymap = keymap_new_from_device(&context, &conn, device_id, xkb::KEYMAP_COMPILE_NO_FLAGS);
|
||||
|
||||
Ok(XkbKeymap { keymap })
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use super::hid::{self, HidProvider, VirtualKey};
|
||||
use crate::overlays::wayvr::WayVRData;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum KeyboardFocus {
|
||||
PhysicalScreen,
|
||||
|
||||
#[allow(dead_code)] // Not available if "wayvr" feature is disabled
|
||||
WayVR, // (for now without wayland window id data, it's handled internally),
|
||||
}
|
||||
|
||||
pub struct HidWrapper {
|
||||
pub keyboard_focus: KeyboardFocus,
|
||||
pub inner: Box<dyn HidProvider>,
|
||||
pub wayvr: Option<Rc<RefCell<WayVRData>>>, // Dynamically created if requested
|
||||
}
|
||||
|
||||
impl HidWrapper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
keyboard_focus: KeyboardFocus::PhysicalScreen,
|
||||
inner: hid::initialize(),
|
||||
wayvr: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_key_routed(&self, key: VirtualKey, down: bool) {
|
||||
match self.keyboard_focus {
|
||||
KeyboardFocus::PhysicalScreen => self.inner.send_key(key, down),
|
||||
KeyboardFocus::WayVR =>
|
||||
{
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = &self.wayvr {
|
||||
wayvr.borrow_mut().data.state.send_key(key as u32, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_modifiers_routed(&mut self, mods: u8) {
|
||||
match self.keyboard_focus {
|
||||
KeyboardFocus::PhysicalScreen => self.inner.set_modifiers(mods),
|
||||
KeyboardFocus::WayVR =>
|
||||
{
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = &self.wayvr {
|
||||
wayvr.borrow_mut().data.state.set_modifiers(mods);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod audio;
|
||||
pub mod hid;
|
||||
pub mod input;
|
||||
pub mod notifications;
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
pub mod osc;
|
||||
@@ -0,0 +1,294 @@
|
||||
#[allow(clippy::all)]
|
||||
mod notifications_dbus;
|
||||
|
||||
use dbus::{
|
||||
arg::{PropMap, Variant},
|
||||
blocking::Connection,
|
||||
channel::MatchingReceiver,
|
||||
message::MatchRule,
|
||||
};
|
||||
use notifications_dbus::OrgFreedesktopNotifications;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
overlays::toast::{Toast, ToastTopic},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub struct NotificationManager {
|
||||
rx_toast: mpsc::Receiver<Toast>,
|
||||
tx_toast: mpsc::SyncSender<Toast>,
|
||||
dbus_data: Option<Connection>,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl NotificationManager {
|
||||
pub fn new() -> Self {
|
||||
let (tx_toast, rx_toast) = mpsc::sync_channel(10);
|
||||
Self {
|
||||
rx_toast,
|
||||
tx_toast,
|
||||
dbus_data: None,
|
||||
running: Arc::new(AtomicBool::new(true)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit_pending(&self, app: &mut AppState) {
|
||||
if let Some(c) = &self.dbus_data {
|
||||
let _ = c.process(Duration::ZERO);
|
||||
}
|
||||
|
||||
if app.session.config.notifications_enabled {
|
||||
self.rx_toast.try_iter().for_each(|toast| {
|
||||
toast.submit(app);
|
||||
});
|
||||
} else {
|
||||
// consume without submitting
|
||||
self.rx_toast.try_iter().last();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_dbus(&mut self) {
|
||||
let c = match Connection::new_session() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to connect to dbus. Desktop notifications will not work. Cause: {e:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut rule = MatchRule::new_method_call();
|
||||
rule.member = Some("Notify".into());
|
||||
rule.interface = Some("org.freedesktop.Notifications".into());
|
||||
rule.path = Some("/org/freedesktop/Notifications".into());
|
||||
rule.eavesdrop = true;
|
||||
|
||||
let proxy = c.with_proxy(
|
||||
"org.freedesktop.DBus",
|
||||
"/org/freedesktop/DBus",
|
||||
Duration::from_millis(5000),
|
||||
);
|
||||
let result: Result<(), dbus::Error> = proxy.method_call(
|
||||
"org.freedesktop.DBus.Monitoring",
|
||||
"BecomeMonitor",
|
||||
(vec![rule.match_str()], 0u32),
|
||||
);
|
||||
|
||||
if matches!(result, Ok(())) {
|
||||
let sender = self.tx_toast.clone();
|
||||
c.start_receive(
|
||||
rule,
|
||||
Box::new(move |msg, _| {
|
||||
if let Ok(toast) = parse_dbus(&msg) {
|
||||
match sender.try_send(toast) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}),
|
||||
);
|
||||
log::info!("Listening to DBus notifications via BecomeMonitor.");
|
||||
} else {
|
||||
let rule_with_eavesdrop = {
|
||||
let mut rule = rule.clone();
|
||||
rule.eavesdrop = true;
|
||||
rule
|
||||
};
|
||||
|
||||
let sender2 = self.tx_toast.clone();
|
||||
let result = c.add_match(rule_with_eavesdrop, move |(): (), _, msg| {
|
||||
if let Ok(toast) = parse_dbus(msg) {
|
||||
match sender2.try_send(toast) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::info!("Listening to DBus notifications via eavesdrop.");
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Failed to add DBus match. Desktop notifications will not work.",);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dbus_data = Some(c);
|
||||
}
|
||||
|
||||
pub fn run_udp(&mut self) {
|
||||
let sender = self.tx_toast.clone();
|
||||
let running = self.running.clone();
|
||||
let _ = std::thread::spawn(move || {
|
||||
let addr = "127.0.0.1:42069";
|
||||
let socket = match std::net::UdpSocket::bind(addr) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to bind notification socket @ {addr}: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = socket.set_read_timeout(Some(Duration::from_millis(200))) {
|
||||
log::error!("Failed to set read timeout: {err:?}");
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 1024 * 16]; // vrcx embeds icons as b64
|
||||
|
||||
while running.load(Ordering::Relaxed) {
|
||||
if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) {
|
||||
let json_str = match std::str::from_utf8(&buf[..num_bytes]) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive notification message: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse notification message: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if msg.messageType != 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toast = Toast::new(
|
||||
ToastTopic::XSNotification,
|
||||
msg.title,
|
||||
msg.content.unwrap_or(String::new()),
|
||||
)
|
||||
.with_timeout(msg.timeout.unwrap_or(5.))
|
||||
.with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0,
|
||||
|
||||
match sender.try_send(toast) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("Notification listener stopped.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NotificationManager {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DbusNotificationSender {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl DbusNotificationSender {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
connection: Connection::new_session()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn notify_send(
|
||||
&self,
|
||||
summary: &str,
|
||||
body: &str,
|
||||
urgency: u8,
|
||||
timeout: i32,
|
||||
replaces_id: u32,
|
||||
transient: bool,
|
||||
) -> anyhow::Result<u32> {
|
||||
let proxy = self.connection.with_proxy(
|
||||
"org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications",
|
||||
Duration::from_millis(1000),
|
||||
);
|
||||
|
||||
let mut hints = PropMap::new();
|
||||
hints.insert("urgency".to_string(), Variant(Box::new(urgency)));
|
||||
hints.insert("transient".to_string(), Variant(Box::new(transient)));
|
||||
|
||||
Ok(proxy.notify(
|
||||
"WlxOverlay-S",
|
||||
replaces_id,
|
||||
"",
|
||||
summary,
|
||||
body,
|
||||
vec![],
|
||||
hints,
|
||||
timeout,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn notify_close(&self, id: u32) -> anyhow::Result<()> {
|
||||
let proxy = self.connection.with_proxy(
|
||||
"org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications",
|
||||
Duration::from_millis(1000),
|
||||
);
|
||||
|
||||
proxy.close_notification(id)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
|
||||
let mut args = msg.iter_init();
|
||||
let app_name: String = args.read()?;
|
||||
let _replaces_id: u32 = args.read()?;
|
||||
let _app_icon: String = args.read()?;
|
||||
let summary: String = args.read()?;
|
||||
let body: String = args.read()?;
|
||||
|
||||
let title = if summary.is_empty() {
|
||||
app_name
|
||||
} else {
|
||||
summary
|
||||
};
|
||||
|
||||
Ok(Toast::new(ToastTopic::DesktopNotification, title, body)
|
||||
.with_timeout(5.0)
|
||||
.with_opacity(1.0))
|
||||
// leave the audio part to the desktop env
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct XsoMessage {
|
||||
messageType: i32,
|
||||
index: Option<i32>,
|
||||
volume: Option<f32>,
|
||||
audioPath: Option<String>,
|
||||
timeout: Option<f32>,
|
||||
title: String,
|
||||
content: Option<String>,
|
||||
icon: Option<String>,
|
||||
height: Option<f32>,
|
||||
opacity: Option<f32>,
|
||||
useBase64Icon: Option<bool>,
|
||||
sourceApp: Option<String>,
|
||||
alwaysShow: Option<bool>,
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
// This code was autogenerated with `dbus-codegen-rust -g -m None -d org.freedesktop.Notifications -p /org/freedesktop/Notifications`, see https://github.com/diwic/dbus-rs
|
||||
use dbus;
|
||||
#[allow(unused_imports)]
|
||||
use dbus::arg;
|
||||
use dbus::blocking;
|
||||
|
||||
pub trait OrgFreedesktopDBusProperties {
|
||||
fn get<R0: for<'b> arg::Get<'b> + 'static>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
) -> Result<R0, dbus::Error>;
|
||||
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error>;
|
||||
fn set<I2: arg::Arg + arg::Append>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
value: I2,
|
||||
) -> Result<(), dbus::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
pub interface_name: String,
|
||||
pub changed_properties: arg::PropMap,
|
||||
pub invalidated_properties: Vec<String>,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.interface_name, i);
|
||||
arg::RefArg::append(&self.changed_properties, i);
|
||||
arg::RefArg::append(&self.invalidated_properties, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
interface_name: i.read()?,
|
||||
changed_properties: i.read()?,
|
||||
invalidated_properties: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
const NAME: &'static str = "PropertiesChanged";
|
||||
const INTERFACE: &'static str = "org.freedesktop.DBus.Properties";
|
||||
}
|
||||
|
||||
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusProperties
|
||||
for blocking::Proxy<'_, C>
|
||||
{
|
||||
fn get<R0: for<'b> arg::Get<'b> + 'static>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
) -> Result<R0, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"Get",
|
||||
(interface_name, property_name),
|
||||
)
|
||||
.and_then(|r: (arg::Variant<R0>,)| Ok((r.0).0))
|
||||
}
|
||||
|
||||
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"GetAll",
|
||||
(interface_name,),
|
||||
)
|
||||
.and_then(|r: (arg::PropMap,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn set<I2: arg::Arg + arg::Append>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
value: I2,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"Set",
|
||||
(interface_name, property_name, arg::Variant(value)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OrgFreedesktopDBusIntrospectable {
|
||||
fn introspect(&self) -> Result<String, dbus::Error>;
|
||||
}
|
||||
|
||||
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusIntrospectable
|
||||
for blocking::Proxy<'_, C>
|
||||
{
|
||||
fn introspect(&self) -> Result<String, dbus::Error> {
|
||||
self.method_call("org.freedesktop.DBus.Introspectable", "Introspect", ())
|
||||
.and_then(|r: (String,)| Ok(r.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OrgFreedesktopDBusPeer {
|
||||
fn ping(&self) -> Result<(), dbus::Error>;
|
||||
fn get_machine_id(&self) -> Result<String, dbus::Error>;
|
||||
}
|
||||
|
||||
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusPeer
|
||||
for blocking::Proxy<'_, C>
|
||||
{
|
||||
fn ping(&self) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.DBus.Peer", "Ping", ())
|
||||
}
|
||||
|
||||
fn get_machine_id(&self) -> Result<String, dbus::Error> {
|
||||
self.method_call("org.freedesktop.DBus.Peer", "GetMachineId", ())
|
||||
.and_then(|r: (String,)| Ok(r.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OrgFreedesktopNotifications {
|
||||
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error>;
|
||||
fn toggle_dnd(&self) -> Result<bool, dbus::Error>;
|
||||
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error>;
|
||||
fn get_dnd(&self) -> Result<bool, dbus::Error>;
|
||||
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error>;
|
||||
fn close_all_notifications(&self) -> Result<(), dbus::Error>;
|
||||
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error>;
|
||||
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error>;
|
||||
fn notify(
|
||||
&self,
|
||||
app_name: &str,
|
||||
replaces_id: u32,
|
||||
app_icon: &str,
|
||||
summary: &str,
|
||||
body: &str,
|
||||
actions: Vec<&str>,
|
||||
hints: arg::PropMap,
|
||||
expire_timeout: i32,
|
||||
) -> Result<u32, dbus::Error>;
|
||||
fn close_notification(&self, id: u32) -> Result<(), dbus::Error>;
|
||||
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error>;
|
||||
fn dnd(&self) -> Result<bool, dbus::Error>;
|
||||
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsOnDndToggle {
|
||||
pub dnd: bool,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsOnDndToggle {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.dnd, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsOnDndToggle {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self { dnd: i.read()? })
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsOnDndToggle {
|
||||
const NAME: &'static str = "OnDndToggle";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsNotificationClosed {
|
||||
pub id: u32,
|
||||
pub reason: u32,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationClosed {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.id, i);
|
||||
arg::RefArg::append(&self.reason, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationClosed {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
id: i.read()?,
|
||||
reason: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationClosed {
|
||||
const NAME: &'static str = "NotificationClosed";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsActionInvoked {
|
||||
pub id: u32,
|
||||
pub action_key: String,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsActionInvoked {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.id, i);
|
||||
arg::RefArg::append(&self.action_key, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsActionInvoked {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
id: i.read()?,
|
||||
action_key: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsActionInvoked {
|
||||
const NAME: &'static str = "ActionInvoked";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsNotificationReplied {
|
||||
pub id: u32,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationReplied {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.id, i);
|
||||
arg::RefArg::append(&self.text, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationReplied {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
id: i.read()?,
|
||||
text: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationReplied {
|
||||
const NAME: &'static str = "NotificationReplied";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopNotifications
|
||||
for blocking::Proxy<'_, C>
|
||||
{
|
||||
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"SetNotiWindowVisibility",
|
||||
(value,),
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_dnd(&self) -> Result<bool, dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "ToggleDnd", ())
|
||||
.and_then(|r: (bool,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "SetDnd", (state,))
|
||||
}
|
||||
|
||||
fn get_dnd(&self) -> Result<bool, dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "GetDnd", ())
|
||||
.and_then(|r: (bool,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"ManuallyCloseNotification",
|
||||
(id, timeout),
|
||||
)
|
||||
}
|
||||
|
||||
fn close_all_notifications(&self) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "CloseAllNotifications", ())
|
||||
}
|
||||
|
||||
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"HideLatestNotification",
|
||||
(close,),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "GetCapabilities", ())
|
||||
.and_then(|r: (Vec<String>,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn notify(
|
||||
&self,
|
||||
app_name: &str,
|
||||
replaces_id: u32,
|
||||
app_icon: &str,
|
||||
summary: &str,
|
||||
body: &str,
|
||||
actions: Vec<&str>,
|
||||
hints: arg::PropMap,
|
||||
expire_timeout: i32,
|
||||
) -> Result<u32, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"Notify",
|
||||
(
|
||||
app_name,
|
||||
replaces_id,
|
||||
app_icon,
|
||||
summary,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
expire_timeout,
|
||||
),
|
||||
)
|
||||
.and_then(|r: (u32,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn close_notification(&self, id: u32) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "CloseNotification", (id,))
|
||||
}
|
||||
|
||||
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "GetServerInformation", ())
|
||||
}
|
||||
|
||||
fn dnd(&self) -> Result<bool, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
self,
|
||||
"org.freedesktop.Notifications",
|
||||
"Dnd",
|
||||
)
|
||||
}
|
||||
|
||||
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::set(
|
||||
self,
|
||||
"org.freedesktop.Notifications",
|
||||
"Dnd",
|
||||
value,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
use rosc::{OscMessage, OscPacket, OscType};
|
||||
|
||||
use crate::{
|
||||
backend::{common::OverlayContainer, input::TrackedDevice},
|
||||
overlays::{keyboard::KEYBOARD_NAME, watch::WATCH_NAME},
|
||||
};
|
||||
|
||||
use crate::backend::input::TrackedDeviceRole;
|
||||
|
||||
pub struct OscSender {
|
||||
last_sent_overlay: Instant,
|
||||
last_sent_battery: Instant,
|
||||
upstream: UdpSocket,
|
||||
}
|
||||
|
||||
impl OscSender {
|
||||
pub fn new(send_port: u16) -> anyhow::Result<Self> {
|
||||
let ip = IpAddr::V4(Ipv4Addr::LOCALHOST);
|
||||
|
||||
let Ok(upstream) = UdpSocket::bind("0.0.0.0:0") else {
|
||||
bail!("Failed to bind UDP socket - OSC will not function.");
|
||||
};
|
||||
|
||||
let Ok(()) = upstream.connect(SocketAddr::new(ip, send_port)) else {
|
||||
bail!("Failed to connect UDP socket - OSC will not function.");
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
upstream,
|
||||
last_sent_overlay: Instant::now(),
|
||||
last_sent_battery: Instant::now(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_message(&self, addr: String, args: Vec<OscType>) -> anyhow::Result<()> {
|
||||
let packet = OscPacket::Message(OscMessage { addr, args });
|
||||
let Ok(bytes) = rosc::encoder::encode(&packet) else {
|
||||
bail!("Could not encode OSC packet.");
|
||||
};
|
||||
|
||||
let Ok(_) = self.upstream.send(&bytes) else {
|
||||
bail!("Could not send OSC packet.");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_params<D>(
|
||||
&mut self,
|
||||
overlays: &OverlayContainer<D>,
|
||||
devices: &Vec<TrackedDevice>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
D: Default,
|
||||
{
|
||||
// send overlay data every 0.1 seconds
|
||||
if self.last_sent_overlay.elapsed().as_millis() >= 100 {
|
||||
self.last_sent_overlay = Instant::now();
|
||||
|
||||
let mut num_overlays = 0;
|
||||
let mut has_keyboard = false;
|
||||
let mut has_wrist = false;
|
||||
|
||||
for o in overlays.iter() {
|
||||
if !o.state.want_visible {
|
||||
continue;
|
||||
}
|
||||
match o.state.name.as_ref() {
|
||||
WATCH_NAME => has_wrist = true,
|
||||
KEYBOARD_NAME => has_keyboard = true,
|
||||
_ => {
|
||||
if o.state.interactable {
|
||||
num_overlays += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.send_message(
|
||||
"/avatar/parameters/isOverlayOpen".into(),
|
||||
vec![OscType::Bool(num_overlays > 0)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/isKeyboardOpen".into(),
|
||||
vec![OscType::Bool(has_keyboard)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/isWristVisible".into(),
|
||||
vec![OscType::Bool(has_wrist)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/openOverlayCount".into(),
|
||||
vec![OscType::Int(num_overlays)],
|
||||
)?;
|
||||
}
|
||||
|
||||
// send battery levels every 10 seconds
|
||||
if self.last_sent_battery.elapsed().as_millis() >= 10000 {
|
||||
self.last_sent_battery = Instant::now();
|
||||
|
||||
let mut tracker_count: i8 = 0;
|
||||
let mut controller_count: i8 = 0;
|
||||
let mut tracker_total_bat = 0.0;
|
||||
let mut controller_total_bat = 0.0;
|
||||
|
||||
for device in devices {
|
||||
let tracker_param;
|
||||
|
||||
// soc is the battery level (set to device status.charge)
|
||||
let level = device.soc.unwrap_or(-1.0);
|
||||
let parameter = match device.role {
|
||||
TrackedDeviceRole::None => continue,
|
||||
TrackedDeviceRole::Hmd => {
|
||||
// legacy OVR Toolkit style (int)
|
||||
// as of 20 Nov 2024 OVR Toolkit uses int 0-100, but this may change in a future update.
|
||||
//TODO: update this once their implementation matches their docs
|
||||
self.send_message(
|
||||
"/avatar/parameters/hmdBattery".into(),
|
||||
vec![OscType::Int((level * 100.0f32).round() as i32)],
|
||||
)?;
|
||||
|
||||
"headset"
|
||||
}
|
||||
TrackedDeviceRole::LeftHand => {
|
||||
controller_count += 1;
|
||||
controller_total_bat += level;
|
||||
"leftController"
|
||||
}
|
||||
TrackedDeviceRole::RightHand => {
|
||||
controller_count += 1;
|
||||
controller_total_bat += level;
|
||||
"rightController"
|
||||
}
|
||||
TrackedDeviceRole::Tracker => {
|
||||
tracker_count += 1;
|
||||
tracker_total_bat += level;
|
||||
tracker_param = format!("tracker{tracker_count}");
|
||||
tracker_param.as_str()
|
||||
}
|
||||
};
|
||||
|
||||
// send device battery parameters
|
||||
self.send_message(
|
||||
format!("/avatar/parameters/{parameter}Battery"),
|
||||
vec![OscType::Float(level)],
|
||||
)?;
|
||||
self.send_message(
|
||||
format!("/avatar/parameters/{parameter}Charging"),
|
||||
vec![OscType::Bool(device.charging)],
|
||||
)?;
|
||||
}
|
||||
|
||||
// send average controller and tracker battery parameters
|
||||
self.send_message(
|
||||
String::from("/avatar/parameters/averageControllerBattery"),
|
||||
vec![OscType::Float(
|
||||
controller_total_bat / f32::from(controller_count),
|
||||
)],
|
||||
)?;
|
||||
self.send_message(
|
||||
String::from("/avatar/parameters/averageTrackerBattery"),
|
||||
vec![OscType::Float(tracker_total_bat / f32::from(tracker_count))],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_single_param(
|
||||
&mut self,
|
||||
parameter: String,
|
||||
values: Vec<OscType>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.send_message(parameter, values)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user