Compare commits

..

30 Commits

Author SHA1 Message Date
Ferdinand Schober
bc6d35c457 windows: fix resolution with scaling enabled 2024-05-06 20:42:24 +02:00
Ferdinand Schober
effb9ce0fa libei: fix touchpad scrolling (#121) 2024-05-04 12:53:45 +02:00
Ferdinand Schober
1f0d386d4a add discrete120 scroll event (#120)
* add discrete120 scroll event
2024-05-04 03:34:13 +02:00
Ferdinand Schober
973360a774 libei emulation: use discrete scroll events 2024-05-04 01:27:38 +02:00
Ferdinand Schober
e21ab02a6e layer-shell: use value120 scroll events #115 2024-05-04 01:27:13 +02:00
Ferdinand Schober
18a3c10f8e Update FUNDING.yml 2024-05-03 16:40:59 +02:00
Ferdinand Schober
c76d9ef7af implement dns indicator (#119) 2024-05-03 13:00:00 +02:00
Ferdinand Schober
5318f5a02d Separate config state (#118)
* change internal api
* frontend now keeps and more correctly reflects backend state
2024-05-03 11:27:06 +02:00
Johan
1e4312b3ce nix: Add macOS launchd service to hm-module module (#110)
Home manager has the possibility to run macOS launchd services. These
are automatically disabled when running on linux.
2024-04-28 12:25:35 +02:00
Ferdinand Schober
77aa96e09a defer creation of input capture / emulation (#117) 2024-04-26 23:59:00 +02:00
Ferdinand Schober
636c5924bf trust_dns_resolver is now hickory_resolver (#116) 2024-04-26 22:52:00 +02:00
Ferdinand Schober
3e96b42067 use slab instead of reinventing the wheel (#112) 2024-04-26 00:10:04 +02:00
Ferdinand Schober
279e582698 Rename FrontendEvent to FrontendRequest (#111)
* rename frontend event and notify
* simplify event names
2024-04-25 22:18:43 +02:00
Johan
9edd2f7f3b nix: enable creating config file via home-manager (#109) 2024-04-25 14:06:31 +02:00
Johan
43c16a537b nix: Add aarch64-darwin package and devshell (#108)
Enable building and developing using nix on aarch64-darwin
2024-04-24 21:27:24 +02:00
Ferdinand Schober
36855a1a17 pub glib-build-tools behind gtk feature flag 2024-04-15 10:14:48 +02:00
Ferdinand Schober
e537cdbc7e properly reset copy icon 2024-04-13 00:28:53 +02:00
Ferdinand Schober
5b76c3bcda add hostname entry row with clipboard button 2024-04-13 00:24:46 +02:00
Ferdinand Schober
81f65dcd3d gtk: use predifined css classes instead of custom ones 2024-04-13 00:24:46 +02:00
Ferdinand Schober
f0099ee535 windows: revert scrolling multiplier 2024-04-12 15:28:21 +02:00
Ferdinand Schober
633d2c346e windows: impl back and forward mouse buttons 2024-04-12 15:28:05 +02:00
Ferdinand Schober
ccb201ea53 formatting 2024-04-11 13:54:11 +02:00
Ferdinand Schober
f7edfecba9 add tests for capture and emulation 2024-04-11 13:53:49 +02:00
Ferdinand Schober
141ea2809d fix a race condition in windows capture 2024-04-11 13:09:38 +02:00
Ferdinand Schober
058097c618 Update OS support table 2024-04-11 04:05:15 +02:00
Ferdinand Schober
f9eeb254d3 Windows Input Capture (#100)
initial support for windows input capture.
Some things need fixing;
- scrolling
- mouse buttons > 2
2024-04-11 03:55:42 +02:00
Ferdinand Schober
9ca7e2378c enforce only one client at a position (#102) 2024-04-10 14:16:19 +02:00
Ferdinand Schober
cc7984c066 fix clippy lint 2024-04-08 21:14:07 +02:00
Ferdinand Schober
1a2645cfbc fix formatting (#101) 2024-04-08 17:52:16 +02:00
Ferdinand Schober
e52febf457 simplify windows installation instructions 2024-04-08 17:48:22 +02:00
51 changed files with 2477 additions and 878 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
github: [feschber]
ko_fi: feschber

View File

@@ -3,8 +3,14 @@ name: Binary Cache
on: [push, pull_request, workflow_dispatch]
jobs:
nix:
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
- macos-14
name: "Build"
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -20,5 +26,15 @@ jobs:
name: lan-mouse
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build lan-mouse
run: nix build --print-build-logs
- name: Build lan-mouse (x86_64-linux)
if: matrix.os == 'ubuntu-latest'
run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
- name: Build lan-mouse (x86_64-darwin)
if: matrix.os == 'macos-latest'
run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse
- name: Build lan-mouse (aarch64-darwin)
if: matrix.os == 'macos-14'
run: nix build --print-build-logs --show-trace .#packages.aarch64-darwin.lan-mouse

153
Cargo.lock generated
View File

@@ -1131,6 +1131,51 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-proto"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna 0.4.0",
"ipnet",
"once_cell",
"rand",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "hostname"
version = "0.3.1"
@@ -1142,6 +1187,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "hostname"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
dependencies = [
"cfg-if",
"libc",
"windows 0.52.0",
]
[[package]]
name = "humantime"
version = "2.1.0"
@@ -1241,24 +1297,27 @@ dependencies = [
"futures-core",
"glib-build-tools",
"gtk4",
"hickory-resolver",
"hostname 0.4.0",
"keycode",
"libadwaita",
"libc",
"log",
"memmap",
"num_enum",
"once_cell",
"reis",
"serde",
"serde_json",
"slab",
"tempfile",
"tokio",
"toml",
"trust-dns-resolver",
"wayland-client",
"wayland-protocols",
"wayland-protocols-misc",
"wayland-protocols-wlr",
"windows",
"windows 0.54.0",
"x11",
]
@@ -1427,6 +1486,27 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.53",
]
[[package]]
name = "object"
version = "0.32.2"
@@ -1711,7 +1791,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname",
"hostname 0.3.1",
"quick-error",
]
@@ -2069,52 +2149,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "trust-dns-proto"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna 0.4.0",
"ipnet",
"once_cell",
"rand",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
"trust-dns-proto",
]
[[package]]
name = "typenum"
version = "1.17.0"
@@ -2309,13 +2343,32 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core 0.52.0",
"windows-targets 0.52.4",
]
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core",
"windows-core 0.54.0",
"windows-targets 0.52.4",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.4",
]

View File

@@ -14,7 +14,7 @@ lto = "fat"
[dependencies]
tempfile = "3.8"
trust-dns-resolver = "0.23"
hickory-resolver = "0.24.1"
memmap = "0.7"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
@@ -22,7 +22,7 @@ anyhow = "1.0.71"
log = "0.4.20"
env_logger = "0.11.3"
serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
tokio = {version = "1.32.0", features = ["io-util", "io-std", "macros", "net", "rt", "sync", "signal"] }
async-trait = "0.1.73"
futures-core = "0.3.28"
futures = "0.3.28"
@@ -32,6 +32,9 @@ adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional
async-channel = { version = "2.1.1", optional = true }
keycode = "0.4.0"
once_cell = "1.19.0"
num_enum = "0.7.2"
hostname = "0.4.0"
slab = "0.4.9"
[target.'cfg(unix)'.dependencies]
libc = "0.2.148"
@@ -49,10 +52,18 @@ reis = { version = "0.2", features = [ "tokio" ], optional = true }
core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.54.0", features = [ "Win32_UI_Input_KeyboardAndMouse" ] }
windows = { version = "0.54.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",
"Win32_Graphics",
"Win32_Graphics_Gdi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
] }
[build-dependencies]
glib-build-tools = "0.19.0"
glib-build-tools = { version = "0.19.0", optional = true }
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
@@ -60,4 +71,4 @@ wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols
x11 = ["dep:x11"]
xdg_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"]

View File

@@ -39,13 +39,13 @@ For an alternative (with slightly different goals) you may check out [Input Leap
The following table shows support for input emulation (to emulate events received from other clients) and
input capture (to send events *to* other clients) on different operating systems:
| Backend | input emulation | input capture |
| OS / Desktop Environment | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: (starting at GNOME 45) |
| Windows | :heavy_check_mark: | :heavy_check_mark: |
| X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP |
| MacOS | :heavy_check_mark: | WIP |
> [!Important]
@@ -58,13 +58,13 @@ input capture (to send events *to* other clients) on different operating systems
> Otherwise input capture will not work.
## Installation
### Install with cargo
### Install via cargo
```sh
cargo install lan-mouse
```
### Download from Releases
The easiest way to install Lan Mouse is to download precompiled release binaries from the [releases section](https://github.com/feschber/lan-mouse/releases).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
@@ -83,7 +83,9 @@ paru -S lan-mouse-bin
- flake: [README.md](./nix/README.md)
### Building from Source
### Manual Installation
First make sure to [install the necessary dependencies](#installing-dependencies).
Build in release mode:
```sh
@@ -138,56 +140,61 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
## Installing Dependencies
<details>
<summary>MacOS</summary>
#### Macos
```sh
brew install libadwaita
```
</details>
<details>
<summary>Ubuntu and derivatives</summary>
#### Ubuntu and derivatives
```sh
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
```
</details>
<details>
<summary>Arch and derivatives</summary>
#### Arch and derivatives
```sh
sudo pacman -S libadwaita gtk libx11 libxtst
```
</details>
<details>
<summary>Fedora and derivatives</summary>
#### Fedora and derivatives
```sh
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
</details>
<details>
<summary>Windows</summary>
#### Windows
> [!NOTE]
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
- First install [Rust](https://www.rust-lang.org/tools/install).
- Then follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:*
Build gtk from source
- The following commands should be run in an admin power shell instance:
- The following commands should be run in an **admin power shell** instance:
```sh
# install chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# install python 3.11 (Version is important, as 3.12 does not work currently) -> Has been fixed recently
choco install python --version=3.11.0
# install git
choco install git
# install msys2
choco install msys2
# install Visual Studio 2022
choco install visualstudio2022-workload-vctools
# install gvsbuild dependencies
choco install python git msys2 visualstudio2022-workload-vctools
```
- The following commands should be run in a regular power shell instance:
- The following commands should be run in a **regular power shell** instance:
```sh
# install gvsbuild with python
@@ -203,11 +210,12 @@ pipx install gvsbuild
gvsbuild build gtk4 libadwaita librsvg
```
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
- **Make sure to add the directory** `C:\gtk-build\gtk\x64\release\bin`
[**to the `PATH` environment variable**]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below).
</details>
## Usage
### Gtk Frontend

View File

@@ -1,5 +1,6 @@
fn main() {
// composite_templates
#[cfg(feature = "gtk")]
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresource.xml",

View File

@@ -15,6 +15,8 @@
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"aarch64-darwin"
"x86_64-darwin"
"x86_64-linux"
];
pkgsFor = system:
@@ -49,6 +51,8 @@
gtk4
libadwaita
xorg.libXtst
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk_11_0.frameworks.CoreGraphics
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";

View File

@@ -34,6 +34,9 @@ enable lan-mouse
enable = true;
# systemd = false;
# package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
# Optional configuration in nix syntax, see config.toml for available options
# settings = { };
};
};
}

View File

@@ -18,6 +18,8 @@ rustPlatform.buildRustPackage {
gtk4
libadwaita
xorg.libXtst
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk_11_0.frameworks.CoreGraphics
];
src = builtins.path {

View File

@@ -7,6 +7,7 @@ self: {
with lib; let
cfg = config.programs.lan-mouse;
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
tomlFormat = pkgs.formats.toml {};
in {
options.programs.lan-mouse = with types; {
enable = mkEnableOption "Whether or not to enable lan-mouse.";
@@ -23,7 +24,23 @@ in {
systemd = mkOption {
type = types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for lan-mouse.";
description = "Whether to enable to systemd service for lan-mouse on linux.";
};
launchd = mkOption {
type = types.bool;
default = pkgs.stdenv.isDarwin;
description = "Whether to enable to launchd service for lan-mouse on macOS.";
};
settings = lib.mkOption {
inherit (tomlFormat) type;
default = {};
example = builtins.fromTOML (builtins.readFile (self + /config.toml));
description = ''
Optional configuration written to {file}`$XDG_CONFIG_HOME/lan-mouse/config.toml`.
See <https://github.com/feschber/lan-mouse/> for
available options and documentation.
'';
};
};
@@ -43,8 +60,23 @@ in {
];
};
launchd.agents.lan-mouse = lib.mkIf cfg.launchd {
enable = true;
config = {
ProgramArguments = [
"${cfg.package}/bin/lan-mouse"
"--daemon"
];
KeepAlive = true;
};
};
home.packages = [
cfg.package
];
xdg.configFile."lan-mouse/config.toml" = lib.mkIf (cfg.settings != {}) {
source = tomlFormat.generate "config.toml" cfg.settings;
};
};
}

View File

@@ -11,6 +11,20 @@
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
<child type="suffix">
<object class="GtkButton" id="dns_button">
<signal name="clicked" handler="handle_request_dns" swapped="true"/>
<!--<property name="icon-name">network-wired-disconnected-symbolic</property>-->
<property name="icon-name">network-wired-symbolic</property>
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">resolve host</property>
</object>
</child>
<child type="suffix">
<object class="GtkSpinner" id="dns_loading_indicator">
</object>
</child>
<!-- host -->
<child>
<object class="AdwActionRow">
@@ -66,6 +80,7 @@
<property name="valign">center</property>
<property name="halign">center</property>
<property name="name">delete-button</property>
<style><class name="error"/></style>
</object>
</child>
</object>

View File

@@ -3,10 +3,8 @@
<gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true">style.css</file>
<file compressed="true">style-dark.css</file>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
</gresource>
</gresources>

View File

@@ -1,11 +0,0 @@
#delete-button {
color: @red_1;
}
#port-edit-cancel {
color: @red_1;
}
#port-edit-apply {
color: @green_1;
}

View File

@@ -1,11 +0,0 @@
#delete-button {
color: @red_3;
}
#port-edit-cancel {
color: @red_3;
}
#port-edit-apply {
color: @green_3;
}

View File

@@ -84,6 +84,7 @@
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-apply</property>
<style><class name="success"/></style>
</object>
</child>
<child>
@@ -93,6 +94,26 @@
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-cancel</property>
<style><class name="error"/></style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="copy-hostname-button">
<property name="icon-name">edit-copy-symbolic</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
</object>
</child>
</object>

View File

@@ -25,7 +25,7 @@ pub mod x11;
/// fallback input capture (does not produce events)
pub mod dummy;
pub async fn create() -> Box<dyn InputCapture> {
pub async fn create() -> Box<dyn InputCapture<Item = io::Result<(ClientHandle, Event)>>> {
#[cfg(target_os = "macos")]
match macos::MacOSInputCapture::new() {
Ok(p) => return Box::new(p),

View File

@@ -46,7 +46,7 @@ enum ProducerEvent {
pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>,
libei_task: JoinHandle<Result<()>>,
event_rx: tokio::sync::mpsc::Receiver<(u32, Event)>,
event_rx: tokio::sync::mpsc::Receiver<(ClientHandle, Event)>,
notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>,
}
@@ -119,7 +119,7 @@ async fn update_barriers(
.set_pointer_barriers(session, &barriers, zones.zone_set())
.await?;
let response = response.response()?;
log::info!("{response:?}");
log::debug!("{response:?}");
Ok(id_map)
}
@@ -132,7 +132,7 @@ impl<'a> Drop for LibeiInputCapture<'a> {
async fn create_session<'a>(
input_capture: &'a InputCapture<'a>,
) -> Result<(Session<'a>, BitFlags<Capabilities>)> {
log::info!("creating input capture session");
log::debug!("creating input capture session");
let (session, capabilities) = loop {
match input_capture
.create_session(
@@ -154,7 +154,7 @@ async fn connect_to_eis(
input_capture: &InputCapture<'_>,
session: &Session<'_>,
) -> Result<(ei::Context, EiConvertEventStream)> {
log::info!("connect_to_eis");
log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?;
// create unix stream from fd
@@ -183,7 +183,7 @@ async fn connect_to_eis(
async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream,
context: ei::Context,
event_tx: Sender<(u32, Event)>,
event_tx: Sender<(ClientHandle, Event)>,
current_client: Rc<Cell<Option<ClientHandle>>>,
) -> Result<()> {
loop {
@@ -270,7 +270,7 @@ impl<'a> LibeiInputCapture<'a> {
)
.await?;
log::info!("enabling session");
log::debug!("enabling session");
input_capture.enable(&session).await?;
loop {
@@ -396,7 +396,7 @@ async fn handle_ei_event(
ei_event: EiEvent,
current_client: Option<ClientHandle>,
context: &ei::Context,
event_tx: &Sender<(u32, Event)>,
event_tx: &Sender<(ClientHandle, Event)>,
) {
match ei_event {
EiEvent::SeatAdded(s) => {
@@ -466,15 +466,38 @@ async fn handle_ei_event(
.unwrap();
}
}
EiEvent::ScrollDelta(_) => {}
EiEvent::ScrollDelta(delta) => {
if let Some(handle) = current_client {
let mut events = vec![];
if delta.dy != 0. {
events.push(PointerEvent::Axis {
time: 0,
axis: 0,
value: delta.dy as f64,
});
}
if delta.dx != 0. {
events.push(PointerEvent::Axis {
time: 0,
axis: 1,
value: delta.dx as f64,
});
}
for event in events {
event_tx
.send((handle, Event::Pointer(event)))
.await
.unwrap();
}
}
}
EiEvent::ScrollStop(_) => {}
EiEvent::ScrollCancel(_) => {}
EiEvent::ScrollDiscrete(scroll) => {
if scroll.discrete_dy != 0 {
let event = PointerEvent::Axis {
time: 0,
let event = PointerEvent::AxisDiscrete120 {
axis: 0,
value: scroll.discrete_dy as f64,
value: scroll.discrete_dy,
};
if let Some(current_client) = current_client {
event_tx
@@ -484,10 +507,9 @@ async fn handle_ei_event(
}
}
if scroll.discrete_dx != 0 {
let event = PointerEvent::Axis {
time: 0,
let event = PointerEvent::AxisDiscrete120 {
axis: 1,
value: scroll.discrete_dx as f64,
value: scroll.discrete_dx,
};
if let Some(current_client) = current_client {
event_tx

View File

@@ -761,6 +761,16 @@ impl Dispatch<WlPointer, ()> for State {
}),
));
}
wl_pointer::Event::AxisValue120 { axis, value120 } => {
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push_back((
*client,
Event::Pointer(PointerEvent::AxisDiscrete120 {
axis: u32::from(axis) as u8,
value: value120,
}),
));
}
wl_pointer::Event::Frame {} => {
// TODO properly handle frame events
// we simply insert a frame event on the client side

View File

@@ -1,35 +1,612 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use core::task::{Context, Poll};
use futures::Stream;
use std::{io, pin::Pin};
use once_cell::unsync::Lazy;
use std::collections::HashMap;
use std::ptr::{addr_of, addr_of_mut};
use futures::executor::block_on;
use std::default::Default;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc;
use std::task::ready;
use std::{io, pin::Pin, thread};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK,
HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL,
WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
WNDPROC,
};
use crate::client::Position;
use crate::event::{
KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use crate::scancode::Linux;
use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle},
event::Event,
scancode,
};
pub struct WindowsInputCapture {}
pub struct WindowsInputCapture {
event_rx: Receiver<(ClientHandle, Event)>,
msg_thread: Option<std::thread::JoinHandle<()>>,
}
enum EventType {
ClientEvent = 0,
Release = 1,
Exit = 2,
}
unsafe fn signal_message_thread(event_type: EventType) {
if let Some(event_tid) = get_event_tid() {
PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap();
} else {
log::warn!("lost event");
}
}
impl InputCapture for WindowsInputCapture {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
fn notify(&mut self, event: ClientEvent) -> io::Result<()> {
unsafe {
EVENT_BUFFER.push(event);
signal_message_thread(EventType::ClientEvent);
}
Ok(())
}
fn release(&mut self) -> io::Result<()> {
unsafe { signal_message_thread(EventType::Release) };
Ok(())
}
}
static mut EVENT_BUFFER: Vec<ClientEvent> = Vec::new();
static mut ACTIVE_CLIENT: Option<ClientHandle> = None;
static mut CLIENT_FOR_POS: Lazy<HashMap<Position, ClientHandle>> = Lazy::new(HashMap::new);
static mut EVENT_TX: Option<Sender<(ClientHandle, Event)>> = None;
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
unsafe fn set_event_tid(tid: u32) {
EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
}
unsafe fn get_event_tid() -> Option<u32> {
match EVENT_THREAD_ID.load(Ordering::SeqCst) {
0 => None,
id => Some(id),
}
}
static mut ENTRY_POINT: (i32, i32) = (0, 0);
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT =
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
}),
WPARAM(p) if p == WM_MBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
}),
WPARAM(p) if p == WM_RBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
}),
WPARAM(p) if p == WM_LBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
}),
WPARAM(p) if p == WM_MBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
}),
WPARAM(p) if p == WM_RBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
}),
WPARAM(p) if p == WM_MOUSEMOVE as usize => unsafe {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT;
let (dx, dy) = (x - ex, y - ey);
Some(PointerEvent::Motion {
time: 0,
relative_x: dx as f64,
relative_y: dy as f64,
})
},
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0,
value: -(mouse_low_level.mouseData as i32),
}),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16;
let button = match hb {
1 => BTN_BACK,
2 => BTN_FORWARD,
_ => {
log::warn!("unknown mouse button");
return None;
}
};
Some(PointerEvent::Button {
time: 0,
button,
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
})
}
w => {
log::warn!("unknown mouse event: {w:?}");
None
}
}
}
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT =
*std::mem::transmute::<LPARAM, *const KBDLLHOOKSTRUCT>(lparam);
let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
scan_code |= 0xE000;
}
let Ok(win_scan_code) = scancode::Windows::try_from(scan_code) else {
log::warn!("failed to translate to windows scancode: {scan_code}");
return None;
};
log::trace!("windows_scan: {win_scan_code:?}");
let Ok(linux_scan_code): Result<Linux, ()> = win_scan_code.try_into() else {
log::warn!("failed to translate into linux scancode: {win_scan_code:?}");
return None;
};
log::trace!("windows_scan: {linux_scan_code:?}");
let scan_code = linux_scan_code as u32;
match wparam {
WPARAM(p) if p == WM_KEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_KEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
WPARAM(p) if p == WM_SYSKEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_SYSKEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
_ => None,
}
}
///
/// 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) {
if let Some(active) = ACTIVE_CLIENT {
block_on(async move {
let _ = EVENT_TX.as_ref().unwrap().send((active, event)).await;
});
}
}
unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.is_some();
}
let mouse_low_level: MSLLHOOKSTRUCT =
unsafe { *std::mem::transmute::<LPARAM, *const MSLLHOOKSTRUCT>(lparam) };
static mut PREV_POS: Option<(i32, i32)> = None;
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.unwrap_or(curr_pos);
PREV_POS.replace(curr_pos);
/* next event is the first actual event */
let ret = ACTIVE_CLIENT.is_some();
/* client already active, no need to check */
if ACTIVE_CLIENT.is_some() {
return ret;
}
/* check if a client was activated */
let Some(pos) = entered_barrier(prev_pos, curr_pos, get_display_regions()) else {
return ret;
};
/* check if a client is registered for the barrier */
let Some(client) = CLIENT_FOR_POS.get(&pos) else {
return ret;
};
/* update active client and entry point */
ACTIVE_CLIENT.replace(*client);
ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
/* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
send_blocking(Event::Enter());
ret
}
unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let active = check_client_activation(wparam, lparam);
/* no client was active */
if !active {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
}
/* get active client if any */
let Some(client) = ACTIVE_CLIENT else {
return LRESULT(1);
};
/* convert to lan-mouse event */
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (client, Event::Pointer(pointer_event));
/* notify mainthread (drop events if sending too fast) */
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */
let Some(client) = ACTIVE_CLIENT else {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
};
/* convert to key event */
let Some(key_event) = to_key_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (client, Event::Keyboard(key_event));
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn window_proc(
_hwnd: HWND,
uint: u32,
_wparam: WPARAM,
_lparam: LPARAM,
) -> LRESULT {
match uint {
x if x == WM_DISPLAYCHANGE => {
log::debug!("display resolution changed");
DISPLAY_RESOLUTION_CHANGED = true;
}
_ => {}
}
LRESULT(1)
}
fn enumerate_displays() -> Vec<RECT> {
unsafe {
let mut display_rects = vec![];
let mut devices = vec![];
for i in 0.. {
let mut device: DISPLAY_DEVICEW = std::mem::zeroed();
device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
let ret = EnumDisplayDevicesW(None, i, &mut device, EDD_GET_DEVICE_INTERFACE_NAME);
if ret == FALSE {
break;
}
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
log::info!("{:?}", device.DeviceName);
devices.push(device.DeviceName);
}
}
for device in devices {
let mut dev_mode: DEVMODEW = std::mem::zeroed();
dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
let ret = EnumDisplaySettingsW(
PCWSTR::from_raw(&device as *const _),
ENUM_CURRENT_SETTINGS,
&mut dev_mode,
);
if ret == FALSE {
log::warn!("no display mode");
}
let pos = dev_mode.Anonymous1.Anonymous2.dmPosition;
let (x, y) = (pos.x, pos.y);
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
display_rects.push(RECT {
left: x,
right: x + width as i32,
top: y,
bottom: y + height as i32,
});
}
display_rects
}
}
static mut DISPLAY_RESOLUTION_CHANGED: bool = true;
unsafe fn get_display_regions() -> &'static Vec<RECT> {
static mut DISPLAYS: Vec<RECT> = vec![];
if DISPLAY_RESOLUTION_CHANGED {
DISPLAYS = enumerate_displays();
DISPLAY_RESOLUTION_CHANGED = false;
log::debug!("displays: {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> {
unsafe {
let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0);
match ret.0 {
0 => None,
x if x > 0 => Some(msg),
_ => panic!("error in GetMessageW"),
}
}
}
fn message_thread(ready_tx: mpsc::Sender<()>) {
unsafe {
set_event_tid(GetCurrentThreadId());
ready_tx.send(()).expect("channel closed");
let mouse_proc: HOOKPROC = Some(mouse_proc);
let kybrd_proc: HOOKPROC = Some(kybrd_proc);
let window_proc: WNDPROC = Some(window_proc);
/* register hooks */
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
let instance = GetModuleHandleW(None).unwrap();
let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc,
hInstance: instance.into(),
lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default()
};
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
/* window is used ro receive WM_DISPLAYCHANGE messages */
let ret = CreateWindowExW(
Default::default(),
w!("lan-mouse-message-window-class"),
w!("lan-mouse-msg-window"),
WINDOW_STYLE::default(),
0,
0,
0,
0,
HWND::default(),
HMENU::default(),
instance,
None,
);
if ret.0 == 0 {
panic!("CreateWindowExW");
}
/* run message loop */
loop {
// mouse / keybrd proc do not actually return a message
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0 == 0 {
/* messages sent via PostThreadMessage */
match msg.wParam.0 {
x if x == EventType::Exit as usize => break,
x if x == EventType::Release as usize => {
ACTIVE_CLIENT.take();
}
x if x == EventType::ClientEvent as usize => {
while let Some(event) = EVENT_BUFFER.pop() {
update_clients(event)
}
}
_ => {}
}
} else {
/* other messages for window_procs */
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
}
fn update_clients(client_event: ClientEvent) {
match client_event {
ClientEvent::Create(handle, pos) => {
unsafe { CLIENT_FOR_POS.insert(pos, handle) };
}
ClientEvent::Destroy(handle) => unsafe {
for pos in [
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
] {
if ACTIVE_CLIENT == Some(handle) {
ACTIVE_CLIENT.take();
}
if CLIENT_FOR_POS.get(&pos).copied() == Some(handle) {
CLIENT_FOR_POS.remove(&pos);
}
}
},
}
}
impl WindowsInputCapture {
pub(crate) fn new() -> Result<Self> {
Err(anyhow!("not implemented"))
unsafe {
let (tx, rx) = channel(10);
EVENT_TX.replace(tx);
let (ready_tx, ready_rx) = mpsc::channel();
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
/* wait for thread to set its id */
ready_rx.recv().expect("channel closed");
Ok(Self {
msg_thread,
event_rx: rx,
})
}
}
}
impl Stream for WindowsInputCapture {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None),
Some(e) => Poll::Ready(Some(Ok(e))),
}
}
}
impl Drop for WindowsInputCapture {
fn drop(&mut self) {
unsafe { signal_message_thread(EventType::Exit) };
let _ = self.msg_thread.take().unwrap().join();
}
}

