Compare commits

..

25 Commits

Author SHA1 Message Date
Ferdinand Schober
ce10eb6c3e fix macos, the 2nd 2024-06-28 23:54:34 +02:00
Ferdinand Schober
89c5067e0d fix macos build 2024-06-28 23:48:56 +02:00
Ferdinand Schober
c2fde88900 add option to config 2024-06-28 23:44:55 +02:00
Ferdinand Schober
1db43c4751 adapt windows + macos error types 2024-06-28 23:31:42 +02:00
Ferdinand Schober
16df8e9693 fix Display 2024-06-28 23:20:38 +02:00
Ferdinand Schober
769597af58 adapt windows 2024-06-28 23:16:34 +02:00
Ferdinand Schober
b874898c11 configurable capture backend 2024-06-28 22:56:49 +02:00
Ferdinand Schober
232c048c19 fix transmuting to pointer types UB (#147)
closes #134
2024-06-25 14:13:05 +02:00
Orhun Parmaksız
1c37579ae5 Update Arch Linux instructions (#145) 2024-06-19 20:28:30 +02:00
Ferdinand Schober
460bacade5 fix sizeof usize assumed to be 8 (#143)
closes #141
2024-06-09 00:49:00 +02:00
虢豳
5fd3b719d6 Extract package name and version from Cargo.toml (#136)
* chore: nix flake update

* feat: Extract package name and version from Cargo.toml
2024-05-22 08:06:44 +02:00
Ferdinand Schober
e6d4585bb2 chore: Release lan-mouse version 0.8.0 2024-05-17 18:06:11 +02:00
Ferdinand Schober
1c082d5c0c fix win -> lin scancode translations for intl keys 2024-05-17 17:39:52 +02:00
Ferdinand Schober
cd98acbd08 fix right shift not working 2024-05-16 22:36:51 +02:00
Ferdinand Schober
11e1919588 fix most international keybindings 2024-05-16 22:29:41 +02:00
Ferdinand Schober
152bceaa86 fix dns resolving 2024-05-13 08:12:55 +02:00
Ferdinand Schober
60180d841c fix formatting 2024-05-12 15:50:03 +02:00
Ferdinand Schober
5802a0be0b fix discrete scrolling on wlroots 2024-05-12 15:49:39 +02:00
Ferdinand Schober
1737727d61 fix cast 2024-05-12 15:07:29 +02:00
Ferdinand Schober
ba46037a1f fix scrolling in windows 2024-05-12 14:55:56 +02:00
Ferdinand Schober
da768b6fb8 ignore Axis events corresponding to v120 events 2024-05-12 13:32:14 +02:00
Ferdinand Schober
799b45104a enter hook command (#130)
new configuration option `enter_hook` can now be used to spawn a command when a client is entered
2024-05-12 13:01:07 +02:00
Ferdinand Schober
e9738fc024 explicit state synchronisation (#129)
prevents unnecessary state updates and makes synchronization less error prone
2024-05-12 00:49:53 +02:00
Ferdinand Schober
9969f997d3 macos-latest on longer runs on intel macs 2024-05-07 11:46:01 +02:00
Ferdinand Schober
b8cc9e2197 hotfix: race condition when activating clients 2024-05-07 11:18:15 +02:00
33 changed files with 643 additions and 458 deletions

View File

@@ -7,7 +7,7 @@ jobs:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
- macos-latest - macos-13
- macos-14 - macos-14
name: "Build" name: "Build"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -31,7 +31,7 @@ jobs:
run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
- name: Build lan-mouse (x86_64-darwin) - name: Build lan-mouse (x86_64-darwin)
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-13'
run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse
- name: Build lan-mouse (aarch64-darwin) - name: Build lan-mouse (aarch64-darwin)

View File

@@ -78,7 +78,7 @@ jobs:
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies

View File

@@ -92,7 +92,7 @@ jobs:
target/debug/*.dll target/debug/*.dll
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies

View File

@@ -74,7 +74,7 @@ jobs:
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies

12
Cargo.lock generated
View File

@@ -1284,7 +1284,7 @@ dependencies = [
[[package]] [[package]]
name = "lan-mouse" name = "lan-mouse"
version = "0.7.3" version = "0.8.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ashpd", "ashpd",
@@ -1292,6 +1292,7 @@ dependencies = [
"async-trait", "async-trait",
"clap", "clap",
"core-graphics", "core-graphics",
"endi",
"env_logger", "env_logger",
"futures", "futures",
"futures-core", "futures-core",
@@ -1311,6 +1312,7 @@ dependencies = [
"serde_json", "serde_json",
"slab", "slab",
"tempfile", "tempfile",
"thiserror",
"tokio", "tokio",
"toml", "toml",
"wayland-client", "wayland-client",
@@ -2010,18 +2012,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.58" version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.58" version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lan-mouse" name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks" description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.7.3" version = "0.8.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse" repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -22,7 +22,7 @@ anyhow = "1.0.71"
log = "0.4.20" log = "0.4.20"
env_logger = "0.11.3" env_logger = "0.11.3"
serde_json = "1.0.107" serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "rt", "sync", "signal"] } tokio = {version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "process", "rt", "sync", "signal"] }
async-trait = "0.1.73" async-trait = "0.1.73"
futures-core = "0.3.28" futures-core = "0.3.28"
futures = "0.3.28" futures = "0.3.28"
@@ -35,6 +35,8 @@ once_cell = "1.19.0"
num_enum = "0.7.2" num_enum = "0.7.2"
hostname = "0.4.0" hostname = "0.4.0"
slab = "0.4.9" slab = "0.4.9"
endi = "1.1.0"
thiserror = "1.0.61"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.148" libc = "0.2.148"

View File

@@ -69,7 +69,15 @@ Precompiled release binaries for Windows, MacOS and Linux are available in the [
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies). For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux ### Arch Linux
Lan Mouse is available on the AUR:
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
```sh
pacman -S lan-mouse
```
It is also available on the AUR:
```sh ```sh
# git version (includes latest changes) # git version (includes latest changes)
paru -S lan-mouse-git paru -S lan-mouse-git

View File

@@ -1,5 +1,7 @@
# example configuration # example configuration
# capture_backend = "LayerShell"
# release bind # release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ] release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]

12
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1710806803, "lastModified": 1716293225,
"narHash": "sha256-qrxvLS888pNJFwJdK+hf1wpRCSQcqA6W5+Ox202NDa0=", "narHash": "sha256-pU9ViBVE3XYb70xZx+jK6SEVphvt7xMTbm6yDIF4xPs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b06025f1533a1e07b6db3e75151caa155d1c7eb3", "rev": "3eaeaeb6b1e08a016380c279f8846e0bd8808916",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -48,11 +48,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1710987136, "lastModified": 1716257780,
"narHash": "sha256-Q8GRdlAIKZ8tJUXrbcRO1pA33AdoPfTUirsSnmGQnOU=", "narHash": "sha256-R+NjvJzKEkTVCmdrKRfPE4liX/KMGVqGUwwS5H8ET8A=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "97596b54ac34ad8184ca1eef44b1ec2e5c2b5f9e", "rev": "4e5e3d2c5c9b2721bd266f9e43c14e96811b89d2",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -2,10 +2,14 @@
rustPlatform, rustPlatform,
lib, lib,
pkgs, pkgs,
}: }: let
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
pname = cargoToml.package.name;
version = cargoToml.package.version;
in
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage {
pname = "lan-mouse"; pname = pname;
version = "0.7.0"; version = version;
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
pkg-config pkg-config
@@ -23,7 +27,7 @@ rustPlatform.buildRustPackage {
]; ];
src = builtins.path { src = builtins.path {
name = "lan-mouse"; name = pname;
path = lib.cleanSource ../.; path = lib.cleanSource ../.;
}; };
@@ -38,7 +42,7 @@ rustPlatform.buildRustPackage {
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch. Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details). The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
''; '';
mainProgram = "lan-mouse"; mainProgram = pname;
platforms = platforms.all; platforms = platforms.all;
}; };
} }

View File

@@ -4,9 +4,14 @@ use futures_core::Stream;
use crate::{ use crate::{
client::{ClientEvent, ClientHandle}, client::{ClientEvent, ClientHandle},
config::CaptureBackend,
event::Event, event::Event,
}; };
use self::error::CaptureCreationError;
pub mod error;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei; pub mod libei;
@@ -25,24 +30,42 @@ pub mod x11;
/// fallback input capture (does not produce events) /// fallback input capture (does not produce events)
pub mod dummy; pub mod dummy;
pub async fn create() -> Box<dyn InputCapture<Item = io::Result<(ClientHandle, Event)>>> { #[allow(unreachable_code)]
pub async fn create(
backend: Option<CaptureBackend>,
) -> Result<Box<dyn InputCapture<Item = io::Result<(ClientHandle, Event)>>>, CaptureCreationError> {
if let Some(backend) = backend {
return match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => {
Ok(Box::new(libei::LibeiInputCapture::new().await?))
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
CaptureBackend::LayerShell => Ok(Box::new(wayland::WaylandInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
CaptureBackend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)]
CaptureBackend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())),
#[cfg(target_os = "macos")]
CaptureBackend::MacOs => Ok(Box::new(macos::MacOSInputCapture::new()?)),
CaptureBackend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())),
};
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
match macos::MacOSInputCapture::new() { match macos::MacOSInputCapture::new() {
Ok(p) => return Box::new(p), Ok(p) => return Ok(Box::new(p)),
Err(e) => log::info!("macos input capture not available: {e}"), Err(e) => log::info!("macos input capture not available: {e}"),
} }
#[cfg(windows)] #[cfg(windows)]
match windows::WindowsInputCapture::new() { return Ok(Box::new(windows::WindowsInputCapture::new()));
Ok(p) => return Box::new(p),
Err(e) => log::info!("windows input capture not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match libei::LibeiInputCapture::new().await { match libei::LibeiInputCapture::new().await {
Ok(p) => { Ok(p) => {
log::info!("using libei input capture"); log::info!("using libei input capture");
return Box::new(p); return Ok(Box::new(p));
} }
Err(e) => log::info!("libei input capture not available: {e}"), Err(e) => log::info!("libei input capture not available: {e}"),
} }
@@ -51,7 +74,7 @@ pub async fn create() -> Box<dyn InputCapture<Item = io::Result<(ClientHandle, E
match wayland::WaylandInputCapture::new() { match wayland::WaylandInputCapture::new() {
Ok(p) => { Ok(p) => {
log::info!("using layer-shell input capture"); log::info!("using layer-shell input capture");
return Box::new(p); return Ok(Box::new(p));
} }
Err(e) => log::info!("layer_shell input capture not available: {e}"), Err(e) => log::info!("layer_shell input capture not available: {e}"),
} }
@@ -60,13 +83,13 @@ pub async fn create() -> Box<dyn InputCapture<Item = io::Result<(ClientHandle, E
match x11::X11InputCapture::new() { match x11::X11InputCapture::new() {
Ok(p) => { Ok(p) => {
log::info!("using x11 input capture"); log::info!("using x11 input capture");
return Box::new(p); return Ok(Box::new(p));
} }
Err(e) => log::info!("x11 input capture not available: {e}"), Err(e) => log::info!("x11 input capture not available: {e}"),
} }
log::error!("falling back to dummy input capture"); log::error!("falling back to dummy input capture");
Box::new(dummy::DummyInputCapture::new()) Ok(Box::new(dummy::DummyInputCapture::new()))
} }
pub trait InputCapture: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin { pub trait InputCapture: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {

140
src/capture/error.rs Normal file
View File

@@ -0,0 +1,140 @@
use std::fmt::Display;
use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use std::io;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[derive(Debug, Error)]
pub enum CaptureCreationError {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei(#[from] LibeiCaptureCreationError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11(#[from] X11InputCaptureCreationError),
#[cfg(target_os = "macos")]
Macos(#[from] MacOSInputCaptureCreationError),
#[cfg(windows)]
Windows,
}
impl Display for CaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let reason = match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
CaptureCreationError::Libei(reason) => {
format!("error creating portal backend: {reason}")
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
CaptureCreationError::LayerShell(reason) => {
format!("error creating layer-shell backend: {reason}")
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
CaptureCreationError::X11(e) => format!("{e}"),
#[cfg(target_os = "macos")]
CaptureCreationError::Macos(e) => format!("{e}"),
#[cfg(windows)]
CaptureCreationError::Windows => String::from(""),
};
write!(f, "could not create input capture: {reason}")
}
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiCaptureCreationError {
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl Display for LibeiCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibeiCaptureCreationError::Ashpd(portal_error) => write!(f, "{portal_error}"),
}
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl Display for WaylandBindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} protocol not supported: {}",
self.protocol, self.inner
)
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError {
Connect(#[from] ConnectError),
Global(#[from] GlobalError),
Wayland(#[from] WaylandError),
Bind(#[from] WaylandBindError),
Dispatch(#[from] DispatchError),
Io(#[from] io::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl Display for LayerShellCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LayerShellCaptureCreationError::Bind(e) => write!(f, "{e}"),
LayerShellCaptureCreationError::Connect(e) => {
write!(f, "could not connect to wayland compositor: {e}")
}
LayerShellCaptureCreationError::Global(e) => write!(f, "wayland error: {e}"),
LayerShellCaptureCreationError::Wayland(e) => write!(f, "wayland error: {e}"),
LayerShellCaptureCreationError::Dispatch(e) => {
write!(f, "error dispatching wayland events: {e}")
}
LayerShellCaptureCreationError::Io(e) => write!(f, "io error: {e}"),
}
}
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11InputCaptureCreationError {
NotImplemented,
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
impl Display for X11InputCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "X11 input capture is not yet implemented :(")
}
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacOSInputCaptureCreationError {
NotImplemented,
}
#[cfg(target_os = "macos")]
impl Display for MacOSInputCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "macos input capture is not yet implemented :(")
}
}

View File

@@ -36,6 +36,8 @@ use crate::{
event::{Event, KeyboardEvent, PointerEvent}, event::{Event, KeyboardEvent, PointerEvent},
}; };
use super::error::LibeiCaptureCreationError;
#[derive(Debug)] #[derive(Debug)]
enum ProducerEvent { enum ProducerEvent {
Release, Release,
@@ -131,7 +133,7 @@ impl<'a> Drop for LibeiInputCapture<'a> {
async fn create_session<'a>( async fn create_session<'a>(
input_capture: &'a InputCapture<'a>, input_capture: &'a InputCapture<'a>,
) -> Result<(Session<'a>, BitFlags<Capabilities>)> { ) -> std::result::Result<(Session<'a>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session"); log::debug!("creating input capture session");
let (session, capabilities) = loop { let (session, capabilities) = loop {
match input_capture match input_capture
@@ -213,7 +215,7 @@ async fn wait_for_active_client(
} }
impl<'a> LibeiInputCapture<'a> { impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> Result<Self> { pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?); let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>; let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
let mut first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); let mut first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);

View File

@@ -1,7 +1,7 @@
use crate::capture::error::MacOSInputCaptureCreationError;
use crate::capture::InputCapture; use crate::capture::InputCapture;
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
use crate::event::Event; use crate::event::Event;
use anyhow::{anyhow, Result};
use futures_core::Stream; use futures_core::Stream;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{io, pin::Pin}; use std::{io, pin::Pin};
@@ -9,8 +9,8 @@ use std::{io, pin::Pin};
pub struct MacOSInputCapture; pub struct MacOSInputCapture;
impl MacOSInputCapture { impl MacOSInputCapture {
pub fn new() -> Result<Self> { pub fn new() -> std::result::Result<Self, MacOSInputCaptureCreationError> {
Err(anyhow!("not yet implemented")) Err(MacOSInputCaptureCreationError::NotImplemented)
} }
} }

View File

@@ -1,9 +1,8 @@
use crate::{ use crate::{
capture::InputCapture, capture::{error::WaylandBindError, InputCapture},
client::{ClientEvent, ClientHandle, Position}, client::{ClientEvent, ClientHandle, Position},
}; };
use anyhow::{anyhow, Result};
use futures_core::Stream; use futures_core::Stream;
use memmap::MmapOptions; use memmap::MmapOptions;
use std::{ use std::{
@@ -68,6 +67,8 @@ use tempfile;
use crate::event::{Event, KeyboardEvent, PointerEvent}; use crate::event::{Event, KeyboardEvent, PointerEvent};
use super::error::LayerShellCaptureCreationError;
struct Globals { struct Globals {
compositor: wl_compositor::WlCompositor, compositor: wl_compositor::WlCompositor,
pointer_constraints: ZwpPointerConstraintsV1, pointer_constraints: ZwpPointerConstraintsV1,
@@ -111,6 +112,7 @@ struct State {
qh: QueueHandle<Self>, qh: QueueHandle<Self>,
pending_events: VecDeque<(ClientHandle, Event)>, pending_events: VecDeque<(ClientHandle, Event)>,
output_info: Vec<(WlOutput, OutputInfo)>, output_info: Vec<(WlOutput, OutputInfo)>,
scroll_discrete_pending: bool,
} }
struct Inner { struct Inner {
@@ -257,64 +259,37 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
} }
impl WaylandInputCapture { impl WaylandInputCapture {
pub fn new() -> Result<Self> { pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
let conn = match Connection::connect_to_env() { let conn = Connection::connect_to_env()?;
Ok(c) => c, let (g, mut queue) = registry_queue_init::<State>(&conn)?;
Err(e) => return Err(anyhow!("could not connect to wayland compositor: {e:?}")),
};
let (g, mut queue) = match registry_queue_init::<State>(&conn) {
Ok(q) => q,
Err(e) => return Err(anyhow!("failed to initialize wl_registry: {e:?}")),
};
let qh = queue.handle(); let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) { let compositor: wl_compositor::WlCompositor = g
Ok(compositor) => compositor, .bind(&qh, 4..=5, ())
Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")), .map_err(|e| WaylandBindError::new(e, "wl_compositor 4..=5"))?;
}; let xdg_output_manager: ZxdgOutputManagerV1 = g
.bind(&qh, 1..=3, ())
let xdg_output_manager: ZxdgOutputManagerV1 = match g.bind(&qh, 1..=3, ()) { .map_err(|e| WaylandBindError::new(e, "xdg_output_manager 1..=3"))?;
Ok(xdg_output_manager) => xdg_output_manager, let shm: wl_shm::WlShm = g
Err(_) => return Err(anyhow!("xdg_output not supported!")), .bind(&qh, 1..=1, ())
}; .map_err(|e| WaylandBindError::new(e, "wl_shm"))?;
let layer_shell: ZwlrLayerShellV1 = g
let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) { .bind(&qh, 3..=4, ())
Ok(wl_shm) => wl_shm, .map_err(|e| WaylandBindError::new(e, "wlr_layer_shell 3..=4"))?;
Err(_) => return Err(anyhow!("wl_shm v1 not supported")), let seat: wl_seat::WlSeat = g
}; .bind(&qh, 7..=8, ())
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
let layer_shell: ZwlrLayerShellV1 = match g.bind(&qh, 3..=4, ()) {
Ok(layer_shell) => layer_shell,
Err(_) => return Err(anyhow!("zwlr_layer_shell_v1 >= v3 not supported - required to display a surface at the edge of the screen")),
};
let seat: wl_seat::WlSeat = match g.bind(&qh, 7..=8, ()) {
Ok(wl_seat) => wl_seat,
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
};
let pointer_constraints: ZwpPointerConstraintsV1 = match g.bind(&qh, 1..=1, ()) {
Ok(pointer_constraints) => pointer_constraints,
Err(_) => return Err(anyhow!("zwp_pointer_constraints_v1 not supported")),
};
let relative_pointer_manager: ZwpRelativePointerManagerV1 = match g.bind(&qh, 1..=1, ()) {
Ok(relative_pointer_manager) => relative_pointer_manager,
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
};
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 =
match g.bind(&qh, 1..=1, ()) {
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
Err(_) => {
return Err(anyhow!(
"zwp_keyboard_shortcuts_inhibit_manager_v1 not supported"
))
}
};
let pointer_constraints: ZwpPointerConstraintsV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_pointer_constraints_v1"))?;
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?;
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"))?;
let outputs = vec![]; let outputs = vec![];
let g = Globals { let g = Globals {
@@ -351,6 +326,7 @@ impl WaylandInputCapture {
read_guard: None, read_guard: None,
pending_events: VecDeque::new(), pending_events: VecDeque::new(),
output_info: vec![], output_info: vec![],
scroll_discrete_pending: false,
}; };
// dispatch registry to () again, in order to read all wl_outputs // dispatch registry to () again, in order to read all wl_outputs
@@ -752,17 +728,25 @@ impl Dispatch<WlPointer, ()> for State {
} }
wl_pointer::Event::Axis { time, axis, value } => { wl_pointer::Event::Axis { time, axis, value } => {
let (_, client) = app.focused.as_ref().unwrap(); let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push_back(( if app.scroll_discrete_pending {
*client, // each axisvalue120 event is coupled with
Event::Pointer(PointerEvent::Axis { // a corresponding axis event, which needs to
time, // be ignored to not duplicate the scrolling
axis: u32::from(axis) as u8, app.scroll_discrete_pending = false;
value, } else {
}), app.pending_events.push_back((
)); *client,
Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
}),
));
}
} }
wl_pointer::Event::AxisValue120 { axis, value120 } => { wl_pointer::Event::AxisValue120 { axis, value120 } => {
let (_, client) = app.focused.as_ref().unwrap(); let (_, client) = app.focused.as_ref().unwrap();
app.scroll_discrete_pending = true;
app.pending_events.push_back(( app.pending_events.push_back((
*client, *client,
Event::Pointer(PointerEvent::AxisDiscrete120 { Event::Pointer(PointerEvent::AxisDiscrete120 {

View File

@@ -14,7 +14,7 @@ use std::task::ready;
use std::{io, pin::Pin, thread}; use std::{io, pin::Pin, thread};
use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::mpsc::{channel, Receiver, Sender};
use windows::core::{w, PCWSTR}; use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}; use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{ use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW, EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS, DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
@@ -43,7 +43,6 @@ use crate::{
event::Event, event::Event,
scancode, scancode,
}; };
use crate::display_util::{DirectedLine, Display, Point};
pub struct WindowsInputCapture { pub struct WindowsInputCapture {
event_rx: Receiver<(ClientHandle, Event)>, event_rx: Receiver<(ClientHandle, Event)>,
@@ -94,11 +93,10 @@ unsafe fn get_event_tid() -> Option<u32> {
} }
} }
static mut ENTRY_POINT: Point<i32> = Point { x: 0, y: 0 }; static mut ENTRY_POINT: (i32, i32) = (0, 0);
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> { fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
match wparam { match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button { WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0, time: 0,
@@ -131,17 +129,18 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
state: 0, state: 0,
}), }),
WPARAM(p) if p == WM_MOUSEMOVE as usize => unsafe { WPARAM(p) if p == WM_MOUSEMOVE as usize => unsafe {
let current = Point { x: mouse_low_level.pt.x, y: mouse_low_level.pt.y }; let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let diff = current - ENTRY_POINT; let (ex, ey) = ENTRY_POINT;
let (dx, dy) = (x - ex, y - ey);
Some(PointerEvent::Motion { Some(PointerEvent::Motion {
time: 0, time: 0,
relative_x: diff.x as f64, relative_x: dx as f64,
relative_y: diff.y as f64, relative_y: dy as f64,
}) })
}, },
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 { WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0, axis: 0,
value: -(mouse_low_level.mouseData as i32), value: -(mouse_low_level.mouseData as i32 >> 16),
}), }),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => { WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16; let hb = mouse_low_level.mouseData >> 16;
@@ -167,8 +166,7 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
} }
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> { unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = let kybrdllhookstruct: KBDLLHOOKSTRUCT = *(lparam.0 as *const KBDLLHOOKSTRUCT);
*std::mem::transmute::<LPARAM, *const KBDLLHOOKSTRUCT>(lparam);
let mut scan_code = kybrdllhookstruct.scanCode; let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}"); log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) { if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
@@ -210,6 +208,31 @@ unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent>
} }
} }
///
/// clamp point to display bounds
///
/// # Arguments
///
/// * `prev_point`: coordinates, the cursor was before entering, within bounds of a display
/// * `entry_point`: point to clamp
///
/// returns: (i32, i32), the corrected entry point
///
fn clamp_to_display_bounds(prev_point: (i32, i32), point: (i32, i32)) -> (i32, i32) {
/* find display where movement came from */
let display_regions = unsafe { get_display_regions() };
let display = display_regions
.iter()
.find(|&d| is_within_dp_region(prev_point, d))
.unwrap();
/* clamp to bounds (inclusive) */
let (x, y) = point;
let (min_x, max_x) = (display.left, display.right - 1);
let (min_y, max_y) = (display.top, display.bottom - 1);
(x.clamp(min_x, max_x), y.clamp(min_y, max_y))
}
unsafe fn send_blocking(event: Event) { unsafe fn send_blocking(event: Event) {
if let Some(active) = ACTIVE_CLIENT { if let Some(active) = ACTIVE_CLIENT {
block_on(async move { block_on(async move {
@@ -222,11 +245,9 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize { if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.is_some(); return ACTIVE_CLIENT.is_some();
} }
let mouse_low_level: MSLLHOOKSTRUCT = let mouse_low_level: MSLLHOOKSTRUCT = *(lparam.0 as *const MSLLHOOKSTRUCT);
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) }; static mut PREV_POS: Option<(i32, i32)> = None;
static mut PREV_POS: Option<Point<i32>> = None; let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let curr_pos = Point { x: mouse_low_level.pt.x, y: mouse_low_level.pt.y};
log::warn!("POSITION: {curr_pos:?} - display: {:?}", curr_pos.display_in_bounds(get_display_regions()));
let prev_pos = PREV_POS.unwrap_or(curr_pos); let prev_pos = PREV_POS.unwrap_or(curr_pos);
PREV_POS.replace(curr_pos); PREV_POS.replace(curr_pos);
@@ -239,8 +260,7 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
} }
/* check if a client was activated */ /* check if a client was activated */
let line = DirectedLine { start: prev_pos, end: curr_pos }; let Some(pos) = entered_barrier(prev_pos, curr_pos, get_display_regions()) else {
let Some((display, pos)) = line.crossed_display_bounds(get_display_regions()) else {
return ret; return ret;
}; };
@@ -251,7 +271,7 @@ unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
/* update active client and entry point */ /* update active client and entry point */
ACTIVE_CLIENT.replace(*client); ACTIVE_CLIENT.replace(*client);
ENTRY_POINT = curr_pos.clamp_to_display(&display); ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
/* notify main thread */ /* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}"); log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
@@ -324,7 +344,7 @@ unsafe extern "system" fn window_proc(
LRESULT(1) LRESULT(1)
} }
fn enumerate_displays() -> Vec<Display<i32>> { fn enumerate_displays() -> Vec<RECT> {
unsafe { unsafe {
let mut display_rects = vec![]; let mut display_rects = vec![];
let mut devices = vec![]; let mut devices = vec![];
@@ -356,11 +376,11 @@ fn enumerate_displays() -> Vec<Display<i32>> {
let (x, y) = (pos.x, pos.y); let (x, y) = (pos.x, pos.y);
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight); let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
display_rects.push(Display { display_rects.push(RECT {
left: x, left: x,
right: x + width as i32 - 1, right: x + width as i32,
top: y, top: y,
bottom: y + height as i32 - 1, bottom: y + height as i32,
}); });
} }
display_rects display_rects
@@ -369,8 +389,8 @@ fn enumerate_displays() -> Vec<Display<i32>> {
static mut DISPLAY_RESOLUTION_CHANGED: bool = true; static mut DISPLAY_RESOLUTION_CHANGED: bool = true;
unsafe fn get_display_regions() -> &'static Vec<Display<i32>> { unsafe fn get_display_regions() -> &'static Vec<RECT> {
static mut DISPLAYS: Vec<Display<i32>> = vec![]; static mut DISPLAYS: Vec<RECT> = vec![];
if DISPLAY_RESOLUTION_CHANGED { if DISPLAY_RESOLUTION_CHANGED {
DISPLAYS = enumerate_displays(); DISPLAYS = enumerate_displays();
DISPLAY_RESOLUTION_CHANGED = false; DISPLAY_RESOLUTION_CHANGED = false;
@@ -379,6 +399,73 @@ unsafe fn get_display_regions() -> &'static Vec<Display<i32>> {
&*addr_of!(DISPLAYS) &*addr_of!(DISPLAYS)
} }
fn is_within_dp_region(point: (i32, i32), display: &RECT) -> bool {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.iter()
.all(|&pos| is_within_dp_boundary(point, display, pos))
}
fn is_within_dp_boundary(point: (i32, i32), display: &RECT, pos: Position) -> bool {
let (x, y) = point;
match pos {
Position::Left => display.left <= x,
Position::Right => display.right > x,
Position::Top => display.top <= y,
Position::Bottom => display.bottom > y,
}
}
/// returns whether the given position is within the display bounds with respect to the given
/// barrier position
///
/// # Arguments
///
/// * `x`:
/// * `y`:
/// * `displays`:
/// * `pos`:
///
/// returns: bool
///
fn in_bounds(point: (i32, i32), displays: &[RECT], pos: Position) -> bool {
displays
.iter()
.any(|d| is_within_dp_boundary(point, d, pos))
}
fn in_display_region(point: (i32, i32), displays: &[RECT]) -> bool {
displays.iter().any(|d| is_within_dp_region(point, d))
}
fn moved_across_boundary(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
pos: Position,
) -> bool {
/* was within bounds, but is not anymore */
in_display_region(prev_pos, displays) && !in_bounds(curr_pos, displays, pos)
}
fn entered_barrier(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
) -> Option<Position> {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.into_iter()
.find(|&pos| moved_across_boundary(prev_pos, curr_pos, displays, pos))
}
fn get_msg() -> Option<MSG> { fn get_msg() -> Option<MSG> {
unsafe { unsafe {
let mut msg = std::mem::zeroed(); let mut msg = std::mem::zeroed();
@@ -488,7 +575,7 @@ fn update_clients(client_event: ClientEvent) {
} }
impl WindowsInputCapture { impl WindowsInputCapture {
pub(crate) fn new() -> Result<Self> { pub(crate) fn new() -> Self {
unsafe { unsafe {
let (tx, rx) = channel(10); let (tx, rx) = channel(10);
EVENT_TX.replace(tx); EVENT_TX.replace(tx);
@@ -496,10 +583,10 @@ impl WindowsInputCapture {
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx))); let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
/* wait for thread to set its id */ /* wait for thread to set its id */
ready_rx.recv().expect("channel closed"); ready_rx.recv().expect("channel closed");
Ok(Self { Self {
msg_thread, msg_thread,
event_rx: rx, event_rx: rx,
}) }
} }
} }
} }

View File

@@ -1,4 +1,3 @@
use anyhow::{anyhow, Result};
use std::io; use std::io;
use std::task::Poll; use std::task::Poll;
@@ -9,11 +8,13 @@ use crate::event::Event;
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
use super::error::X11InputCaptureCreationError;
pub struct X11InputCapture {} pub struct X11InputCapture {}
impl X11InputCapture { impl X11InputCapture {
pub fn new() -> Result<Self> { pub fn new() -> std::result::Result<Self, X11InputCaptureCreationError> {
Err(anyhow!("not implemented")) Err(X11InputCaptureCreationError::NotImplemented)
} }
} }

View File

@@ -1,5 +1,6 @@
use crate::capture; use crate::capture;
use crate::client::{ClientEvent, Position}; use crate::client::{ClientEvent, Position};
use crate::config::Config;
use crate::event::{Event, KeyboardEvent}; use crate::event::{Event, KeyboardEvent};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures::StreamExt; use futures::StreamExt;
@@ -12,12 +13,14 @@ pub fn run() -> Result<()> {
.enable_time() .enable_time()
.build()?; .build()?;
runtime.block_on(LocalSet::new().run_until(input_capture_test())) let config = Config::new()?;
runtime.block_on(LocalSet::new().run_until(input_capture_test(config)))
} }
async fn input_capture_test() -> Result<()> { async fn input_capture_test(config: Config) -> Result<()> {
log::info!("creating input capture"); log::info!("creating input capture");
let mut input_capture = capture::create().await; let mut input_capture = capture::create(config.capture_backend).await?;
log::info!("creating clients"); log::info!("creating clients");
input_capture.notify(ClientEvent::Create(0, Position::Left))?; input_capture.notify(ClientEvent::Create(0, Position::Left))?;
input_capture.notify(ClientEvent::Create(1, Position::Right))?; input_capture.notify(ClientEvent::Create(1, Position::Right))?;

View File

@@ -102,6 +102,8 @@ pub struct ClientConfig {
pub port: u16, pub port: u16,
/// position of a client on screen /// position of a client on screen
pub pos: Position, pub pos: Position,
/// enter hook
pub cmd: Option<String>,
} }
impl Default for ClientConfig { impl Default for ClientConfig {
@@ -111,6 +113,7 @@ impl Default for ClientConfig {
hostname: Default::default(), hostname: Default::default(),
fix_ips: Default::default(), fix_ips: Default::default(),
pos: Default::default(), pos: Default::default(),
cmd: None,
} }
} }
} }

View File

@@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
@@ -15,6 +15,7 @@ pub const DEFAULT_PORT: u16 = 4242;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml { pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub port: Option<u16>, pub port: Option<u16>,
pub frontend: Option<String>, pub frontend: Option<String>,
pub release_bind: Option<Vec<scancode::Linux>>, pub release_bind: Option<Vec<scancode::Linux>>,
@@ -26,11 +27,13 @@ pub struct ConfigToml {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient { pub struct TomlClient {
pub capture_backend: Option<CaptureBackend>,
pub hostname: Option<String>, pub hostname: Option<String>,
pub host_name: Option<String>, pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>, pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>, pub port: Option<u16>,
pub activate_on_startup: Option<bool>, pub activate_on_startup: Option<bool>,
pub enter_hook: Option<String>,
} }
impl ConfigToml { impl ConfigToml {
@@ -67,8 +70,34 @@ struct CliArgs {
/// test input emulation /// test input emulation
#[arg(long)] #[arg(long)]
test_emulation: bool, test_emulation: bool,
/// capture backend override
#[arg(long)]
capture_backend: Option<CaptureBackend>,
/// emulation backend override
#[arg(long)]
emulation_backend: Option<EmulationBackend>,
} }
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
MacOs,
Dummy,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum EmulationBackend {}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Frontend { pub enum Frontend {
Gtk, Gtk,
@@ -77,6 +106,7 @@ pub enum Frontend {
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub capture_backend: Option<CaptureBackend>,
pub frontend: Frontend, pub frontend: Frontend,
pub port: u16, pub port: u16,
pub clients: Vec<(TomlClient, Position)>, pub clients: Vec<(TomlClient, Position)>,
@@ -92,6 +122,7 @@ pub struct ConfigClient {
pub port: u16, pub port: u16,
pub pos: Position, pub pos: Position,
pub active: bool, pub active: bool,
pub enter_hook: Option<String>,
} }
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
@@ -161,6 +192,11 @@ impl Config {
.and_then(|c| c.release_bind.clone()) .and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())); .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let capture_backend = match args.capture_backend {
Some(b) => Some(b),
None => config_toml.as_ref().and_then(|c| c.capture_backend),
};
let mut clients: Vec<(TomlClient, Position)> = vec![]; let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml { if let Some(config_toml) = config_toml {
@@ -183,6 +219,7 @@ impl Config {
let test_emulation = args.test_emulation; let test_emulation = args.test_emulation;
Ok(Config { Ok(Config {
capture_backend,
daemon, daemon,
frontend, frontend,
clients, clients,
@@ -208,12 +245,14 @@ impl Config {
None => c.host_name.clone(), None => c.host_name.clone(),
}; };
let active = c.activate_on_startup.unwrap_or(false); let active = c.activate_on_startup.unwrap_or(false);
let enter_hook = c.enter_hook.clone();
ConfigClient { ConfigClient {
ips, ips,
hostname, hostname,
port, port,
pos: *pos, pos: *pos,
active, active,
enter_hook,
} }
}) })
.collect() .collect()

View File

@@ -1,114 +0,0 @@
use std::ops::{Add, Mul, Sub};
use crate::client::Position;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Point<T> {
pub x: T,
pub y: T,
}
impl<T: Sub<Output = T>> Sub for Point<T> {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
let x: T = self.x - rhs.x;
let y: T = self.y - rhs.y;
Self { x, y }
}
}
impl<T: Copy + Eq + Ord + Sub<Output = T> + Mul<Output = T> + Add<Output = T>> Point<T> {
pub fn display_in_bounds<'a>(&self, displays: &'a[Display<T>]) -> Option<&'a Display<T>> {
displays.iter().find(|&d| self.in_display_bounds(d))
}
pub fn in_display_bounds(&self, display: &Display<T>) -> bool {
self.clamp_to_display(display) == *self
}
pub fn clamp_to_display(&self, display: &Display<T>) -> Self {
let x = self.x.clamp(display.left, display.right);
let y = self.y.clamp(display.top, display.bottom);
Self { x, y }
}
/// Calculates the direction of maximum change between this point and the point given by other
///
/// # Arguments
///
/// * `other`: the point to calculate the distance
///
/// returns: Position -> The direction in which the distance is largest
///
/// # Examples
///
/// ```
/// use lan_mouse::client::Position;
/// use lan_mouse::display_util::Point;
/// let a = Point { x: 0, y: 0 };
/// let b = Point { x: 1, y: 2 };
/// assert_eq!(a.direction_of_maximum_change(b), Position::Bottom)
/// ```
///
/// ```
/// use lan_mouse::client::Position;
/// use lan_mouse::display_util::Point;
/// let a = Point { x: 0, y: 0 };
/// let b = Point { x: 1, y: -2 };
/// assert_eq!(a.direction_of_maximum_change(b), Position::Top)
/// ```
/// ```
/// use lan_mouse::client::Position;
/// use lan_mouse::display_util::Point;
/// let a = Point { x: 0, y: 0 };
/// let b = Point { x: -2, y: -1 };
/// assert_eq!(a.direction_of_maximum_change(b), Position::Left)
/// ```
/// ```
/// use lan_mouse::client::Position;
/// use lan_mouse::display_util::Point;
/// let a = Point { x: 0, y: 0 };
/// let b = Point { x: 2, y: -1 };
/// assert_eq!(a.direction_of_maximum_change(b), Position::Right)
/// ```
pub fn direction_of_maximum_change(self, other: Self) -> Position {
let distances = [
(Position::Left, self.x - other.x),
(Position::Right, other.x - self.x),
(Position::Top, self.y - other.y),
(Position::Bottom, other.y - self.y),
];
distances.into_iter().max_by_key(|(_, d)| *d).map(|(p, _)| p).expect("no position")
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Display<T> {
pub left: T,
pub right: T,
pub top: T,
pub bottom: T,
}
#[derive(Clone, Copy)]
pub struct DirectedLine<T> {
pub start: Point<T>,
pub end: Point<T>,
}
impl<T: Copy + Eq + Ord + Sub<Output = T> + Mul<Output = T> + Add<Output = T>> DirectedLine<T> {
pub fn crossed_display_bounds<'a>(&self, displays: &'a [Display<T>]) -> Option<(&'a Display<T>, Position)> {
// was in bounds
let Some(display) = self.start.display_in_bounds(displays) else {
return None;
};
// still in bounds
if self.end.display_in_bounds(displays).is_some() {
return None;
}
// was in bounds of `display`, now out of bounds
let clamped = self.end.clamp_to_display(&display);
let dir = clamped.direction_of_maximum_change(self.end);
Some((display, dir))
}
}

View File

@@ -182,7 +182,8 @@ impl VirtualInput {
} }
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?; let axis: Axis = (axis as u32).try_into()?;
self.pointer.axis(0, axis, (value / 15) as f64); self.pointer
.axis_discrete(0, axis, value as f64 / 6., value / 120);
self.pointer.frame(); self.pointer.frame();
} }
PointerEvent::Frame {} => self.pointer.frame(), PointerEvent::Frame {} => self.pointer.frame(),

View File

@@ -107,16 +107,18 @@ pub enum FrontendRequest {
UpdatePosition(ClientHandle, Position), UpdatePosition(ClientHandle, Position),
/// update fix-ips /// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>), UpdateFixIps(ClientHandle, Vec<IpAddr>),
/// request the state of the given client
GetState(ClientHandle),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendEvent { pub enum FrontendEvent {
/// a client was created /// a client was created
Created(ClientHandle, ClientConfig, ClientState), Created(ClientHandle, ClientConfig, ClientState),
/// a client was updated /// no such client
Updated(ClientHandle, ClientConfig), NoSuchClient(ClientHandle),
/// state changed /// state changed
StateChange(ClientHandle, ClientState), State(ClientHandle, ClientConfig, ClientState),
/// the client was deleted /// the client was deleted
Deleted(ClientHandle), Deleted(ClientHandle),
/// new port, reason of failure (if failed) /// new port, reason of failure (if failed)
@@ -235,9 +237,7 @@ impl FrontendListener {
let json = serde_json::to_string(&notify).unwrap(); let json = serde_json::to_string(&notify).unwrap();
let payload = json.as_bytes(); let payload = json.as_bytes();
let len = payload.len().to_be_bytes(); let len = payload.len().to_be_bytes();
log::debug!("json: {json}, len: {}", payload.len()); log::debug!("broadcasting event to streams: {json}");
log::debug!("broadcasting event to streams: {:?}", self.tx_streams);
let mut keep = vec![]; let mut keep = vec![];
// TODO do simultaneously // TODO do simultaneously
for tx in self.tx_streams.iter_mut() { for tx in self.tx_streams.iter_mut() {

View File

@@ -100,6 +100,18 @@ impl<'a> Cli<'a> {
} }
} }
async fn update_client(&mut self, handle: ClientHandle) -> Result<()> {
self.send_request(FrontendRequest::GetState(handle)).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
break;
}
}
Ok(())
}
async fn execute(&mut self, cmd: Command) -> Result<()> { async fn execute(&mut self, cmd: Command) -> Result<()> {
match cmd { match cmd {
Command::None => {} Command::None => {}
@@ -125,14 +137,8 @@ impl<'a> Cli<'a> {
FrontendRequest::UpdatePosition(handle, pos), FrontendRequest::UpdatePosition(handle, pos),
] { ] {
self.send_request(request).await?; self.send_request(request).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Updated(_, _) = event {
break;
}
}
} }
self.update_client(handle).await?;
} }
Command::Disconnect(id) => { Command::Disconnect(id) => {
self.send_request(FrontendRequest::Delete(id)).await?; self.send_request(FrontendRequest::Delete(id)).await?;
@@ -148,26 +154,12 @@ impl<'a> Cli<'a> {
Command::Activate(id) => { Command::Activate(id) => {
self.send_request(FrontendRequest::Activate(id, true)) self.send_request(FrontendRequest::Activate(id, true))
.await?; .await?;
loop { self.update_client(id).await?;
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::StateChange(_, _) = event {
self.handle_event(event);
break;
}
}
} }
Command::Deactivate(id) => { Command::Deactivate(id) => {
self.send_request(FrontendRequest::Activate(id, false)) self.send_request(FrontendRequest::Activate(id, false))
.await?; .await?;
loop { self.update_client(id).await?;
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::StateChange(_, _) = event {
self.handle_event(event);
break;
}
}
} }
Command::List => { Command::List => {
self.send_request(FrontendRequest::Enumerate()).await?; self.send_request(FrontendRequest::Enumerate()).await?;
@@ -182,25 +174,12 @@ impl<'a> Cli<'a> {
Command::SetHost(handle, host) => { Command::SetHost(handle, host) => {
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone())); let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
self.send_request(request).await?; self.send_request(request).await?;
loop { self.update_client(handle).await?;
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Updated(_, _) = event {
self.handle_event(event);
break;
}
}
} }
Command::SetPort(handle, port) => { Command::SetPort(handle, port) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)); let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.send_request(request).await?; self.send_request(request).await?;
loop { self.update_client(handle).await?;
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Updated(_, _) = event {
break;
}
}
} }
Command::Help => { Command::Help => {
for cmd_type in [ for cmd_type in [
@@ -244,8 +223,11 @@ impl<'a> Cli<'a> {
eprintln!(); eprintln!();
self.clients.push((h, c, s)); self.clients.push((h, c, s));
} }
FrontendEvent::Updated(h, c) => { FrontendEvent::NoSuchClient(h) => {
if let Some((_, config, _)) = self.find_mut(h) { eprintln!("no such client: {h}");
}
FrontendEvent::State(h, c, s) => {
if let Some((_, config, state)) = self.find_mut(h) {
let old_host = config.hostname.clone().unwrap_or("\"\"".into()); let old_host = config.hostname.clone().unwrap_or("\"\"".into());
let new_host = c.hostname.clone().unwrap_or("\"\"".into()); let new_host = c.hostname.clone().unwrap_or("\"\"".into());
if old_host != new_host { if old_host != new_host {
@@ -261,10 +243,6 @@ impl<'a> Cli<'a> {
eprintln!("client {h} ips updated: {:?}", c.fix_ips) eprintln!("client {h} ips updated: {:?}", c.fix_ips)
} }
*config = c; *config = c;
}
}
FrontendEvent::StateChange(h, s) => {
if let Some((_, _, state)) = self.find_mut(h) {
if state.active ^ s.active { if state.active ^ s.active {
eprintln!( eprintln!(
"client {h} {}", "client {h} {}",

View File

@@ -11,6 +11,7 @@ use std::{
use crate::frontend::{gtk::window::Window, FrontendRequest}; use crate::frontend::{gtk::window::Window, FrontendRequest};
use adw::Application; use adw::Application;
use endi::{Endian, ReadBytes};
use gtk::{ use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme, gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
}; };
@@ -85,16 +86,14 @@ fn build_ui(app: &Application) {
gio::spawn_blocking(move || { gio::spawn_blocking(move || {
match loop { match loop {
// read length // read length
let mut len = [0u8; 8]; let len = match rx.read_u64(Endian::Big) {
match rx.read_exact(&mut len) { Ok(l) => l,
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e), Err(e) => break Err(e),
}; };
let len = usize::from_be_bytes(len);
// read payload // read payload
let mut buf = vec![0u8; len]; let mut buf = vec![0u8; len as usize];
match rx.read_exact(&mut buf) { match rx.read_exact(&mut buf) {
Ok(_) => (), Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
@@ -126,12 +125,11 @@ fn build_ui(app: &Application) {
FrontendEvent::Deleted(client) => { FrontendEvent::Deleted(client) => {
window.delete_client(client); window.delete_client(client);
} }
FrontendEvent::Updated(handle, client) => { FrontendEvent::State(handle, config, state) => {
window.update_client_config(handle, client); window.update_client_config(handle, config);
}
FrontendEvent::StateChange(handle, state) => {
window.update_client_state(handle, state); window.update_client_state(handle, state);
} }
FrontendEvent::NoSuchClient(_) => { }
FrontendEvent::Error(e) => { FrontendEvent::Error(e) => {
window.show_toast(e.as_str()); window.show_toast(e.as_str());
}, },

View File

@@ -10,6 +10,7 @@ use std::net::TcpStream;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use endi::{Endian, WriteBytes};
use glib::{clone, Object}; use glib::{clone, Object};
use gtk::{ use gtk::{
gio, gio,
@@ -51,6 +52,10 @@ impl Window {
.expect("Could not get clients") .expect("Could not get clients")
} }
fn client_by_idx(&self, idx: u32) -> Option<ClientObject> {
self.clients().item(idx).map(|o| o.downcast().unwrap())
}
fn setup_clients(&self) { fn setup_clients(&self) {
let model = gio::ListStore::new::<ClientObject>(); let model = gio::ListStore::new::<ClientObject>();
self.imp().clients.replace(Some(model)); self.imp().clients.replace(Some(model));
@@ -62,27 +67,24 @@ impl Window {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`."); let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object); let row = window.create_client_row(client_object);
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| { row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
let index = row.index() as u32; if let Some(client) = window.client_by_idx(row.index() as u32) {
let Some(client) = window.clients().item(index) else { window.request_client_activate(&client, active);
return; window.request_client_update(&client);
}; window.request_client_state(&client);
let client = client.downcast_ref::<ClientObject>().unwrap(); }
window.request_client_update(client);
window.request_client_activate(client, active)
})); }));
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| { row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
let index = row.index() as u32; if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_delete(index); window.request_client_delete(&client);
}
})); }));
row.connect_closure("request-dns", false, closure_local!(@strong window => move row.connect_closure("request-dns", false, closure_local!(@strong window => move
|row: ClientRow| { |row: ClientRow| {
let index = row.index() as u32; if let Some(client) = window.client_by_idx(row.index() as u32) {
let Some(client) = window.clients().item(index) else { window.request_client_update(&client);
return; window.request_dns(&client);
}; window.request_client_state(&client);
let client = client.downcast_ref::<ClientObject>().unwrap(); }
window.request_client_update(client);
window.request_dns(index);
})); }));
row.upcast() row.upcast()
}) })
@@ -177,7 +179,7 @@ impl Window {
if state.resolving != data.resolving { if state.resolving != data.resolving {
client_object.set_resolving(state.resolving); client_object.set_resolving(state.resolving);
log::debug!("resolving {}: {}", data.handle, state.active); log::debug!("resolving {}: {}", data.handle, state.resolving);
} }
self.update_dns_state(handle, !state.ips.is_empty()); self.update_dns_state(handle, !state.ips.is_empty());
@@ -204,20 +206,6 @@ impl Window {
} }
} }
pub fn request_dns(&self, idx: u32) {
let client_object = self.clients().item(idx).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
let event = FrontendRequest::ResolveDns(data.handle);
self.request(event);
}
pub fn request_client_create(&self) {
let event = FrontendRequest::Create;
self.imp().set_port(DEFAULT_PORT);
self.request(event);
}
pub fn request_port_change(&self) { pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string(); let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = port.as_str().parse::<u16>() { if let Ok(port) = port.as_str().parse::<u16>() {
@@ -227,6 +215,23 @@ impl Window {
} }
} }
pub fn request_client_state(&self, client: &ClientObject) {
let handle = client.handle();
let event = FrontendRequest::GetState(handle);
self.request(event);
}
pub fn request_client_create(&self) {
let event = FrontendRequest::Create;
self.request(event);
}
pub fn request_dns(&self, client: &ClientObject) {
let data = client.get_data();
let event = FrontendRequest::ResolveDns(data.handle);
self.request(event);
}
pub fn request_client_update(&self, client: &ClientObject) { pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle(); let handle = client.handle();
let data = client.get_data(); let data = client.get_data();
@@ -239,38 +244,29 @@ impl Window {
FrontendRequest::UpdatePosition(handle, position), FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port), FrontendRequest::UpdatePort(handle, port),
] { ] {
log::debug!("requesting: {event:?}");
self.request(event); self.request(event);
} }
} }
pub fn request_client_activate(&self, client: &ClientObject, active: bool) { pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
let handle = client.handle(); let handle = client.handle();
let event = FrontendRequest::Activate(handle, active); let event = FrontendRequest::Activate(handle, active);
log::debug!("requesting: {event:?}");
self.request(event); self.request(event);
} }
pub fn request_client_delete(&self, idx: u32) { pub fn request_client_delete(&self, client: &ClientObject) {
if let Some(obj) = self.clients().item(idx) { let handle = client.handle();
let client_object: &ClientObject = obj let event = FrontendRequest::Delete(handle);
.downcast_ref() self.request(event);
.expect("Expected object of type `ClientObject`.");
let handle = client_object.handle();
let event = FrontendRequest::Delete(handle);
self.request(event);
}
} }
pub fn request(&self, event: FrontendRequest) { pub fn request(&self, event: FrontendRequest) {
let json = serde_json::to_string(&event).unwrap(); let json = serde_json::to_string(&event).unwrap();
log::debug!("requesting {json}"); log::debug!("requesting: {json}");
let mut stream = self.imp().stream.borrow_mut(); let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap(); let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes(); let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes(); if let Err(e) = stream.write_u64(Endian::Big, bytes.len() as u64) {
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}"); log::error!("error sending message: {e}");
}; };
if let Err(e) = stream.write(bytes) { if let Err(e) = stream.write(bytes) {

View File

@@ -11,4 +11,3 @@ pub mod capture_test;
pub mod emulation_test; pub mod emulation_test;
pub mod frontend; pub mod frontend;
pub mod scancode; pub mod scancode;
pub mod display_util;

View File

@@ -71,7 +71,7 @@ fn run_service(config: &Config) -> Result<()> {
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse"); log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
let server = Server::new(config); let server = Server::new(config);
server.run().await?; server.run(config.capture_backend).await?;
log::debug!("service exiting"); log::debug!("service exiting");
anyhow::Ok(()) anyhow::Ok(())

View File

@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
/* /*
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input * https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
* https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf
* https://kbd-project.org/docs/scancodes/scancodes-1.html
*/ */
#[repr(u32)] #[repr(u32)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)] #[derive(Debug, Clone, Copy, TryFromPrimitive)]
@@ -120,15 +122,15 @@ pub enum Windows {
KeyF21 = 0x006C, KeyF21 = 0x006C,
KeyF22 = 0x006D, KeyF22 = 0x006D,
KeyF23 = 0x006E, KeyF23 = 0x006E,
KeyF24 = 0x0076, KeyF24 = 0x0076, // KeyLANG5
KeypadComma = 0x007E, KeypadComma = 0x007E,
KeyInternational1 = 0x0073, KeyInternational1 = 0x0073,
KeyInternational2 = 0x0070, KeyInternational2 = 0x0070,
KeyInternational3 = 0x007D, KeyInternational3 = 0x007D, // typo in doc -> its Int'l 3 not Int'l 2
#[allow(dead_code)] #[allow(dead_code)]
KeyInternational4 = 0x0079, // FIXME unused KeyInternational4 = 0x0079,
#[allow(dead_code)] #[allow(dead_code)]
KeyInternational5 = 0x007B, // FIXME unused KeyInternational5 = 0x007B,
// KeyInternational6 = 0x005C, // KeyInternational6 = 0x005C,
KeyLANG1 = 0x0072, KeyLANG1 = 0x0072,
KeyLANG2 = 0x0071, KeyLANG2 = 0x0071,
@@ -141,6 +143,7 @@ pub enum Windows {
KeyLeftGUI = 0xE05B, KeyLeftGUI = 0xE05B,
KeyRightCtrl = 0xE01D, KeyRightCtrl = 0xE01D,
KeyRightShift = 0x0036, KeyRightShift = 0x0036,
KeyFakeRightShift = 0xE036,
KeyRightAlt = 0xE038, KeyRightAlt = 0xE038,
KeyRightGUI = 0xE05C, KeyRightGUI = 0xE05C,
KeyScanNextTrack = 0xE019, KeyScanNextTrack = 0xE019,
@@ -293,7 +296,7 @@ pub enum Linux {
KeyPause = 119, KeyPause = 119,
KeyScale = 120, /* AL Compiz Scale (Expose) */ KeyScale = 120, /* AL Compiz Scale (Expose) */
KeyKpcomma = 121, KeyKpcomma = 121,
KeyHangeul = 122, KeyHanguel = 122,
// KEY_HANGUEL = KeyHangeul, // KEY_HANGUEL = KeyHangeul,
KeyHanja = 123, KeyHanja = 123,
KeyYen = 124, KeyYen = 124,
@@ -518,16 +521,16 @@ impl TryFrom<Linux> for Windows {
Linux::KeyKp3 => Ok(Self::Keypad3PageDn), Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
Linux::KeyKp0 => Ok(Self::Keypad0Insert), Linux::KeyKp0 => Ok(Self::Keypad0Insert),
Linux::KeyKpDot => Ok(Self::KeypadDot), Linux::KeyKpDot => Ok(Self::KeypadDot),
Linux::KeyZenkakuhankaku => Ok(Self::KeyLANG1), // TODO unsure Linux::KeyZenkakuhankaku => Ok(Self::KeyF24), // KeyLANG5
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
Linux::KeyF11 => Ok(Self::KeyF11), Linux::KeyF11 => Ok(Self::KeyF11),
Linux::KeyF12 => Ok(Self::KeyF12), Linux::KeyF12 => Ok(Self::KeyF12),
Linux::KeyRo => Ok(Self::ErrorRollOver), // TODO unsure Linux::KeyRo => Ok(Self::KeyInternational1),
Linux::KeyKatakana => Ok(Self::KeyLANG1), // TODO unsure Linux::KeyKatakana => Ok(Self::KeyLANG3),
Linux::KeyHiragana => Ok(Self::KeyLANG2), // TODO unsure Linux::KeyHiragana => Ok(Self::KeyLANG4),
Linux::KeyHenkan => Ok(Self::KeyLANG3), // TODO unsure Linux::KeyHenkan => Ok(Self::KeyInternational4),
Linux::KeyKatakanahiragana => Ok(Self::KeyLANG4), // TODO unsure Linux::KeyKatakanahiragana => Ok(Self::KeyInternational2),
Linux::KeyMuhenkan => Ok(Self::KeyLANG4), // TODO unsure Linux::KeyMuhenkan => Ok(Self::KeyInternational5),
Linux::KeyKpJpComma => Ok(Self::KeypadComma), Linux::KeyKpJpComma => Ok(Self::KeypadComma),
Linux::KeyKpEnter => Ok(Self::KeypadEnter), Linux::KeyKpEnter => Ok(Self::KeypadEnter),
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl), Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
@@ -555,9 +558,9 @@ impl TryFrom<Linux> for Windows {
Linux::KeyPause => Ok(Self::KeyPause), Linux::KeyPause => Ok(Self::KeyPause),
Linux::KeyScale => Err(()), // TODO Linux::KeyScale => Err(()), // TODO
Linux::KeyKpcomma => Ok(Self::KeypadComma), Linux::KeyKpcomma => Ok(Self::KeypadComma),
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure Linux::KeyHanguel => Ok(Self::KeyLANG1), // FIXME should be 00F2?
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure Linux::KeyHanja => Ok(Self::KeyLANG2), // FIXME should be 00F1?
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure Linux::KeyYen => Ok(Self::KeyInternational3),
Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI), Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI),
Linux::KeyRightmeta => Ok(Self::KeyRightGUI), Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
Linux::KeyCompose => Ok(Self::KeyApplication), Linux::KeyCompose => Ok(Self::KeyApplication),
@@ -807,21 +810,22 @@ impl TryFrom<Windows> for Linux {
Windows::KeyF23 => Ok(Self::KeyF23), Windows::KeyF23 => Ok(Self::KeyF23),
Windows::KeyF24 => Ok(Self::KeyF24), Windows::KeyF24 => Ok(Self::KeyF24),
Windows::KeypadComma => Ok(Self::KeyKpcomma), Windows::KeypadComma => Ok(Self::KeyKpcomma),
Windows::KeyInternational1 => Ok(Self::KeyHangeul), Windows::KeyInternational1 => Ok(Self::KeyRo),
Windows::KeyInternational2 => Ok(Self::KeyHanja), Windows::KeyInternational2 => Ok(Self::KeyKatakanahiragana),
Windows::KeyInternational3 => Ok(Self::KeyYen), Windows::KeyInternational3 => Ok(Self::KeyYen),
Windows::KeyInternational4 => Err(()), Windows::KeyInternational4 => Ok(Self::KeyHenkan),
Windows::KeyInternational5 => Err(()), Windows::KeyInternational5 => Ok(Self::KeyMuhenkan),
Windows::KeyLANG1 => Ok(Self::KeyKatakana), Windows::KeyLANG1 => Ok(Self::KeyHanguel),
Windows::KeyLANG2 => Ok(Self::KeyHiragana), Windows::KeyLANG2 => Ok(Self::KeyHanja),
Windows::KeyLANG3 => Ok(Self::KeyHenkan), Windows::KeyLANG3 => Ok(Self::KeyKatakana),
Windows::KeyLANG4 => Ok(Self::KeyKatakanahiragana), Windows::KeyLANG4 => Ok(Self::KeyHiragana),
Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl), Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Windows::KeyLeftShift => Ok(Self::KeyLeftShift), Windows::KeyLeftShift => Ok(Self::KeyLeftShift),
Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt), Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt),
Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta), Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta),
Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl), Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Windows::KeyRightShift => Ok(Self::KeyRightShift), Windows::KeyRightShift => Ok(Self::KeyRightShift),
Windows::KeyFakeRightShift => Ok(Self::KeyRightShift),
Windows::KeyRightAlt => Ok(Self::KeyRightalt), Windows::KeyRightAlt => Ok(Self::KeyRightalt),
Windows::KeyRightGUI => Ok(Self::KeyRightmeta), Windows::KeyRightGUI => Ok(Self::KeyRightmeta),
Windows::KeyScanNextTrack => Ok(Self::KeyNextsong), Windows::KeyScanNextTrack => Ok(Self::KeyNextsong),

View File

@@ -8,7 +8,7 @@ use tokio::signal;
use crate::{ use crate::{
client::{ClientConfig, ClientHandle, ClientManager, ClientState}, client::{ClientConfig, ClientHandle, ClientManager, ClientState},
config::Config, config::{CaptureBackend, Config},
dns, dns,
frontend::{FrontendListener, FrontendRequest}, frontend::{FrontendListener, FrontendRequest},
server::capture_task::CaptureEvent, server::capture_task::CaptureEvent,
@@ -55,6 +55,7 @@ impl Server {
fix_ips: config_client.ips.into_iter().collect(), fix_ips: config_client.ips.into_iter().collect(),
port: config_client.port, port: config_client.port,
pos: config_client.pos, pos: config_client.pos,
cmd: config_client.enter_hook,
}; };
let state = ClientState { let state = ClientState {
active: config_client.active, active: config_client.active,
@@ -76,7 +77,7 @@ impl Server {
} }
} }
pub async fn run(&self) -> anyhow::Result<()> { pub async fn run(&self, backend: Option<CaptureBackend>) -> anyhow::Result<()> {
// create frontend communication adapter // create frontend communication adapter
let frontend = match FrontendListener::new().await { let frontend = match FrontendListener::new().await {
Some(f) => f?, Some(f) => f?,
@@ -96,11 +97,12 @@ impl Server {
// input capture // input capture
let (mut capture_task, capture_channel) = capture_task::new( let (mut capture_task, capture_channel) = capture_task::new(
backend,
self.clone(), self.clone(),
sender_tx.clone(), sender_tx.clone(),
timer_tx.clone(), timer_tx.clone(),
self.release_bind.clone(), self.release_bind.clone(),
); )?;
// input emulation // input emulation
let (mut emulation_task, emulate_channel) = emulation_task::new( let (mut emulation_task, emulate_channel) = emulation_task::new(

View File

@@ -2,11 +2,12 @@ use anyhow::{anyhow, Result};
use futures::StreamExt; use futures::StreamExt;
use std::{collections::HashSet, net::SocketAddr}; use std::{collections::HashSet, net::SocketAddr};
use tokio::{sync::mpsc::Sender, task::JoinHandle}; use tokio::{process::Command, sync::mpsc::Sender, task::JoinHandle};
use crate::{ use crate::{
capture::{self, InputCapture}, capture::{self, error::CaptureCreationError, InputCapture},
client::{ClientEvent, ClientHandle}, client::{ClientEvent, ClientHandle},
config::CaptureBackend,
event::{Event, KeyboardEvent}, event::{Event, KeyboardEvent},
scancode, scancode,
server::State, server::State,
@@ -25,14 +26,15 @@ pub enum CaptureEvent {
} }
pub fn new( pub fn new(
backend: Option<CaptureBackend>,
server: Server, server: Server,
sender_tx: Sender<(Event, SocketAddr)>, sender_tx: Sender<(Event, SocketAddr)>,
timer_tx: Sender<()>, timer_tx: Sender<()>,
release_bind: Vec<scancode::Linux>, release_bind: Vec<scancode::Linux>,
) -> (JoinHandle<Result<()>>, Sender<CaptureEvent>) { ) -> Result<(JoinHandle<Result<()>>, Sender<CaptureEvent>), CaptureCreationError> {
let (tx, mut rx) = tokio::sync::mpsc::channel(32); let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let task = tokio::task::spawn_local(async move { let task = tokio::task::spawn_local(async move {
let mut capture = capture::create().await; let mut capture = capture::create(backend).await?;
let mut pressed_keys = HashSet::new(); let mut pressed_keys = HashSet::new();
loop { loop {
tokio::select! { tokio::select! {
@@ -62,7 +64,7 @@ pub fn new(
} }
anyhow::Ok(()) anyhow::Ok(())
}); });
(task, tx) Ok((task, tx))
} }
fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) { fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) {
@@ -140,6 +142,9 @@ async fn handle_capture_event(
if start_timer { if start_timer {
let _ = timer_tx.try_send(()); let _ = timer_tx.try_send(());
} }
if enter {
spawn_hook_command(server, handle);
}
if let Some(addr) = addr { if let Some(addr) = addr {
if enter { if enter {
let _ = sender_tx.send((Event::Enter(), addr)).await; let _ = sender_tx.send((Event::Enter(), addr)).await;
@@ -148,3 +153,34 @@ async fn handle_capture_event(
} }
Ok(()) Ok(())
} }
fn spawn_hook_command(server: &Server, handle: ClientHandle) {
let Some(cmd) = server
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.cmd.clone())
else {
return;
};
tokio::task::spawn_local(async move {
log::info!("spawning command!");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute cmd: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
}
});
}

View File

@@ -107,20 +107,22 @@ async fn handle_frontend_event(
log::debug!("frontend: {event:?}"); log::debug!("frontend: {event:?}");
match event { match event {
FrontendRequest::Create => { FrontendRequest::Create => {
add_client(server, frontend).await; let handle = add_client(server, frontend).await;
resolve_dns(server, resolve_tx, handle).await;
} }
FrontendRequest::Activate(handle, active) => { FrontendRequest::Activate(handle, active) => {
if active { if active {
activate_client(server, frontend, capture, emulate, handle).await; activate_client(server, capture, emulate, handle).await;
} else { } else {
deactivate_client(server, frontend, capture, emulate, handle).await; deactivate_client(server, capture, emulate, handle).await;
} }
} }
FrontendRequest::ChangePort(port) => { FrontendRequest::ChangePort(port) => {
let _ = port_tx.send(port).await; let _ = port_tx.send(port).await;
} }
FrontendRequest::Delete(handle) => { FrontendRequest::Delete(handle) => {
remove_client(server, frontend, capture, emulate, handle).await; remove_client(server, capture, emulate, handle).await;
broadcast(frontend, FrontendEvent::Deleted(handle)).await;
} }
FrontendRequest::Enumerate() => { FrontendRequest::Enumerate() => {
let clients = server let clients = server
@@ -131,65 +133,74 @@ async fn handle_frontend_event(
.collect(); .collect();
broadcast(frontend, FrontendEvent::Enumerate(clients)).await; broadcast(frontend, FrontendEvent::Enumerate(clients)).await;
} }
FrontendRequest::GetState(handle) => {
broadcast_client(server, frontend, handle).await;
}
FrontendRequest::Terminate() => { FrontendRequest::Terminate() => {
log::info!("terminating gracefully..."); log::info!("terminating gracefully...");
return true; return true;
} }
FrontendRequest::UpdateFixIps(handle, fix_ips) => { FrontendRequest::UpdateFixIps(handle, fix_ips) => {
update_fix_ips(server, resolve_tx, handle, fix_ips).await; update_fix_ips(server, handle, fix_ips).await;
broadcast_client_update(server, frontend, handle).await; resolve_dns(server, resolve_tx, handle).await;
} }
FrontendRequest::UpdateHostname(handle, hostname) => { FrontendRequest::UpdateHostname(handle, hostname) => {
update_hostname(server, resolve_tx, handle, hostname).await; update_hostname(server, resolve_tx, handle, hostname).await;
broadcast_client_update(server, frontend, handle).await; resolve_dns(server, resolve_tx, handle).await;
} }
FrontendRequest::UpdatePort(handle, port) => { FrontendRequest::UpdatePort(handle, port) => {
update_port(server, handle, port).await; update_port(server, handle, port).await;
broadcast_client_update(server, frontend, handle).await;
} }
FrontendRequest::UpdatePosition(handle, pos) => { FrontendRequest::UpdatePosition(handle, pos) => {
update_pos(server, handle, capture, emulate, pos).await; update_pos(server, handle, capture, emulate, pos).await;
broadcast_client_update(server, frontend, handle).await;
} }
FrontendRequest::ResolveDns(handle) => { FrontendRequest::ResolveDns(handle) => {
let hostname = server resolve_dns(server, resolve_tx, handle).await;
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.hostname.clone());
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
} }
}; };
false false
} }
async fn resolve_dns(server: &Server, resolve_tx: &Sender<DnsRequest>, handle: ClientHandle) {
let hostname = server
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.hostname.clone());
if let Some(hostname) = hostname {
let _ = resolve_tx
.send(DnsRequest {
hostname: hostname.clone(),
handle,
})
.await;
}
}
async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) { async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) {
if let Err(e) = frontend.broadcast_event(event).await { if let Err(e) = frontend.broadcast_event(event).await {
log::error!("error notifying frontend: {e}"); log::error!("error notifying frontend: {e}");
} }
} }
pub async fn add_client(server: &Server, frontend: &mut FrontendListener) { pub async fn add_client(server: &Server, frontend: &mut FrontendListener) -> ClientHandle {
let handle = server.client_manager.borrow_mut().add_client(); let handle = server.client_manager.borrow_mut().add_client();
log::info!("added client {handle}"); log::info!("added client {handle}");
let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone(); let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone();
broadcast(frontend, FrontendEvent::Created(handle, c, s)).await; broadcast(frontend, FrontendEvent::Created(handle, c, s)).await;
handle
} }
pub async fn deactivate_client( pub async fn deactivate_client(
server: &Server, server: &Server,
frontend: &mut FrontendListener,
capture: &Sender<CaptureEvent>, capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>, emulate: &Sender<EmulationEvent>,
handle: ClientHandle, handle: ClientHandle,
) { ) {
let state = match server.client_manager.borrow_mut().get_mut(handle) { match server.client_manager.borrow_mut().get_mut(handle) {
Some((_, s)) => { Some((_, s)) => {
s.active = false; s.active = false;
s.clone()
} }
None => return, None => return,
}; };
@@ -197,13 +208,10 @@ pub async fn deactivate_client(
let event = ClientEvent::Destroy(handle); let event = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::ClientEvent(event)).await; let _ = capture.send(CaptureEvent::ClientEvent(event)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(event)).await; let _ = emulate.send(EmulationEvent::ClientEvent(event)).await;
let event = FrontendEvent::StateChange(handle, state);
broadcast(frontend, event).await;
} }
pub async fn activate_client( pub async fn activate_client(
server: &Server, server: &Server,
frontend: &mut FrontendListener,
capture: &Sender<CaptureEvent>, capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>, emulate: &Sender<EmulationEvent>,
handle: ClientHandle, handle: ClientHandle,
@@ -217,14 +225,13 @@ pub async fn activate_client(
let other = server.client_manager.borrow_mut().find_client(pos); let other = server.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other { if let Some(other) = other {
if other != handle { if other != handle {
deactivate_client(server, frontend, capture, emulate, other).await; deactivate_client(server, capture, emulate, other).await;
} }
} }
/* activate the client */ /* activate the client */
let state = if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) { if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) {
s.active = true; s.active = true;
s.clone()
} else { } else {
return; return;
}; };
@@ -233,13 +240,10 @@ pub async fn activate_client(
let event = ClientEvent::Create(handle, pos); let event = ClientEvent::Create(handle, pos);
let _ = capture.send(CaptureEvent::ClientEvent(event)).await; let _ = capture.send(CaptureEvent::ClientEvent(event)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(event)).await; let _ = emulate.send(EmulationEvent::ClientEvent(event)).await;
let event = FrontendEvent::StateChange(handle, state);
broadcast(frontend, event).await;
} }
pub async fn remove_client( pub async fn remove_client(
server: &Server, server: &Server,
frontend: &mut FrontendListener,
capture: &Sender<CaptureEvent>, capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>, emulate: &Sender<EmulationEvent>,
handle: ClientHandle, handle: ClientHandle,
@@ -258,30 +262,15 @@ pub async fn remove_client(
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await; let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await; let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
} }
let event = FrontendEvent::Deleted(handle);
broadcast(frontend, event).await;
} }
async fn update_fix_ips( async fn update_fix_ips(server: &Server, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
server: &Server, let mut client_manager = server.client_manager.borrow_mut();
resolve_tx: &Sender<DnsRequest>, let Some((c, _)) = client_manager.get_mut(handle) else {
handle: ClientHandle, return;
fix_ips: Vec<IpAddr>,
) {
let hostname = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, _)) = client_manager.get_mut(handle) else {
return;
};
c.fix_ips = fix_ips;
c.hostname.clone()
}; };
if let Some(hostname) = hostname { c.fix_ips = fix_ips;
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
} }
async fn update_hostname( async fn update_hostname(
@@ -356,11 +345,11 @@ async fn update_pos(
} }
} }
async fn broadcast_client_update( async fn broadcast_client(server: &Server, frontend: &mut FrontendListener, handle: ClientHandle) {
server: &Server, let client = server.client_manager.borrow().get(handle).cloned();
frontend: &mut FrontendListener, if let Some((config, state)) = client {
handle: ClientHandle, broadcast(frontend, FrontendEvent::State(handle, config, state)).await;
) { } else {
let (client, _) = server.client_manager.borrow().get(handle).unwrap().clone(); broadcast(frontend, FrontendEvent::NoSuchClient(handle)).await;
broadcast(frontend, FrontendEvent::Updated(handle, client)).await; }
} }

View File

@@ -35,7 +35,7 @@ pub fn new(
Ok(ips) => ips, Ok(ips) => ips,
Err(e) => { Err(e) => {
log::warn!("could not resolve host '{host}': {e}"); log::warn!("could not resolve host '{host}': {e}");
continue; vec![]
} }
}; };
@@ -59,14 +59,10 @@ async fn notify_state_change(
server: &mut Server, server: &mut Server,
handle: ClientHandle, handle: ClientHandle,
) { ) {
let state = server let state = server.client_manager.borrow_mut().get_mut(handle).cloned();
.client_manager if let Some((config, state)) = state {
.borrow_mut()
.get_mut(handle)
.map(|(_, s)| s.clone());
if let Some(state) = state {
let _ = frontend let _ = frontend
.send(FrontendEvent::StateChange(handle, state)) .send(FrontendEvent::State(handle, config, state))
.await; .await;
} }
} }