42
src/capture_test.rs Normal file
View File

@@ -0,0 +1,42 @@
use crate::capture;
use crate::client::{ClientEvent, Position};
use crate::event::{Event, KeyboardEvent};
use anyhow::{anyhow, Result};
use futures::StreamExt;
use tokio::task::LocalSet;
pub fn run() -> Result<()> {
log::info!("running input capture test");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(input_capture_test()))
}
async fn input_capture_test() -> Result<()> {
log::info!("creating input capture");
let mut input_capture = capture::create().await;
log::info!("creating clients");
input_capture.notify(ClientEvent::Create(0, Position::Left))?;
input_capture.notify(ClientEvent::Create(1, Position::Right))?;
input_capture.notify(ClientEvent::Create(2, Position::Top))?;
input_capture.notify(ClientEvent::Create(3, Position::Bottom))?;
loop {
let (client, event) = input_capture
.next()
.await
.ok_or(anyhow!("capture stream closed"))??;
let pos = match client {
0 => Position::Left,
1 => Position::Right,
2 => Position::Top,
_ => Position::Bottom,
};
log::info!("position: {pos}, event: {event}");
if let Event::Keyboard(KeyboardEvent::Key { key: 1, .. }) = event {
input_capture.release()?;
}
}
}

View File

@@ -1,10 +1,15 @@
use std::{
collections::HashSet,
error::Error,
fmt::Display,
net::{IpAddr, SocketAddr},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use slab::Slab;
use crate::config::DEFAULT_PORT;
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
@@ -20,6 +25,33 @@ impl Default for Position {
}
}
#[derive(Debug)]
pub struct PositionParseError {
string: String,
}
impl Display for PositionParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not a valid position: {}", self.string)
}
}
impl Error for PositionParseError {}
impl FromStr for Position {
type Err = PositionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
"top" => Ok(Self::Top),
"bottom" => Ok(Self::Bottom),
_ => Err(PositionParseError { string: s.into() }),
}
}
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
@@ -61,37 +93,38 @@ impl TryFrom<&str> for Position {
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client {
pub struct ClientConfig {
/// hostname of this client
pub hostname: Option<String>,
/// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>,
/// unique handle to refer to the client.
/// This way any emulation / capture backend does not
/// need to know anything about a client other than its handle.
pub handle: ClientHandle,
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub ips: HashSet<IpAddr>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16,
/// position of a client on screen
pub pos: Position,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
port: DEFAULT_PORT,
hostname: Default::default(),
fix_ips: Default::default(),
pos: Default::default(),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
}
pub type ClientHandle = u32;
pub type ClientHandle = u64;
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ClientState {
/// information about the client
pub client: Client,
/// events should be sent to and received from the client
pub active: bool,
/// `active` address of the client, used to send data to.
@@ -100,12 +133,18 @@ pub struct ClientState {
pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings
pub alive: bool,
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub ips: HashSet<IpAddr>,
/// keys currently pressed by this client
pub pressed_keys: HashSet<u32>,
/// dns resolving in progress
pub resolving: bool,
}
pub struct ClientManager {
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
clients: Slab<(ClientConfig, ClientState)>,
}
impl Default for ClientManager {
@@ -116,50 +155,15 @@ impl Default for ClientManager {
impl ClientManager {
pub fn new() -> Self {
Self { clients: vec![] }
let clients = Slab::new();
Self { clients }
}
/// add a new client to this manager
pub fn add_client(
&mut self,
hostname: Option<String>,
ips: HashSet<IpAddr>,
port: u16,
pos: Position,
active: bool,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
// store the client
let client = Client {
hostname,
fix_ips,
handle,
ips,
port,
pos,
};
// client was never seen, nor pinged
let client_state = ClientState {
client,
active,
active_addr: None,
alive: false,
pressed_keys: HashSet::new(),
};
if handle as usize >= self.clients.len() {
assert_eq!(handle as usize, self.clients.len());
self.clients.push(Some(client_state));
} else {
self.clients[handle as usize] = Some(client_state);
}
handle
pub fn add_client(&mut self) -> ClientHandle {
let client_config = Default::default();
let client_state = Default::default();
self.clients.insert((client_config, client_state)) as ClientHandle
}
/// find a client by its address
@@ -168,49 +172,54 @@ impl ClientManager {
// time this is likely faster than using a HashMap
self.clients
.iter()
.position(|c| {
if let Some(c) = c {
c.active && c.client.ips.contains(&addr.ip())
.find_map(|(k, (_, s))| {
if s.active && s.ips.contains(&addr.ip()) {
Some(k)
} else {
false
None
}
})
.map(|p| p as ClientHandle)
}
pub fn find_client(&self, pos: Position) -> Option<ClientHandle> {
self.clients
.iter()
.find_map(|(k, (c, s))| {
if s.active && c.pos == pos {
Some(k)
} else {
None
}
})
.map(|p| p as ClientHandle)
}
/// remove a client from the list
pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientState> {
pub fn remove_client(&mut self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
// remove id from occupied ids
self.clients.get_mut(client as usize)?.take()
}
/// get a free slot in the client list
fn free_id(&mut self) -> ClientHandle {
for i in 0..u32::MAX {
if self.clients.get(i as usize).is_none()
|| self.clients.get(i as usize).unwrap().is_none()
{
return i;
}
}
panic!("Out of client ids");
self.clients.try_remove(client as usize)
}
// returns an immutable reference to the client state corresponding to `client`
pub fn get(&self, client: ClientHandle) -> Option<&ClientState> {
self.clients.get(client as usize)?.as_ref()
pub fn get(&self, handle: ClientHandle) -> Option<&(ClientConfig, ClientState)> {
self.clients.get(handle as usize)
}
/// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> {
self.clients.get_mut(client as usize)?.as_mut()
pub fn get_mut(&mut self, handle: ClientHandle) -> Option<&mut (ClientConfig, ClientState)> {
self.clients.get_mut(handle as usize)
}
pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> {
self.clients.iter().filter_map(|x| x.as_ref())
pub fn get_client_states(
&self,
) -> impl Iterator<Item = (ClientHandle, &(ClientConfig, ClientState))> {
self.clients.iter().map(|(k, v)| (k as ClientHandle, v))
}
pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> {
self.clients.iter_mut().filter_map(|x| x.as_mut())
pub fn get_client_states_mut(
&mut self,
) -> impl Iterator<Item = (ClientHandle, &mut (ClientConfig, ClientState))> {
self.clients.iter_mut().map(|(k, v)| (k as ClientHandle, v))
}
}

View File

@@ -59,6 +59,14 @@ struct CliArgs {
/// run only the service as a daemon without the frontend
#[arg(short, long)]
daemon: bool,
/// test input capture
#[arg(long)]
test_capture: bool,
/// test input emulation
#[arg(long)]
test_emulation: bool,
}
#[derive(Debug, PartialEq, Eq)]
@@ -74,6 +82,8 @@ pub struct Config {
pub clients: Vec<(TomlClient, Position)>,
pub daemon: bool,
pub release_bind: Vec<scancode::Linux>,
pub test_capture: bool,
pub test_emulation: bool,
}
pub struct ConfigClient {
@@ -169,6 +179,8 @@ impl Config {
}
let daemon = args.daemon;
let test_capture = args.test_capture;
let test_emulation = args.test_emulation;
Ok(Config {
daemon,
@@ -176,6 +188,8 @@ impl Config {
clients,
port,
release_bind,
test_capture,
test_emulation,
})
}

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use std::{error::Error, net::IpAddr};
use trust_dns_resolver::TokioAsyncResolver;
use hickory_resolver::TokioAsyncResolver;
pub struct DnsResolver {
resolver: TokioAsyncResolver,

View File

@@ -163,6 +163,18 @@ impl InputEmulation for LibeiEmulation {
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::AxisDiscrete120 { axis, value } => {
if !self.has_scroll {
return;
}
if let Some((d, s)) = self.scroll.as_mut() {
match axis {
0 => s.scroll_discrete(0, value),
_ => s.scroll_discrete(value, 0),
}
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Frame {} => {}
},
Event::Keyboard(k) => match k {

View File

@@ -220,7 +220,7 @@ impl InputEmulation for MacOSEmulation {
axis,
value,
} => {
let value = value as i32 / 10; // FIXME: high precision scroll events
let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
@@ -231,7 +231,32 @@ impl InputEmulation for MacOSEmulation {
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::LINE,
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return;
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,

View File

@@ -8,14 +8,18 @@ use async_trait::async_trait;
use std::ops::BitOrAssign;
use std::time::Duration;
use tokio::task::AbortHandle;
use windows::Win32::UI::Input::KeyboardAndMouse::{SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY};
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_WHEEL, MOUSEINPUT,
};
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use crate::event::{BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
@@ -55,7 +59,8 @@ impl InputEmulation for WindowsEmulation {
time: _,
axis,
value,
} => scroll(axis, value),
} => scroll(axis, value as i32),
PointerEvent::AxisDiscrete120 { axis, value } => scroll(axis, value),
PointerEvent::Frame {} => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
@@ -145,23 +150,32 @@ fn rel_mouse(dx: i32, dy: i32) {
fn mouse_button(button: u32, state: u32) {
let dw_flags = match state {
0 => match button {
0x110 => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP,
BTN_LEFT => MOUSEEVENTF_LEFTUP,
BTN_RIGHT => MOUSEEVENTF_RIGHTUP,
BTN_MIDDLE => MOUSEEVENTF_MIDDLEUP,
BTN_BACK => MOUSEEVENTF_XUP,
BTN_FORWARD => MOUSEEVENTF_XUP,
_ => return,
},
1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN,
BTN_LEFT => MOUSEEVENTF_LEFTDOWN,
BTN_RIGHT => MOUSEEVENTF_RIGHTDOWN,
BTN_MIDDLE => MOUSEEVENTF_MIDDLEDOWN,
BTN_BACK => MOUSEEVENTF_XDOWN,
BTN_FORWARD => MOUSEEVENTF_XDOWN,
_ => return,
},
_ => return,
};
let mouse_data = match button {
BTN_BACK => XBUTTON1 as u32,
BTN_FORWARD => XBUTTON2 as u32,
_ => 0,
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0, // no movement
mouseData: 0,
mouseData: mouse_data,
dwFlags: dw_flags,
time: 0,
dwExtraInfo: 0,
@@ -169,7 +183,7 @@ fn mouse_button(button: u32, state: u32) {
send_mouse_input(mi);
}
fn scroll(axis: u8, value: f64) {
fn scroll(axis: u8, value: i32) {
let event_type = match axis {
0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL,
@@ -178,7 +192,7 @@ fn scroll(axis: u8, value: f64) {
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: (-value * 15.0) as i32 as u32,
mouseData: -value as u32,
dwFlags: event_type,
time: 0,
dwExtraInfo: 0,

View File

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

View File

@@ -125,6 +125,9 @@ impl InputEmulation for X11Emulation {
} => {
self.emulate_scroll(axis, value);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
self.emulate_scroll(axis, value as f64);
}
PointerEvent::Frame {} => {}
},
Event::Keyboard(KeyboardEvent::Key {

View File

@@ -62,59 +62,73 @@ impl<'a> DesktopPortalEmulation<'a> {
impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event {
Pointer(p) => {
match p {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if let Err(e) = self
.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await
{
log::warn!("{e}");
}
Pointer(p) => match p {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if let Err(e) = self
.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await
{
log::warn!("{e}");
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
// TODO smooth scrolling
if let Err(e) = self
.proxy
.notify_pointer_axis_discrete(&self.session, axis, value as i32)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Frame {} => {}
}
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await
{
log::warn!("{e}");
}
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
if let Err(e) = self
.proxy
.notify_pointer_axis_discrete(&self.session, axis, value)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
let (dx, dy) = match axis {
Axis::Vertical => (0., value),
Axis::Horizontal => (value, 0.),
};
if let Err(e) = self
.proxy
.notify_pointer_axis(&self.session, dx, dy, true)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Frame {} => {}
},
Keyboard(k) => {
match k {
KeyboardEvent::Key {

48
src/emulation_test.rs Normal file
View File

@@ -0,0 +1,48 @@
use crate::client::{ClientEvent, Position};
use crate::emulate;
use crate::event::{Event, PointerEvent};
use anyhow::Result;
use std::f64::consts::PI;
use std::time::{Duration, Instant};
use tokio::task::LocalSet;
pub fn run() -> Result<()> {
log::info!("running input emulation test");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(input_emulation_test()))
}
const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0;
async fn input_emulation_test() -> Result<()> {
let mut emulation = emulate::create().await;
emulation
.notify(ClientEvent::Create(0, Position::Left))
.await;
let start = Instant::now();
let mut offset = (0, 0);
loop {
tokio::select! {
_ = emulation.dispatch() => {}
_ = tokio::time::sleep(Duration::from_millis(1)) => {
let elapsed = start.elapsed();
let elapsed_sec_f64 = elapsed.as_secs_f64();
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
let new_offset_f = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
let new_offset = (new_offset_f.0 as i32, new_offset_f.1 as i32);
if new_offset != offset {
let relative_motion = (new_offset.0 - offset.0, new_offset.1 - offset.1);
offset = new_offset;
let (relative_x, relative_y) = (relative_motion.0 as f64, relative_motion.1 as f64);
emulation.consume(Event::Pointer(PointerEvent::Motion {time: 0, relative_x, relative_y }), 0).await;
}
}
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::scancode;
use anyhow::{anyhow, Result};
use std::{
error::Error,
@@ -28,6 +29,10 @@ pub enum PointerEvent {
axis: u8,
value: f64,
},
AxisDiscrete120 {
axis: u8,
value: i32,
},
Frame {},
}
@@ -83,12 +88,29 @@ impl Display for PointerEvent {
time: _,
button,
state,
} => write!(f, "button({button}, {state})"),
} => {
let str = match *button {
BTN_LEFT => Some("left"),
BTN_RIGHT => Some("right"),
BTN_MIDDLE => Some("middle"),
BTN_FORWARD => Some("forward"),
BTN_BACK => Some("back"),
_ => None,
};
if let Some(button) = str {
write!(f, "button({button}, {state})")
} else {
write!(f, "button({button}, {state}")
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => write!(f, "scroll({axis}, {value})"),
PointerEvent::AxisDiscrete120 { axis, value } => {
write!(f, "scroll-120 ({axis}, {value})")
}
PointerEvent::Frame {} => write!(f, "frame()"),
}
}
@@ -101,7 +123,14 @@ impl Display for KeyboardEvent {
time: _,
key,
state,
} => write!(f, "key({key}, {state})"),
} => {
let scan = scancode::Linux::try_from(*key);
if let Ok(scan) = scan {
write!(f, "key({scan:?}, {state})")
} else {
write!(f, "key({key}, {state})")
}
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
@@ -149,6 +178,7 @@ impl PointerEvent {
Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::Axis,
Self::AxisDiscrete120 { .. } => PointerEventType::AxisDiscrete120,
Self::Frame { .. } => PointerEventType::Frame,
}
}
@@ -167,6 +197,7 @@ enum PointerEventType {
Motion,
Button,
Axis,
AxisDiscrete120,
Frame,
}
enum KeyboardEventType {
@@ -191,6 +222,7 @@ impl TryFrom<u8> for PointerEventType {
x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::AxisDiscrete120 as u8 => Ok(Self::AxisDiscrete120),
x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid pointer event type {}", value),
@@ -291,6 +323,11 @@ impl From<&PointerEvent> for Vec<u8> {
let value = value.to_be_bytes();
[&time[..], &axis[..], &value[..]].concat()
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
[&axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => {
vec![]
}
@@ -399,6 +436,25 @@ impl TryFrom<Vec<u8>> for PointerEvent {
};
Ok(Self::Axis { time, axis, value })
}
PointerEventType::AxisDiscrete120 => {
let axis = match data.get(2) {
Some(d) => *d,
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Byte at index 2".into(),
}));
}
};
let value = match data.get(3..7) {
Some(d) => i32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 3".into(),
}));
}
};
Ok(Self::AxisDiscrete120 { axis, value })
}
PointerEventType::Frame => Ok(Self::Frame {}),
}
}

View File

@@ -1,5 +1,5 @@
use anyhow::{anyhow, Result};
use std::{cmp::min, io::ErrorKind, str, time::Duration};
use std::{cmp::min, io::ErrorKind, net::IpAddr, str, time::Duration};
#[cfg(unix)]
use std::{
@@ -23,7 +23,7 @@ use tokio::net::TcpStream;
use serde::{Deserialize, Serialize};
use crate::{
client::{Client, ClientHandle, Position},
client::{ClientConfig, ClientHandle, ClientState, Position},
config::{Config, Frontend},
};
@@ -84,34 +84,47 @@ pub fn wait_for_service() -> Result<std::net::TcpStream> {
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
/// add a new client
AddClient(Option<String>, u16, Position),
pub enum FrontendRequest {
/// activate/deactivate client
ActivateClient(ClientHandle, bool),
Activate(ClientHandle, bool),
/// add a new client
Create,
/// change the listen port (recreate udp listener)
ChangePort(u16),
/// remove a client
DelClient(ClientHandle),
/// request an enumertaion of all clients
Delete(ClientHandle),
/// request an enumeration of all clients
Enumerate(),
/// resolve dns
ResolveDns(ClientHandle),
/// service shutdown
Shutdown(),
/// update a client (hostname, port, position)
UpdateClient(ClientHandle, Option<String>, u16, Position),
Terminate(),
/// update hostname
UpdateHostname(ClientHandle, Option<String>),
/// update port
UpdatePort(ClientHandle, u16),
/// update position
UpdatePosition(ClientHandle, Position),
/// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendNotify {
NotifyClientActivate(ClientHandle, bool),
NotifyClientCreate(Client),
NotifyClientUpdate(Client),
NotifyClientDelete(ClientHandle),
pub enum FrontendEvent {
/// a client was created
Created(ClientHandle, ClientConfig, ClientState),
/// a client was updated
Updated(ClientHandle, ClientConfig),
/// state changed
StateChange(ClientHandle, ClientState),
/// the client was deleted
Deleted(ClientHandle),
/// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>),
/// Client State, active
Enumerate(Vec<(Client, bool)>),
NotifyError(String),
PortChanged(u16, Option<String>),
/// list of all clients, used for initial state synchronization
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
/// an error occured
Error(String),
}
pub struct FrontendListener {
@@ -217,13 +230,14 @@ impl FrontendListener {
Ok(rx)
}
pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
pub(crate) async fn broadcast_event(&mut self, notify: FrontendEvent) -> Result<()> {
// encode event
let json = serde_json::to_string(&notify).unwrap();
let payload = json.as_bytes();
let len = payload.len().to_be_bytes();
log::debug!("json: {json}, len: {}", payload.len());
log::debug!("broadcasting event to streams: {:?}", self.tx_streams);
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
@@ -255,7 +269,7 @@ impl Drop for FrontendListener {
}
#[cfg(unix)]
pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEvent> {
pub async fn wait_for_request(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendRequest> {
let len = stream.read_u64().await?;
assert!(len <= 256);
let mut buf = [0u8; 256];
@@ -264,7 +278,7 @@ pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEve
}
#[cfg(windows)]
pub async fn read_event(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendEvent> {
pub async fn wait_for_request(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendRequest> {
let len = stream.read_u64().await?;
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;

View File

@@ -1,242 +1,347 @@
use anyhow::{anyhow, Context, Result};
use std::{
io::{ErrorKind, Read, Write},
str::SplitWhitespace,
thread,
use anyhow::{anyhow, Result};
use tokio::{
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
task::LocalSet,
};
use crate::{client::Position, config::DEFAULT_PORT};
#[cfg(windows)]
use tokio::net::tcp::{ReadHalf, WriteHalf};
#[cfg(unix)]
use tokio::net::unix::{ReadHalf, WriteHalf};
use super::{FrontendEvent, FrontendNotify};
use std::io::{self, Write};
use crate::{
client::{ClientConfig, ClientHandle, ClientState},
config::DEFAULT_PORT,
};
use self::command::{Command, CommandType};
use super::{FrontendEvent, FrontendRequest};
mod command;
pub fn run() -> Result<()> {
let Ok(mut tx) = super::wait_for_service() else {
let Ok(stream) = super::wait_for_service() else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
let mut rx = tx.try_clone()?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(async move {
stream.set_nonblocking(true)?;
#[cfg(unix)]
let mut stream = tokio::net::UnixStream::from_std(stream)?;
#[cfg(windows)]
let mut stream = tokio::net::TcpStream::from_std(stream)?;
let (rx, tx) = stream.split();
let reader = thread::Builder::new()
.name("cli-frontend".to_string())
.spawn(move || {
// all further prompts
prompt();
loop {
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(0) => return,
Ok(len) => {
if let Some(events) = parse_cmd(buf, len) {
for event in events.iter() {
let json = serde_json::to_string(&event).unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = tx.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = tx.write(bytes) {
log::error!("error sending message: {e}");
};
if *event == FrontendEvent::Shutdown() {
return;
}
}
// prompt is printed after the server response is received
} else {
prompt();
}
}
Err(e) => {
if e.kind() != ErrorKind::UnexpectedEof {
log::error!("error reading from stdin: {e}");
}
return;
}
}
}
})?;
let _ = thread::Builder::new()
.name("cli-frontend-notify".to_string())
.spawn(move || {
loop {
// read len
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let len = usize::from_be_bytes(len);
// read payload
let mut buf: Vec<u8> = vec![0u8; len];
match rx.read_exact(&mut buf[..len]) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let notify: FrontendNotify = match serde_json::from_slice(&buf) {
Ok(n) => n,
Err(e) => break log::error!("{e}"),
};
match notify {
FrontendNotify::NotifyClientActivate(handle, active) => {
if active {
log::info!("client {handle} activated");
} else {
log::info!("client {handle} deactivated");
}
}
FrontendNotify::NotifyClientCreate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("new client ({handle}): {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientUpdate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("client ({handle}) updated: {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted.");
}
FrontendNotify::NotifyError(e) => {
log::warn!("{e}");
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients.into_iter() {
log::info!(
"client ({}) [{}]: active: {}, associated addresses: [{}]",
client.handle,
client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" },
client
.ips
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ")
);
}
}
FrontendNotify::NotifyPortChange(port, msg) => match msg {
Some(msg) => log::info!("could not change port: {msg}"),
None => log::info!("port changed: {port}"),
},
}
prompt();
}
})?;
match reader.join() {
Ok(_) => {}
Err(e) => {
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
(Some(&s), _) => s,
(_, Some(s)) => s,
_ => "no panic info",
};
log::error!("reader thread paniced: {msg}");
}
}
let mut cli = Cli::new(rx, tx);
cli.run().await
}))?;
Ok(())
}
fn prompt() {
struct Cli<'a> {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
rx: ReadHalf<'a>,
tx: WriteHalf<'a>,
}
impl<'a> Cli<'a> {
fn new(rx: ReadHalf<'a>, tx: WriteHalf<'a>) -> Cli<'a> {
Self {
clients: vec![],
rx,
tx,
}
}
async fn run(&mut self) -> Result<()> {
let stdin = tokio::io::stdin();
let stdin = BufReader::new(stdin);
let mut stdin = stdin.lines();
/* initial state sync */
let request = FrontendRequest::Enumerate();
self.send_request(request).await?;
self.clients = loop {
let event = self.await_event().await?;
if let FrontendEvent::Enumerate(clients) = event {
break clients;
}
};
loop {
prompt()?;
tokio::select! {
line = stdin.next_line() => {
let Some(line) = line? else {
break Ok(());
};
let cmd: Command = match line.parse() {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("{e}");
continue;
}
};
self.execute(cmd).await?;
}
event = self.await_event() => {
let event = event?;
self.handle_event(event);
}
}
}
}
async fn execute(&mut self, cmd: Command) -> Result<()> {
match cmd {
Command::None => {}
Command::Connect(pos, host, port) => {
let request = FrontendRequest::Create;
self.send_request(request).await?;
let handle = loop {
let event = self.await_event().await?;
match event {
FrontendEvent::Created(h, c, s) => {
self.clients.push((h, c, s));
break h;
}
_ => {
self.handle_event(event);
continue;
}
}
};
for request in [
FrontendRequest::UpdateHostname(handle, Some(host.clone())),
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
FrontendRequest::UpdatePosition(handle, pos),
] {
self.send_request(request).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Updated(_, _) = event {
break;
}
}
}
}
Command::Disconnect(id) => {
self.send_request(FrontendRequest::Delete(id)).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Deleted(_) = event {
self.handle_event(event);
break;
}
}
}
Command::Activate(id) => {
self.send_request(FrontendRequest::Activate(id, true))
.await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::StateChange(_, _) = event {
self.handle_event(event);
break;
}
}
}
Command::Deactivate(id) => {
self.send_request(FrontendRequest::Activate(id, false))
.await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::StateChange(_, _) = event {
self.handle_event(event);
break;
}
}
}
Command::List => {
self.send_request(FrontendRequest::Enumerate()).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Enumerate(_) = event {
break;
}
}
}
Command::SetHost(handle, host) => {
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
self.send_request(request).await?;
loop {
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) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.send_request(request).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Updated(_, _) = event {
break;
}
}
}
Command::Help => {
for cmd_type in [
CommandType::List,
CommandType::Connect,
CommandType::Disconnect,
CommandType::Activate,
CommandType::Deactivate,
CommandType::SetHost,
CommandType::SetPort,
] {
eprintln!("{}", cmd_type.usage());
}
}
}
Ok(())
}
fn find_mut(
&mut self,
handle: ClientHandle,
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> {
self.clients.iter_mut().find(|(h, _, _)| *h == handle)
}
fn remove(
&mut self,
handle: ClientHandle,
) -> Option<(ClientHandle, ClientConfig, ClientState)> {
let idx = self.clients.iter().position(|(h, _, _)| *h == handle);
idx.map(|i| self.clients.swap_remove(i))
}
fn handle_event(&mut self, event: FrontendEvent) {
match event {
FrontendEvent::Created(h, c, s) => {
eprint!("client added ({h}): ");
print_config(&c);
eprint!(" ");
print_state(&s);
eprintln!();
self.clients.push((h, c, s));
}
FrontendEvent::Updated(h, c) => {
if let Some((_, config, _)) = self.find_mut(h) {
let old_host = config.hostname.clone().unwrap_or("\"\"".into());
let new_host = c.hostname.clone().unwrap_or("\"\"".into());
if old_host != new_host {
eprintln!(
"client {h}: hostname updated ({} -> {})",
old_host, new_host
);
}
if config.port != c.port {
eprintln!("client {h} changed port: {} -> {}", config.port, c.port);
}
if config.fix_ips != c.fix_ips {
eprintln!("client {h} ips updated: {:?}", c.fix_ips)
}
*config = c;
}
}
FrontendEvent::StateChange(h, s) => {
if let Some((_, _, state)) = self.find_mut(h) {
if state.active ^ s.active {
eprintln!(
"client {h} {}",
if s.active { "activated" } else { "deactivated" }
);
}
*state = s;
}
}
FrontendEvent::Deleted(h) => {
if let Some((h, c, _)) = self.remove(h) {
eprint!("client {h} removed (");
print_config(&c);
eprintln!(")");
}
}
FrontendEvent::PortChanged(p, e) => {
if let Some(e) = e {
eprintln!("failed to change port: {e}");
} else {
eprintln!("changed port to {p}");
}
}
FrontendEvent::Enumerate(clients) => {
self.clients = clients;
self.print_clients();
}
FrontendEvent::Error(e) => {
eprintln!("ERROR: {e}");
}
}
}
fn print_clients(&mut self) {
for (h, c, s) in self.clients.iter() {
eprint!("client {h}: ");
print_config(c);
eprint!(" ");
print_state(s);
eprintln!();
}
}
async fn send_request(&mut self, request: FrontendRequest) -> io::Result<()> {
let json = serde_json::to_string(&request).unwrap();
let bytes = json.as_bytes();
let len = bytes.len();
self.tx.write_u64(len as u64).await?;
self.tx.write_all(bytes).await?;
Ok(())
}
async fn await_event(&mut self) -> Result<FrontendEvent> {
let len = self.rx.read_u64().await?;
let mut buf = vec![0u8; len as usize];
self.rx.read_exact(&mut buf).await?;
let event: FrontendEvent = serde_json::from_slice(&buf)?;
Ok(event)
}
}
fn prompt() -> io::Result<()> {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
std::io::stderr().flush()?;
Ok(())
}
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
if len == 0 {
return Some(vec![FrontendEvent::Shutdown()]);
}
let mut l = s.split_whitespace();
let cmd = l.next()?;
let res = match cmd {
"help" => {
log::info!("list list clients");
log::info!("connect <host> left|right|top|bottom [port] add a new client");
log::info!("disconnect <client> remove a client");
log::info!("activate <client> activate a client");
log::info!("deactivate <client> deactivate a client");
log::info!("exit exit lan-mouse");
log::info!("setport <port> change port");
None
}
"exit" => return Some(vec![FrontendEvent::Shutdown()]),
"list" => return Some(vec![FrontendEvent::Enumerate()]),
"connect" => Some(parse_connect(l)),
"disconnect" => Some(parse_disconnect(l)),
"activate" => Some(parse_activate(l)),
"deactivate" => Some(parse_deactivate(l)),
"setport" => Some(parse_port(l)),
_ => {
log::error!("unknown command: {s}");
None
}
};
match res {
Some(Ok(e)) => Some(e),
Some(Err(e)) => {
log::warn!("{e}");
None
}
_ => None,
}
fn print_config(c: &ClientConfig) {
eprint!(
"{}:{} ({}), ips: {:?}",
c.hostname.clone().unwrap_or("(no hostname)".into()),
c.port,
c.pos,
c.fix_ips
);
}
fn parse_connect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let usage = "usage: connect <host> left|right|top|bottom [port]";
let host = l.next().context(usage)?.to_owned();
let pos = match l.next().context(usage)? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = if let Some(p) = l.next() {
p.parse()?
} else {
DEFAULT_PORT
};
Ok(vec![
FrontendEvent::AddClient(Some(host), port, pos),
FrontendEvent::Enumerate(),
])
}
fn parse_disconnect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::DelClient(client),
FrontendEvent::Enumerate(),
])
}
fn parse_activate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: activate <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::ActivateClient(client, true),
FrontendEvent::Enumerate(),
])
}
fn parse_deactivate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::ActivateClient(client, false),
FrontendEvent::Enumerate(),
])
}
fn parse_port(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let port = l.next().context("usage: setport <port>")?.parse()?;
Ok(vec![FrontendEvent::ChangePort(port)])
fn print_state(s: &ClientState) {
eprint!("active: {}, dns: {:?}", s.active, s.ips);
}

153
src/frontend/cli/command.rs Normal file
View File

@@ -0,0 +1,153 @@
use std::{
fmt::Display,
str::{FromStr, SplitWhitespace},
};
use crate::client::{ClientHandle, Position};
pub(super) enum CommandType {
NoCommand,
Help,
Connect,
Disconnect,
Activate,
Deactivate,
List,
SetHost,
SetPort,
}
#[derive(Debug)]
pub(super) struct InvalidCommand {
cmd: String,
}
impl Display for InvalidCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid command: \"{}\"", self.cmd)
}
}
impl FromStr for CommandType {
type Err = InvalidCommand;
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
match s {
"connect" => Ok(Self::Connect),
"disconnect" => Ok(Self::Disconnect),
"activate" => Ok(Self::Activate),
"deactivate" => Ok(Self::Deactivate),
"list" => Ok(Self::List),
"set-host" => Ok(Self::SetHost),
"set-port" => Ok(Self::SetPort),
"help" => Ok(Self::Help),
_ => Err(InvalidCommand { cmd: s.to_string() }),
}
}
}
#[derive(Debug)]
pub(super) enum Command {
None,
Help,
Connect(Position, String, Option<u16>),
Disconnect(ClientHandle),
Activate(ClientHandle),
Deactivate(ClientHandle),
List,
SetHost(ClientHandle, String),
SetPort(ClientHandle, Option<u16>),
}
impl CommandType {
pub(super) fn usage(&self) -> &'static str {
match self {
CommandType::Help => "help",
CommandType::NoCommand => "",
CommandType::Connect => "connect left|right|top|bottom <host> [<port>]",
CommandType::Disconnect => "disconnect <id>",
CommandType::Activate => "activate <id>",
CommandType::Deactivate => "deactivate <id>",
CommandType::List => "list",
CommandType::SetHost => "set-host <id> <host>",
CommandType::SetPort => "set-port <id> <host>",
}
}
}
pub(super) enum CommandParseError {
Usage(CommandType),
Invalid(InvalidCommand),
}
impl Display for CommandParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Usage(cmd) => write!(f, "usage: {}", cmd.usage()),
Self::Invalid(cmd) => write!(f, "{}", cmd),
}
}
}
impl FromStr for Command {
type Err = CommandParseError;
fn from_str(cmd: &str) -> Result<Self, Self::Err> {
let mut args = cmd.split_whitespace();
let cmd_type: CommandType = match args.next() {
Some(c) => c.parse().map_err(CommandParseError::Invalid),
None => Ok(CommandType::NoCommand),
}?;
match cmd_type {
CommandType::Help => Ok(Command::Help),
CommandType::NoCommand => Ok(Command::None),
CommandType::Connect => parse_connect_cmd(args),
CommandType::Disconnect => parse_disconnect_cmd(args),
CommandType::Activate => parse_activate_cmd(args),
CommandType::Deactivate => parse_deactivate_cmd(args),
CommandType::List => Ok(Command::List),
CommandType::SetHost => parse_set_host(args),
CommandType::SetPort => parse_set_port(args),
}
}
}
fn parse_connect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Connect);
let pos = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let host = args.next().ok_or(USAGE)?.to_string();
let port = args.next().and_then(|p| p.parse().ok());
Ok(Command::Connect(pos, host, port))
}
fn parse_disconnect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Disconnect);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Disconnect(id))
}
fn parse_activate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Activate);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Activate(id))
}
fn parse_deactivate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Deactivate);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Deactivate(id))
}
fn parse_set_host(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetHost);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let host = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::SetHost(id, host))
}
fn parse_set_port(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetPort);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let port = args.next().and_then(|p| p.parse().ok());
Ok(Command::SetPort(id, port))
}

View File

@@ -8,18 +8,17 @@ use std::{
process, str,
};
use crate::frontend::gtk::window::Window;
use crate::frontend::{gtk::window::Window, FrontendRequest};
use adw::Application;
use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider,
IconTheme,
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use super::FrontendNotify;
use super::FrontendEvent;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
@@ -51,23 +50,12 @@ fn gtk_main() -> glib::ExitCode {
.build();
app.connect_startup(|_| load_icons());
app.connect_startup(|_| load_css());
app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![];
app.run_with_args(&args)
}
fn load_css() {
let provider = CssProvider::new();
provider.load_from_resource("de/feschber/LanMouse/style.css");
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn load_icons() {
let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display);
@@ -125,38 +113,39 @@ fn build_ui(app: &Application) {
}
});
let window = Window::new(app);
window.imp().stream.borrow_mut().replace(tx);
let window = Window::new(app, tx);
window.request(FrontendRequest::Enumerate());
glib::spawn_future_local(clone!(@weak window => async move {
loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify {
FrontendNotify::NotifyClientActivate(handle, active) => {
window.activate_client(handle, active);
}
FrontendNotify::NotifyClientCreate(client) => {
window.new_client(client, false);
FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state);
},
FrontendNotify::NotifyClientUpdate(client) => {
window.update_client(client);
}
FrontendNotify::NotifyError(e) => {
window.show_toast(e.as_str());
},
FrontendNotify::NotifyClientDelete(client) => {
FrontendEvent::Deleted(client) => {
window.delete_client(client);
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients {
if window.client_idx(client.handle).is_some() {
window.activate_client(client.handle, active);
window.update_client(client);
FrontendEvent::Updated(handle, client) => {
window.update_client_config(handle, client);
}
FrontendEvent::StateChange(handle, state) => {
window.update_client_state(handle, state);
}
FrontendEvent::Error(e) => {
window.show_toast(e.as_str());
},
FrontendEvent::Enumerate(clients) => {
for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else {
window.new_client(client, active);
window.new_client(handle, client, state);
}
}
},
FrontendNotify::NotifyPortChange(port, msg) => {
FrontendEvent::PortChanged(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),

View File

@@ -3,20 +3,29 @@ mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::client::{Client, ClientHandle};
use crate::client::{ClientConfig, ClientHandle, ClientState};
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(client: Client, active: bool) -> Self {
pub fn new(handle: ClientHandle, client: ClientConfig, state: ClientState) -> Self {
Object::builder()
.property("handle", client.handle)
.property("handle", handle)
.property("hostname", client.hostname)
.property("port", client.port as u32)
.property("position", client.pos.to_string())
.property("active", active)
.property("active", state.active)
.property(
"ips",
state
.ips
.iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>(),
)
.property("resolving", state.resolving)
.build()
}
@@ -32,4 +41,6 @@ pub struct ClientData {
pub port: u32,
pub active: bool,
pub position: String,
pub resolving: bool,
pub ips: Vec<String>,
}

View File

@@ -17,6 +17,8 @@ pub struct ClientObject {
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)]
#[property(name = "position", get, set, type = String, member = position)]
#[property(name = "resolving", get, set, type = bool, member = resolving)]
#[property(name = "ips", get, set, type = Vec<String>, member = ips)]
pub data: RefCell<ClientData>,
}

View File

@@ -109,6 +109,27 @@ impl ClientRow {
.sync_create()
.build();
let resolve_binding = client_object
.bind_property(
"resolving",
&self.imp().dns_loading_indicator.get(),
"spinning",
)
.sync_create()
.build();
let ip_binding = client_object
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
.transform_to(|_, ips: Vec<String>| {
if ips.is_empty() {
Some("no ip addresses associated with this client".into())
} else {
Some(ips.join("\n"))
}
})
.sync_create()
.build();
bindings.push(active_binding);
bindings.push(switch_position_binding);
bindings.push(hostname_binding);
@@ -116,6 +137,8 @@ impl ClientRow {
bindings.push(port_binding);
bindings.push(subtitle_binding);
bindings.push(position_binding);
bindings.push(resolve_binding);
bindings.push(ip_binding);
}
pub fn unbind(&self) {

View File

@@ -14,6 +14,8 @@ pub struct ClientRow {
#[template_child]
pub enable_switch: TemplateChild<gtk::Switch>,
#[template_child]
pub dns_button: TemplateChild<gtk::Button>,
#[template_child]
pub hostname: TemplateChild<gtk::Entry>,
#[template_child]
pub port: TemplateChild<gtk::Entry>,
@@ -23,6 +25,8 @@ pub struct ClientRow {
pub delete_row: TemplateChild<ActionRow>,
#[template_child]
pub delete_button: TemplateChild<gtk::Button>,
#[template_child]
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
pub bindings: RefCell<Vec<Binding>>,
}
@@ -58,6 +62,7 @@ impl ObjectImpl for ClientRow {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("request-dns").build(),
Signal::builder("request-update")
.param_types([bool::static_type()])
.build(),
@@ -76,6 +81,11 @@ impl ClientRow {
true // dont run default handler
}
#[template_callback]
fn handle_request_dns(&self, _: Button) {
self.obj().emit_by_name::<()>("request-dns", &[]);
}
#[template_callback]
fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete");

View File

@@ -2,19 +2,25 @@ mod imp;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[cfg(windows)]
use std::net::TcpStream;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{
gio,
glib::{self, closure_local},
NoSelection,
ListBox, NoSelection,
};
use crate::{
client::{Client, ClientHandle, Position},
client::{ClientConfig, ClientHandle, ClientState, Position},
config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent},
frontend::{gtk::client_object::ClientObject, FrontendRequest},
};
use super::client_row::ClientRow;
@@ -27,8 +33,14 @@ glib::wrapper! {
}
impl Window {
pub(crate) fn new(app: &adw::Application) -> Self {
Object::builder().property("application", app).build()
pub(crate) fn new(
app: &adw::Application,
#[cfg(unix)] tx: UnixStream,
#[cfg(windows)] tx: TcpStream,
) -> Self {
let window: Self = Object::builder().property("application", app).build();
window.imp().stream.borrow_mut().replace(tx);
window
}
pub fn clients(&self) -> gio::ListStore {
@@ -55,12 +67,23 @@ impl Window {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client, active);
window.request_client_update(client);
window.request_client_activate(client, active)
}));
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
let index = row.index() as u32;
window.request_client_delete(index);
}));
row.connect_closure("request-dns", false, closure_local!(@strong window => move
|row: ClientRow| {
let index = row.index() as u32;
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client);
window.request_dns(index);
}));
row.upcast()
})
);
@@ -87,10 +110,11 @@ impl Window {
row
}
pub fn new_client(&self, client: Client, active: bool) {
let client = ClientObject::new(client, active);
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client);
self.set_placeholder_visible(false);
self.update_dns_state(handle, !state.ips.is_empty());
}
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
@@ -115,9 +139,9 @@ impl Window {
}
}
pub fn update_client(&self, client: Client) {
let Some(idx) = self.client_idx(client.handle) else {
log::warn!("could not find client with handle {}", client.handle);
pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
@@ -137,22 +161,59 @@ impl Window {
}
}
pub fn activate_client(&self, handle: ClientHandle, active: bool) {
pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
log::warn!("could not find client with handle {}", handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
if data.active != active {
client_object.set_active(active);
log::debug!("set active to {active}");
if state.active != data.active {
client_object.set_active(state.active);
log::debug!("set active to {}", state.active);
}
if state.resolving != data.resolving {
client_object.set_resolving(state.resolving);
log::debug!("resolving {}: {}", data.handle, state.active);
}
self.update_dns_state(handle, !state.ips.is_empty());
let ips = state
.ips
.into_iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>();
client_object.set_ips(ips);
}
pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let list_box: ListBox = self.imp().client_list.get();
let row = list_box.row_at_index(idx as i32).unwrap();
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
if resolved {
client_row.imp().dns_button.set_css_classes(&["success"])
} else {
client_row.imp().dns_button.set_css_classes(&["warning"])
}
}
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 = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
let event = FrontendRequest::Create;
self.imp().set_port(DEFAULT_PORT);
self.request(event);
}
@@ -160,30 +221,34 @@ impl Window {
pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = port.as_str().parse::<u16>() {
self.request(FrontendEvent::ChangePort(port));
self.request(FrontendRequest::ChangePort(port));
} else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));
self.request(FrontendRequest::ChangePort(DEFAULT_PORT));
}
}
pub fn request_client_update(&self, client: &ClientObject, active: bool) {
pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle();
let data = client.get_data();
let position = match Position::try_from(data.position.as_str()) {
Ok(pos) => pos,
_ => {
log::error!("invalid position: {}", data.position);
return;
}
};
let position = Position::try_from(data.position.as_str()).expect("invalid position");
let hostname = data.hostname;
let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
log::debug!("requesting update: {event:?}");
self.request(event);
for event in [
FrontendRequest::UpdateHostname(handle, hostname),
FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port),
] {
log::debug!("requesting: {event:?}");
self.request(event);
}
}
let event = FrontendEvent::ActivateClient(client.handle(), active);
log::debug!("requesting activate: {event:?}");
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
let handle = client.handle();
let event = FrontendRequest::Activate(handle, active);
log::debug!("requesting: {event:?}");
self.request(event);
}
@@ -193,12 +258,12 @@ impl Window {
.downcast_ref()
.expect("Expected object of type `ClientObject`.");
let handle = client_object.handle();
let event = FrontendEvent::DelClient(handle);
let event = FrontendRequest::Delete(handle);
self.request(event);
}
}
fn request(&self, event: FrontendEvent) {
pub fn request(&self, event: FrontendRequest) {
let json = serde_json::to_string(&event).unwrap();
log::debug!("requesting {json}");
let mut stream = self.imp().stream.borrow_mut();

View File

@@ -8,7 +8,8 @@ use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ToastOverlay};
use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
use gtk::glib::clone;
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
use crate::config::DEFAULT_PORT;
@@ -26,6 +27,8 @@ pub struct Window {
#[template_child]
pub port_entry: TemplateChild<Entry>,
#[template_child]
pub hostname_label: TemplateChild<Label>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>,
#[cfg(unix)]
@@ -61,6 +64,22 @@ impl Window {
self.obj().request_client_create();
}
#[template_callback]
fn handle_copy_hostname(&self, button: &Button) {
if let Ok(hostname) = hostname::get() {
let display = gdk::Display::default().unwrap();
let clipboard = display.clipboard();
clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
button.set_icon_name("emblem-ok-symbolic");
button.set_css_classes(&["success"]);
glib::spawn_future_local(clone!(@weak button => async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
}));
}
}
#[template_callback]
fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true);
@@ -95,6 +114,10 @@ impl Window {
impl ObjectImpl for Window {
fn constructed(&self) {
if let Ok(hostname) = hostname::get() {
self.hostname_label
.set_text(hostname.to_str().expect("hostname: invalid utf8"));
}
self.parent_constructed();
self.set_port(DEFAULT_PORT);
let obj = self.obj();

View File

@@ -7,5 +7,7 @@ pub mod server;
pub mod capture;
pub mod emulate;
pub mod capture_test;
pub mod emulation_test;
pub mod frontend;
pub mod scancode;

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use std::process::{self, Child, Command};
use env_logger::Env;
use lan_mouse::{config::Config, frontend, server::Server};
use lan_mouse::{capture_test, config::Config, emulation_test, frontend, server::Server};
use tokio::task::LocalSet;
@@ -31,7 +31,11 @@ pub fn run() -> Result<()> {
log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind);
if config.daemon {
if config.test_capture {
capture_test::run()?;
} else if config.test_emulation {
emulation_test::run()?;
} else if config.daemon {
// if daemon is specified we run the service
run_service(&config)?;
} else {

View File

@@ -1,10 +1,11 @@
use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize};
/*
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
*/
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum Windows {
Shutdown = 0xE05E,
SystemSleep = 0xE05F,
@@ -167,7 +168,7 @@ pub enum Windows {
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
*/
#[repr(u32)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq, TryFromPrimitive)]
#[allow(dead_code)]
pub enum Linux {
KeyReserved = 0,
@@ -428,18 +429,6 @@ pub enum Linux {
KeyCount = 249,
}
impl TryFrom<u32> for Linux {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value >= Self::KeyCount as u32 {
return Err(());
}
let code: Linux = unsafe { std::mem::transmute(value) };
Ok(code)
}
}
impl TryFrom<Linux> for Windows {
type Error = ();
@@ -698,3 +687,161 @@ impl TryFrom<Linux> for Windows {
}
}
}
impl TryFrom<Windows> for Linux {
type Error = ();
fn try_from(value: Windows) -> Result<Self, Self::Error> {
match value {
Windows::Shutdown => Ok(Self::KeyPower),
Windows::SystemSleep => Ok(Self::KeySleep),
Windows::SystemWakeUp => Ok(Self::KeyWakeup),
Windows::ErrorRollOver => Ok(Self::KeyRo),
Windows::KeyA => Ok(Self::KeyA),
Windows::KeyB => Ok(Self::KeyB),
Windows::KeyC => Ok(Self::KeyC),
Windows::KeyD => Ok(Self::KeyD),
Windows::KeyE => Ok(Self::KeyE),
Windows::KeyF => Ok(Self::KeyF),
Windows::KeyG => Ok(Self::KeyG),
Windows::KeyH => Ok(Self::KeyH),
Windows::KeyI => Ok(Self::KeyI),
Windows::KeyJ => Ok(Self::KeyJ),
Windows::KeyK => Ok(Self::KeyK),
Windows::KeyL => Ok(Self::KeyL),
Windows::KeyM => Ok(Self::KeyM),
Windows::KeyN => Ok(Self::KeyN),
Windows::KeyO => Ok(Self::KeyO),
Windows::KeyP => Ok(Self::KeyP),
Windows::KeyQ => Ok(Self::KeyQ),
Windows::KeyR => Ok(Self::KeyR),
Windows::KeyS => Ok(Self::KeyS),
Windows::KeyT => Ok(Self::KeyT),
Windows::KeyU => Ok(Self::KeyU),
Windows::KeyV => Ok(Self::KeyV),
Windows::KeyW => Ok(Self::KeyW),
Windows::KeyX => Ok(Self::KeyX),
Windows::KeyY => Ok(Self::KeyY),
Windows::KeyZ => Ok(Self::KeyZ),
Windows::Key1 => Ok(Self::Key1),
Windows::Key2 => Ok(Self::Key2),
Windows::Key3 => Ok(Self::Key3),
Windows::Key4 => Ok(Self::Key4),
Windows::Key5 => Ok(Self::Key5),
Windows::Key6 => Ok(Self::Key6),
Windows::Key7 => Ok(Self::Key7),
Windows::Key8 => Ok(Self::Key8),
Windows::Key9 => Ok(Self::Key9),
Windows::Key0 => Ok(Self::Key0),
Windows::KeyEnter => Ok(Self::KeyEnter),
Windows::KeyEsc => Ok(Self::KeyEsc),
Windows::KeyDelete => Ok(Self::KeyBackspace),
Windows::KeyTab => Ok(Self::KeyTab),
Windows::KeySpace => Ok(Self::KeySpace),
Windows::KeyMinus => Ok(Self::KeyMinus),
Windows::KeyEqual => Ok(Self::KeyEqual),
Windows::KeyLeftBrace => Ok(Self::KeyLeftbrace),
Windows::KeyRightBrace => Ok(Self::KeyRightbrace),
Windows::KeyBackslash => Ok(Self::KeyBackslash),
Windows::KeySemiColon => Ok(Self::KeySemicolon),
Windows::KeyApostrophe => Ok(Self::KeyApostrophe),
Windows::KeyGrave => Ok(Self::KeyGrave),
Windows::KeyComma => Ok(Self::KeyComma),
Windows::KeyDot => Ok(Self::KeyDot),
Windows::KeySlash => Ok(Self::KeySlash),
Windows::KeyCapsLock => Ok(Self::KeyCapsLock),
Windows::KeyF1 => Ok(Self::KeyF1),
Windows::KeyF2 => Ok(Self::KeyF2),
Windows::KeyF3 => Ok(Self::KeyF3),
Windows::KeyF4 => Ok(Self::KeyF4),
Windows::KeyF5 => Ok(Self::KeyF5),
Windows::KeyF6 => Ok(Self::KeyF6),
Windows::KeyF7 => Ok(Self::KeyF7),
Windows::KeyF8 => Ok(Self::KeyF8),
Windows::KeyF9 => Ok(Self::KeyF9),
Windows::KeyF10 => Ok(Self::KeyF10),
Windows::KeyF11 => Ok(Self::KeyF11),
Windows::KeyF12 => Ok(Self::KeyF12),
Windows::KeyPrintScreen => Ok(Self::KeySysrq),
Windows::KeyScrollLock => Ok(Self::KeyScrollLock),
Windows::KeyPause => Ok(Self::KeyPause),
Windows::KeyInsert => Ok(Self::KeyInsert),
Windows::KeyHome => Ok(Self::KeyHome),
Windows::KeyPageUp => Ok(Self::KeyPageup),
Windows::KeyDeleteForward => Ok(Self::KeyDelete),
Windows::KeyEnd => Ok(Self::KeyEnd),
Windows::KeyPageDown => Ok(Self::KeyPagedown),
Windows::KeyRight => Ok(Self::KeyRight),
Windows::KeyLeft => Ok(Self::KeyLeft),
Windows::KeyDown => Ok(Self::KeyDown),
Windows::KeyUp => Ok(Self::KeyUp),
Windows::KeypadNumLock => Ok(Self::KeyNumlock),
Windows::KeypadSlash => Ok(Self::KeyKpslash),
Windows::KeypadStar => Ok(Self::KeyKpAsterisk),
Windows::KeypadDash => Ok(Self::KeyKpMinus),
Windows::KeypadPlus => Ok(Self::KeyKpplus),
Windows::KeypadEnter => Ok(Self::KeyKpEnter),
Windows::Keypad1End => Ok(Self::KeyKp1),
Windows::Keypad2DownArrow => Ok(Self::KeyKp2),
Windows::Keypad3PageDn => Ok(Self::KeyKp3),
Windows::Keypad4LeftArrow => Ok(Self::KeyKp4),
Windows::Keypad5 => Ok(Self::KeyKp5),
Windows::Keypad6RightArrow => Ok(Self::KeyKp6),
Windows::Keypad7Home => Ok(Self::KeyKp7),
Windows::Keypad8UpArrow => Ok(Self::KeyKp8),
Windows::Keypad9PageUp => Ok(Self::KeyKp9),
Windows::Keypad0Insert => Ok(Self::KeyKp0),
Windows::KeypadDot => Ok(Self::KeyKpDot),
Windows::KeyNonUSSlashBar => Ok(Self::Key102nd),
Windows::KeyApplication => Ok(Self::KeyMenu),
Windows::KeypadEquals => Ok(Self::KeyKpequal),
Windows::KeyF13 => Ok(Self::KeyF13),
Windows::KeyF14 => Ok(Self::KeyF14),
Windows::KeyF15 => Ok(Self::KeyF15),
Windows::KeyF16 => Ok(Self::KeyF16),
Windows::KeyF17 => Ok(Self::KeyF17),
Windows::KeyF18 => Ok(Self::KeyF18),
Windows::KeyF19 => Ok(Self::KeyF19),
Windows::KeyF20 => Ok(Self::KeyF20),
Windows::KeyF21 => Ok(Self::KeyF21),
Windows::KeyF22 => Ok(Self::KeyF22),
Windows::KeyF23 => Ok(Self::KeyF23),
Windows::KeyF24 => Ok(Self::KeyF24),
Windows::KeypadComma => Ok(Self::KeyKpcomma),
Windows::KeyInternational1 => Ok(Self::KeyHangeul),
Windows::KeyInternational2 => Ok(Self::KeyHanja),
Windows::KeyInternational3 => Ok(Self::KeyYen),
Windows::KeyInternational4 => Err(()),
Windows::KeyInternational5 => Err(()),
Windows::KeyLANG1 => Ok(Self::KeyKatakana),
Windows::KeyLANG2 => Ok(Self::KeyHiragana),
Windows::KeyLANG3 => Ok(Self::KeyHenkan),
Windows::KeyLANG4 => Ok(Self::KeyKatakanahiragana),
Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Windows::KeyLeftShift => Ok(Self::KeyLeftShift),
Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt),
Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta),
Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Windows::KeyRightShift => Ok(Self::KeyRightShift),
Windows::KeyRightAlt => Ok(Self::KeyRightalt),
Windows::KeyRightGUI => Ok(Self::KeyRightmeta),
Windows::KeyScanNextTrack => Ok(Self::KeyNextsong),
Windows::KeyScanPreviousTrack => Ok(Self::KeyPrevioussong),
Windows::KeyStop => Ok(Self::KeyStopcd),
Windows::KeyPlayPause => Ok(Self::KeyPlaypause),
Windows::KeyMute => Ok(Self::KeyMute),
Windows::KeyVolumeUp => Ok(Self::KeyVolumeUp),
Windows::KeyVolumeDown => Ok(Self::KeyVolumeDown),
Windows::ALConsumerControlConfiguration => Err(()),
Windows::ALEmailReader => Ok(Self::KeyMail),
Windows::ALCalculator => Ok(Self::KeyCalc),
Windows::ALLocalMachineBrowser => Ok(Self::KeyFile),
Windows::ACSearch => Ok(Self::KeyWww),
Windows::ACHome => Ok(Self::KeyHomepage),
Windows::ACBack => Ok(Self::KeyBack),
Windows::ACForward => Ok(Self::KeyForward),
Windows::ACStop => Ok(Self::KeyStop),
Windows::ACRefresh => Ok(Self::KeyRefresh),
Windows::ACBookmarks => Ok(Self::KeyBookmarks),
}
}
}

View File

@@ -1,16 +1,16 @@
use log;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
rc::Rc,
};
use tokio::signal;
use crate::{capture, emulate};
use crate::{
client::{ClientHandle, ClientManager},
client::{ClientConfig, ClientHandle, ClientManager, ClientState},
config::Config,
dns,
frontend::{FrontendEvent, FrontendListener},
frontend::{FrontendListener, FrontendRequest},
server::capture_task::CaptureEvent,
};
@@ -50,13 +50,21 @@ impl Server {
let state = Rc::new(Cell::new(State::Receiving));
let port = Rc::new(Cell::new(config.port));
for config_client in config.get_clients() {
client_manager.borrow_mut().add_client(
config_client.hostname,
config_client.ips,
config_client.port,
config_client.pos,
config_client.active,
);
let client = ClientConfig {
hostname: config_client.hostname,
fix_ips: config_client.ips.into_iter().collect(),
port: config_client.port,
pos: config_client.pos,
};
let state = ClientState {
active: config_client.active,
ips: HashSet::from_iter(client.fix_ips.iter().cloned()),
..Default::default()
};
let mut client_manager = client_manager.borrow_mut();
let handle = client_manager.add_client();
let c = client_manager.get_mut(handle).expect("invalid handle");
*c = (client, state);
}
let release_bind = config.release_bind.clone();
Self {
@@ -78,18 +86,16 @@ impl Server {
return anyhow::Ok(());
}
};
let (emulate, capture) = tokio::join!(emulate::create(), capture::create());
let (timer_tx, timer_rx) = tokio::sync::mpsc::channel(1);
let (frontend_notify_tx, frontend_notify_rx) = tokio::sync::mpsc::channel(1);
// udp task
let (mut udp_task, sender_tx, receiver_rx, port_tx) =
network_task::new(self.clone(), frontend_notify_tx).await?;
network_task::new(self.clone(), frontend_notify_tx.clone()).await?;
// input capture
let (mut capture_task, capture_channel) = capture_task::new(
capture,
self.clone(),
sender_tx.clone(),
timer_tx.clone(),
@@ -98,7 +104,6 @@ impl Server {
// input emulation
let (mut emulation_task, emulate_channel) = emulation_task::new(
emulate,
self.clone(),
receiver_rx,
sender_tx.clone(),
@@ -108,7 +113,8 @@ impl Server {
// create dns resolver
let resolver = dns::DnsResolver::new().await?;
let (mut resolver_task, resolve_tx) = resolver_task::new(resolver, self.clone());
let (mut resolver_task, resolve_tx) =
resolver_task::new(resolver, self.clone(), frontend_notify_tx);
// frontend listener
let (mut frontend_task, frontend_tx) = frontend_task::new(
@@ -134,9 +140,9 @@ impl Server {
.client_manager
.borrow()
.get_client_states()
.filter_map(|s| {
.filter_map(|(h, (c, s))| {
if s.active {
Some((s.client.handle, s.client.hostname.clone()))
Some((h, c.hostname.clone()))
} else {
None
}
@@ -144,7 +150,7 @@ impl Server {
.collect::<Vec<_>>();
for (handle, hostname) in active {
frontend_tx
.send(FrontendEvent::ActivateClient(handle, true))
.send(FrontendRequest::Activate(handle, true))
.await?;
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
@@ -178,7 +184,7 @@ impl Server {
let _ = emulate_channel.send(EmulationEvent::Terminate).await;
let _ = capture_channel.send(CaptureEvent::Terminate).await;
let _ = frontend_tx.send(FrontendEvent::Shutdown()).await;
let _ = frontend_tx.send(FrontendRequest::Terminate()).await;
if !capture_task.is_finished() {
if let Err(e) = capture_task.await {

View File

@@ -5,7 +5,7 @@ use std::{collections::HashSet, net::SocketAddr};
use tokio::{sync::mpsc::Sender, task::JoinHandle};
use crate::{
capture::InputCapture,
capture::{self, InputCapture},
client::{ClientEvent, ClientHandle},
event::{Event, KeyboardEvent},
scancode,
@@ -25,7 +25,6 @@ pub enum CaptureEvent {
}
pub fn new(
mut capture: Box<dyn InputCapture>,
server: Server,
sender_tx: Sender<(Event, SocketAddr)>,
timer_tx: Sender<()>,
@@ -33,6 +32,7 @@ pub fn new(
) -> (JoinHandle<Result<()>>, Sender<CaptureEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let task = tokio::task::spawn_local(async move {
let mut capture = capture::create().await;
let mut pressed_keys = HashSet::new();
loop {
tokio::select! {
@@ -84,8 +84,8 @@ async fn handle_capture_event(
pressed_keys: &mut HashSet<scancode::Linux>,
release_bind: &[scancode::Linux],
) -> Result<()> {
let (c, mut e) = event;
log::trace!("({c}) {e:?}");
let (handle, mut e) = event;
log::trace!("({handle}) {e:?}");
if let Event::Keyboard(KeyboardEvent::Key { key, state, .. }) = e {
update_pressed_keys(pressed_keys, key, state);
@@ -107,8 +107,8 @@ async fn handle_capture_event(
// get client state for handle
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(c) {
Some(state) => state,
let client_state = match client_manager.get_mut(handle) {
Some((_, s)) => s,
None => {
// should not happen
log::warn!("unknown client!");
@@ -123,10 +123,8 @@ async fn handle_capture_event(
// we get a leave event
if let Event::Enter() = e {
server.state.replace(State::AwaitingLeave);
server
.active_client
.replace(Some(client_state.client.handle));
log::trace!("Active client => {}", client_state.client.handle);
server.active_client.replace(Some(handle));
log::trace!("Active client => {}", handle);
start_timer = true;
log::trace!("STATE ===> AwaitingLeave");
enter = true;

View File

@@ -8,7 +8,7 @@ use tokio::{
use crate::{
client::{ClientEvent, ClientHandle},
emulate::InputEmulation,
emulate::{self, InputEmulation},
event::{Event, KeyboardEvent},
scancode,
server::State,
@@ -27,7 +27,6 @@ pub enum EmulationEvent {
}
pub fn new(
mut emulate: Box<dyn InputEmulation>,
server: Server,
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
sender_tx: Sender<(Event, SocketAddr)>,
@@ -36,6 +35,7 @@ pub fn new(
) -> (JoinHandle<Result<()>>, Sender<EmulationEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let emulate_task = tokio::task::spawn_local(async move {
let mut emulate = emulate::create().await;
let mut last_ignored = None;
loop {
@@ -65,7 +65,7 @@ pub fn new(
.client_manager
.borrow()
.get_client_states()
.map(|s| s.client.handle)
.map(|(h, _)| h)
.collect::<Vec<_>>();
for client in clients {
release_keys(&server, &mut emulate, client).await;
@@ -108,7 +108,7 @@ async fn handle_udp_rx(
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some(s) => s,
Some((_, s)) => s,
None => {
log::error!("unknown handle");
return;
@@ -156,13 +156,12 @@ async fn handle_udp_rx(
}) = event
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state =
if let Some(client_state) = client_manager.get_mut(handle) {
client_state
} else {
log::error!("unknown handle");
return;
};
let client_state = if let Some((_, s)) = client_manager.get_mut(handle) {
s
} else {
log::error!("unknown handle");
return;
};
if state == 0 {
// ignore release event if key not pressed
ignore_event = !client_state.pressed_keys.remove(&key);
@@ -177,7 +176,7 @@ async fn handle_udp_rx(
if !ignore_event {
// consume event
emulate.consume(event, handle).await;
log::trace!("{event:?} => emulate");
log::trace!("{event} => emulate");
}
}
State::AwaitingLeave => {
@@ -213,7 +212,7 @@ async fn release_keys(
.borrow_mut()
.get_mut(client)
.iter_mut()
.flat_map(|s| s.pressed_keys.drain())
.flat_map(|(_, s)| s.pressed_keys.drain())
.collect::<Vec<_>>();
for key in keys {

View File

@@ -18,7 +18,7 @@ use tokio::{
use crate::{
client::{ClientEvent, ClientHandle, Position},
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
frontend::{self, FrontendEvent, FrontendListener, FrontendRequest},
};
use super::{
@@ -27,13 +27,13 @@ use super::{
pub(crate) fn new(
mut frontend: FrontendListener,
mut notify_rx: Receiver<FrontendNotify>,
mut notify_rx: Receiver<FrontendEvent>,
server: Server,
capture_notify: Sender<CaptureEvent>,
emulate_notify: Sender<EmulationEvent>,
capture: Sender<CaptureEvent>,
emulate: Sender<EmulationEvent>,
resolve_ch: Sender<DnsRequest>,
port_tx: Sender<u16>,
) -> (JoinHandle<Result<()>>, Sender<FrontendEvent>) {
) -> (JoinHandle<Result<()>>, Sender<FrontendRequest>) {
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32);
let event_tx_clone = event_tx.clone();
let frontend_task = tokio::task::spawn_local(async move {
@@ -51,13 +51,13 @@ pub(crate) fn new(
}
event = event_rx.recv() => {
let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?;
if handle_frontend_event(&server, &capture_notify, &emulate_notify, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
if handle_frontend_event(&server, &capture, &emulate, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
break;
}
}
notify = notify_rx.recv() => {
let notify = notify.ok_or(anyhow!("frontend notify closed"))?;
let _ = frontend.notify_all(notify).await;
let _ = frontend.broadcast_event(notify).await;
}
}
}
@@ -67,7 +67,7 @@ pub(crate) fn new(
}
async fn handle_frontend_stream(
frontend_tx: &Sender<FrontendEvent>,
frontend_tx: &Sender<FrontendRequest>,
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
) {
@@ -75,12 +75,11 @@ async fn handle_frontend_stream(
let tx = frontend_tx.clone();
tokio::task::spawn_local(async move {
let _ = tx.send(FrontendEvent::Enumerate()).await;
loop {
let event = frontend::read_event(&mut stream).await;
match event {
Ok(event) => {
let _ = tx.send(event).await;
let request = frontend::wait_for_request(&mut stream).await;
match request {
Ok(request) => {
let _ = tx.send(request).await;
}
Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() {
@@ -98,245 +97,270 @@ async fn handle_frontend_stream(
async fn handle_frontend_event(
server: &Server,
capture_tx: &Sender<CaptureEvent>,
emulate_tx: &Sender<EmulationEvent>,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>,
frontend: &mut FrontendListener,
port_tx: &Sender<u16>,
event: FrontendEvent,
event: FrontendRequest,
) -> bool {
log::debug!("frontend: {event:?}");
let response = match event {
FrontendEvent::AddClient(hostname, port, pos) => {
let handle = add_client(server, resolve_tx, hostname, HashSet::new(), port, pos).await;
let client = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.clone();
Some(FrontendNotify::NotifyClientCreate(client))
match event {
FrontendRequest::Create => {
add_client(server, frontend).await;
}
FrontendEvent::ActivateClient(handle, active) => {
activate_client(server, capture_tx, emulate_tx, handle, active).await;
Some(FrontendNotify::NotifyClientActivate(handle, active))
FrontendRequest::Activate(handle, active) => {
if active {
activate_client(server, frontend, capture, emulate, handle).await;
} else {
deactivate_client(server, frontend, capture, emulate, handle).await;
}
}
FrontendEvent::ChangePort(port) => {
FrontendRequest::ChangePort(port) => {
let _ = port_tx.send(port).await;
None
}
FrontendEvent::DelClient(handle) => {
remove_client(server, capture_tx, emulate_tx, frontend, handle).await;
Some(FrontendNotify::NotifyClientDelete(handle))
FrontendRequest::Delete(handle) => {
remove_client(server, frontend, capture, emulate, handle).await;
}
FrontendEvent::Enumerate() => {
FrontendRequest::Enumerate() => {
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|s| (s.client.clone(), s.active))
.map(|(h, (c, s))| (h, c.clone(), s.clone()))
.collect();
Some(FrontendNotify::Enumerate(clients))
broadcast(frontend, FrontendEvent::Enumerate(clients)).await;
}
FrontendEvent::Shutdown() => {
FrontendRequest::Terminate() => {
log::info!("terminating gracefully...");
return true;
}
FrontendEvent::UpdateClient(handle, hostname, port, pos) => {
update_client(
server,
capture_tx,
emulate_tx,
resolve_tx,
(handle, hostname, port, pos),
)
.await;
let client = server
FrontendRequest::UpdateFixIps(handle, fix_ips) => {
update_fix_ips(server, resolve_tx, handle, fix_ips).await;
broadcast_client_update(server, frontend, handle).await;
}
FrontendRequest::UpdateHostname(handle, hostname) => {
update_hostname(server, resolve_tx, handle, hostname).await;
broadcast_client_update(server, frontend, handle).await;
}
FrontendRequest::UpdatePort(handle, port) => {
update_port(server, handle, port).await;
broadcast_client_update(server, frontend, handle).await;
}
FrontendRequest::UpdatePosition(handle, pos) => {
update_pos(server, handle, capture, emulate, pos).await;
broadcast_client_update(server, frontend, handle).await;
}
FrontendRequest::ResolveDns(handle) => {
let hostname = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.clone();
Some(FrontendNotify::NotifyClientUpdate(client))
.and_then(|(c, _)| c.hostname.clone());
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
};
let Some(response) = response else {
return false;
};
if let Err(e) = frontend.notify_all(response).await {
log::error!("error notifying frontend: {e}");
}
false
}
pub async fn add_client(
server: &Server,
resolver_tx: &Sender<DnsRequest>,
hostname: Option<String>,
addr: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
log::info!(
"adding client [{}]{} @ {:?}",
pos,
hostname.as_deref().unwrap_or(""),
&addr
);
let handle =
server
.client_manager
.borrow_mut()
.add_client(hostname.clone(), addr, port, pos, false);
log::debug!("add_client {handle}");
if let Some(hostname) = hostname {
let _ = resolver_tx.send(DnsRequest { hostname, handle }).await;
async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) {
if let Err(e) = frontend.broadcast_event(event).await {
log::error!("error notifying frontend: {e}");
}
}
handle
pub async fn add_client(server: &Server, frontend: &mut FrontendListener) {
let handle = server.client_manager.borrow_mut().add_client();
log::info!("added client {handle}");
let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone();
broadcast(frontend, FrontendEvent::Created(handle, c, s)).await;
}
pub async fn deactivate_client(
server: &Server,
frontend: &mut FrontendListener,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
let state = match server.client_manager.borrow_mut().get_mut(handle) {
Some((_, s)) => {
s.active = false;
s.clone()
}
None => return,
};
let event = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::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(
server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
client: ClientHandle,
active: bool,
frontend: &mut FrontendListener,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
let (client, pos) = match server.client_manager.borrow_mut().get_mut(client) {
Some(state) => {
state.active = active;
(state.client.handle, state.client.pos)
}
/* deactivate potential other client at this position */
let pos = match server.client_manager.borrow().get(handle) {
Some((client, _)) => client.pos,
None => return,
};
if active {
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Create(client, pos)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Create(
client, pos,
)))
.await;
} else {
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let other = server.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other {
if other != handle {
deactivate_client(server, frontend, capture, emulate, other).await;
}
}
/* activate the client */
let state = if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) {
s.active = true;
s.clone()
} else {
return;
};
/* notify emulation, capture and frontends */
let event = ClientEvent::Create(handle, pos);
let _ = capture.send(CaptureEvent::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(
server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
frontend: &mut FrontendListener,
client: ClientHandle,
) -> Option<ClientHandle> {
let Some((client, active)) = server
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
let Some(active) = server
.client_manager
.borrow_mut()
.remove_client(client)
.map(|s| (s.client.handle, s.active))
.remove_client(handle)
.map(|(_, s)| s.active)
else {
return None;
return;
};
if active {
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let destroy = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
}
let notify = FrontendNotify::NotifyClientDelete(client);
log::debug!("{notify:?}");
if let Err(e) = frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
}
Some(client)
let event = FrontendEvent::Deleted(handle);
broadcast(frontend, event).await;
}
async fn update_client(
async fn update_fix_ips(
server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>,
client_update: (ClientHandle, Option<String>, u16, Position),
handle: ClientHandle,
fix_ips: Vec<IpAddr>,
) {
let (handle, hostname, port, pos) = client_update;
let mut changed = false;
let (hostname, handle, active) = {
// retrieve state
let hostname = {
let mut client_manager = server.client_manager.borrow_mut();
let Some(state) = client_manager.get_mut(handle) else {
let Some((c, _)) = client_manager.get_mut(handle) else {
return;
};
// update pos
if state.client.pos != pos {
state.client.pos = pos;
changed = true;
}
// update port
if state.client.port != port {
state.client.port = port;
state.active_addr = state.active_addr.map(|a| SocketAddr::new(a.ip(), port));
changed = true;
}
// update hostname
if state.client.hostname != hostname {
state.client.ips = HashSet::new();
state.active_addr = None;
state.client.hostname = hostname;
changed = true;
}
log::debug!("client updated: {:?}", state);
(
state.client.hostname.clone(),
state.client.handle,
state.active,
)
c.fix_ips = fix_ips;
c.hostname.clone()
};
// resolve dns if something changed
if changed {
// resolve dns
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
// update state in event input emulator & input capture
if changed && active {
// update state
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(handle)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(handle)))
.await;
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Create(handle, pos)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Create(
handle, pos,
)))
.await;
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
async fn update_hostname(
server: &Server,
resolve_tx: &Sender<DnsRequest>,
handle: ClientHandle,
hostname: Option<String>,
) {
let hostname = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
// update hostname
if c.hostname != hostname {
c.hostname = hostname;
s.ips = HashSet::from_iter(c.fix_ips.iter().cloned());
s.active_addr = None;
c.hostname.clone()
} else {
None
}
};
// resolve to update ips in state
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
async fn update_port(server: &Server, handle: ClientHandle, port: u16) {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
if c.port != port {
c.port = port;
s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
}
async fn update_pos(
server: &Server,
handle: ClientHandle,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
pos: Position,
) {
let (changed, active) = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
let changed = c.pos != pos;
c.pos = pos;
(changed, s.active)
};
// update state in event input emulator & input capture
if changed {
if active {
let destroy = ClientEvent::Destroy(handle);
let _ = capture.send(CaptureEvent::ClientEvent(destroy)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(destroy)).await;
}
let create = ClientEvent::Create(handle, pos);
let _ = capture.send(CaptureEvent::ClientEvent(create)).await;
let _ = emulate.send(EmulationEvent::ClientEvent(create)).await;
}
}
async fn broadcast_client_update(
server: &Server,
frontend: &mut FrontendListener,
handle: ClientHandle,
) {
let (client, _) = server.client_manager.borrow().get(handle).unwrap().clone();
broadcast(frontend, FrontendEvent::Updated(handle, client)).await;
}

View File

@@ -7,13 +7,13 @@ use tokio::{
task::JoinHandle,
};
use crate::{event::Event, frontend::FrontendNotify};
use crate::{event::Event, frontend::FrontendEvent};
use super::Server;
pub async fn new(
server: Server,
frontend_notify_tx: Sender<FrontendNotify>,
frontend_notify_tx: Sender<FrontendEvent>,
) -> Result<(
JoinHandle<()>,
Sender<(Event, SocketAddr)>,
@@ -55,12 +55,12 @@ pub async fn new(
Ok(new_socket) => {
socket = new_socket;
server.port.replace(port);
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
let _ = frontend_notify_tx.send(FrontendEvent::PortChanged(port, None)).await;
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port();
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(
let _ = frontend_notify_tx.send(FrontendEvent::PortChanged(
port,
Some(format!("could not change port: {e}")),
)).await;

View File

@@ -34,8 +34,8 @@ pub fn new(
// if receiving we care about clients with pressed keys
client_manager
.get_client_states_mut()
.filter(|s| !s.pressed_keys.is_empty())
.map(|s| s.client.handle)
.filter(|(_, (_, s))| !s.pressed_keys.is_empty())
.map(|(h, _)| h)
.collect()
} else {
// if sending we care about the active client
@@ -46,17 +46,15 @@ pub fn new(
let ping_addrs: Vec<SocketAddr> = {
ping_clients
.iter()
.flat_map(|&c| client_manager.get(c))
.flat_map(|state| {
if state.alive && state.active_addr.is_some() {
vec![state.active_addr.unwrap()]
.flat_map(|&h| client_manager.get(h))
.flat_map(|(c, s)| {
if s.alive && s.active_addr.is_some() {
vec![s.active_addr.unwrap()]
} else {
state
.client
.ips
s.ips
.iter()
.cloned()
.map(|ip| SocketAddr::new(ip, state.client.port))
.map(|ip| SocketAddr::new(ip, c.port))
.collect()
}
})
@@ -64,8 +62,8 @@ pub fn new(
};
// reset alive
for state in client_manager.get_client_states_mut() {
state.alive = false;
for (_, (_, s)) in client_manager.get_client_states_mut() {
s.alive = false;
}
(ping_clients, ping_addrs)
@@ -102,8 +100,8 @@ pub fn new(
let client_manager = server.client_manager.borrow();
ping_clients
.iter()
.filter_map(|&c| match client_manager.get(c) {
Some(state) if !state.alive => Some(c),
.filter_map(|&h| match client_manager.get(h) {
Some((_, s)) if !s.alive => Some(h),
_ => None,
})
.collect()
@@ -112,9 +110,9 @@ pub fn new(
// we may not be receiving anymore but we should respond
// to the original state and not the "new" one
if receiving {
for c in unresponsive_clients {
for h in unresponsive_clients {
log::warn!("device not responding, releasing keys!");
let _ = emulate_notify.send(EmulationEvent::ReleaseKeys(c)).await;
let _ = emulate_notify.send(EmulationEvent::ReleaseKeys(h)).await;
}
} else {
// release pointer if the active client has not responded

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use tokio::{sync::mpsc::Sender, task::JoinHandle};
use crate::{client::ClientHandle, dns::DnsResolver};
use crate::{client::ClientHandle, dns::DnsResolver, frontend::FrontendEvent};
use super::Server;
@@ -12,7 +12,11 @@ pub struct DnsRequest {
pub handle: ClientHandle,
}
pub fn new(resolver: DnsResolver, server: Server) -> (JoinHandle<()>, Sender<DnsRequest>) {
pub fn new(
resolver: DnsResolver,
mut server: Server,
mut frontend: Sender<FrontendEvent>,
) -> (JoinHandle<()>, Sender<DnsRequest>) {
let (dns_tx, mut dns_rx) = tokio::sync::mpsc::channel::<DnsRequest>(32);
let resolver_task = tokio::task::spawn_local(async move {
loop {
@@ -20,6 +24,13 @@ pub fn new(resolver: DnsResolver, server: Server) -> (JoinHandle<()>, Sender<Dns
Some(r) => (r.hostname, r.handle),
None => break,
};
/* update resolving status */
if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) {
s.resolving = true;
}
notify_state_change(&mut frontend, &mut server, handle).await;
let ips = match resolver.resolve(&host).await {
Ok(ips) => ips,
Err(e) => {
@@ -27,14 +38,35 @@ pub fn new(resolver: DnsResolver, server: Server) -> (JoinHandle<()>, Sender<Dns
continue;
}
};
if let Some(state) = server.client_manager.borrow_mut().get_mut(handle) {
let mut addrs = HashSet::from_iter(state.client.fix_ips.iter().cloned());
/* update ips and resolving state */
if let Some((c, s)) = server.client_manager.borrow_mut().get_mut(handle) {
let mut addrs = HashSet::from_iter(c.fix_ips.iter().cloned());
for ip in ips {
addrs.insert(ip);
}
state.client.ips = addrs;
s.ips = addrs;
s.resolving = false;
}
notify_state_change(&mut frontend, &mut server, handle).await;
}
});
(resolver_task, dns_tx)
}
async fn notify_state_change(
frontend: &mut Sender<FrontendEvent>,
server: &mut Server,
handle: ClientHandle,
) {
let state = server
.client_manager
.borrow_mut()
.get_mut(handle)
.map(|(_, s)| s.clone());
if let Some(state) = state {
let _ = frontend
.send(FrontendEvent::StateChange(handle, state))
.await;
}
}