Compare commits

...

40 Commits

Author SHA1 Message Date
Ferdinand Schober
622b820c7f chore: Release lan-mouse version 0.4.0 2023-12-09 02:13:26 +01:00
Ferdinand Schober
09bf535eec Update README.md 2023-12-09 02:06:42 +01:00
Ferdinand Schober
39acce8e6a Update README.md 2023-12-09 02:03:18 +01:00
Ferdinand Schober
e3f9947284 macos: enable running lan-mouse on macos (#42)
* macos: initial support

- adapted conditional compilation
- moved lan-mouse socket to ~/Library/Caches/lan-mouse-socket.sock instead of XDG_RUNTIME_DIR
- support for mouse input emulation
TODO: Keycode translation, input capture
2023-12-09 01:35:08 +01:00
Ferdinand Schober
5a7e0cf89c formatting 2023-12-09 00:43:54 +01:00
Ferdinand Schober
56e5f7a30d Background service (#43)
better handling of background-service: lan-mouse can now be run without a gui by specifying --daemon as an argument.
Otherwise the servic will be run as a child process and correctly terminate when the window is closed / frontend exits.

Closes #38
2023-12-09 00:36:01 +01:00
Ferdinand Schober
9b242f6138 update README 2023-12-04 16:24:35 +01:00
Ferdinand Schober
b01f7c2793 move server to src/ 2023-12-03 22:37:41 +01:00
Ferdinand Schober
61b23c910b update README 2023-12-03 22:34:11 +01:00
Ferdinand Schober
74eebc07d8 Libei support - input emulation (#33)
Add support for input emulation through libei!
2023-12-03 21:56:01 +01:00
Ferdinand Schober
e6677c3061 Respect XDG_CONFIG_HOME for config.toml location (#41)
* Respect XDG_CONFIG_HOME for config.toml location
* add option to specify config via commandline

closes #39
2023-12-01 11:16:56 +01:00
Ferdinand Schober
e88241e816 port changing functionality (#34)
* port changing functionality

* add portchange to cli frontend
2023-10-17 15:12:17 +02:00
Ferdinand Schober
60a73b3cb0 Update README.md 2023-10-16 11:57:59 +02:00
Ferdinand Schober
cc28827721 Update README.md 2023-10-15 14:43:55 +02:00
Ferdinand Schober
dd1fb29f51 Update README.md (#32) 2023-10-15 14:43:18 +02:00
Ferdinand Schober
be0fe9f2d9 Support event consumer on KDE! (portal backend) (#31)
* Support event consumer on KDE! (portal backend)

Support for KDE event emulation using the remote-desktop xdg-desktop-portal

* fix scrolling (TODO: smooth / kinetic scrolling)

* windows: fix compilation errors

* Update README.md
2023-10-13 13:57:33 +02:00
Ferdinand Schober
4cdc5ea49c windows: fix compilation error 2023-10-12 12:48:39 +02:00
Ferdinand Schober
96ab7d304b wlroots: Fix crash when socket is overwhelmed
Previously when the output buffer was overwhelmed, additional
events were submitted until the outgoing buffer filled up, which
causes the wayland-connection to 'break' and not accept further attempts
to flush() the socket.
2023-10-12 12:40:57 +02:00
Ferdinand Schober
ab2514e508 Async (#30)
- manual eventloop now replaced by asycn-await using the tokio runtime
- dns no longer blocks the event loop
- simplifies logic
- makes xdg-desktop-portal easier to integrate
2023-10-11 14:52:18 +02:00
Ferdinand Schober
d4d6f05802 chore: Release lan-mouse version 0.3.3 2023-10-11 14:32:18 +02:00
Ferdinand Schober
79fa42b74e Update README.md 2023-09-30 16:29:15 +02:00
Ferdinand Schober
851b6d60eb Avoid sending frame events (#29)
* Avoid sending frame events

Frame events are now implicit - each network event implies a frame event
TODO: Accumulate correctly

* remove trace logs from producer
2023-09-28 13:01:38 +02:00
Ferdinand Schober
06725f4b14 Frontend improvement (#27)
* removed redundant dns lookups
* frontend now correctly reflects the state of the backend
* config.toml is loaded when starting gtk frontend
2023-09-25 13:03:17 +02:00
Ferdinand Schober
603646c799 Add LM_DEBUG_LAYER_SHELL environment variable
setting LM_DEBUG_LAYER_SHELL to a value will
make the indicators visible
2023-09-21 18:23:01 +02:00
Ferdinand Schober
b2179e88de adjust window size 2023-09-21 13:59:18 +02:00
Ferdinand Schober
bae52eb9e7 chore: Release lan-mouse version 0.3.2 2023-09-21 13:23:45 +02:00
Ferdinand Schober
0fbd09b07f fix 1px gap 2023-09-21 13:22:23 +02:00
Ferdinand Schober
96dd9c05a1 fix interference with swaybar 2023-09-21 12:57:51 +02:00
Ferdinand Schober
15c02ac505 chore: Release lan-mouse version 0.3.1 2023-09-21 12:37:00 +02:00
Ferdinand Schober
08893a39be fix incorrect orientation of layer surfaces
top and bottom surfaces were not sized & oriented correctly
closes #3
2023-09-21 12:35:27 +02:00
Ferdinand Schober
48b701b726 remove an unused import 2023-09-21 00:13:53 +02:00
Ferdinand Schober
891e21d3e9 read all output globals 2023-09-21 00:12:11 +02:00
Ferdinand Schober
6a5de3f025 Update README.md (#25)
scale image
2023-09-20 15:40:19 +02:00
Ferdinand Schober
1eb12baf15 chore: Release lan-mouse version 0.3.0 2023-09-20 15:31:53 +02:00
Ferdinand Schober
65048abcfc Update README.md (#24) 2023-09-20 15:25:09 +02:00
Ferdinand Schober
d042c0aa4a Libadwaita gui (#19)
Major Update: Functional GUI Frontend!
2023-09-20 15:23:33 +02:00
Ferdinand Schober
c50b746816 fix 2023-09-19 21:13:17 +02:00
Ferdinand Schober
3b09abb532 unlink socket in case it's left over from a crash 2023-09-19 21:11:47 +02:00
Ferdinand Schober
b839097cb2 chore: Release lan-mouse version 0.2.1 2023-09-19 19:47:55 +02:00
Ferdinand Schober
61b22fff51 fix a crash 2023-09-19 19:46:43 +02:00
49 changed files with 5073 additions and 1211 deletions

View File

@@ -18,6 +18,7 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Upload build artifact - name: Upload build artifact
@@ -38,9 +39,23 @@ jobs:
name: lan-mouse-windows name: lan-mouse-windows
path: target/release/lan-mouse.exe path: target/release/lan-mouse.exe
macos-release-build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: cargo build --release
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-macos
path: target/release/lan-mouse
pre-release: pre-release:
name: "Pre Release" name: "Pre Release"
needs: [windows-release-build, linux-release-build] needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
@@ -55,3 +70,4 @@ jobs:
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe lan-mouse-windows/lan-mouse.exe
lan-mouse-macos/lan-mouse

View File

@@ -20,6 +20,7 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
@@ -45,3 +46,19 @@ jobs:
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: target/debug/lan-mouse.exe path: target/debug/lan-mouse.exe
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-macos
path: target/debug/lan-mouse

View File

@@ -14,6 +14,7 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Upload build artifact - name: Upload build artifact
@@ -34,9 +35,23 @@ jobs:
name: lan-mouse-windows name: lan-mouse-windows
path: target/release/lan-mouse.exe path: target/release/lan-mouse.exe
macos-release-build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: cargo build --release
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-macos
path: target/release/lan-mouse
tagged-release: tagged-release:
name: "Tagged Release" name: "Tagged Release"
needs: [windows-release-build, linux-release-build] needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
@@ -49,3 +64,4 @@ jobs:
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe lan-mouse-windows/lan-mouse.exe
lan-mouse-macos/lan-mouse

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target /target
.gdbinit

945
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lan-mouse" name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks" description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.2.0" version = "0.4.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse" repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -13,36 +13,49 @@ strip = true
lto = "fat" lto = "fat"
[dependencies] [dependencies]
tempfile = "3.6" tempfile = "3.8"
trust-dns-resolver = "0.22" trust-dns-resolver = "0.23"
memmap = "0.7" memmap = "0.7"
toml = "0.7" toml = "0.7"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71" anyhow = "1.0.71"
log = "0.4.20" log = "0.4.20"
env_logger = "0.10.0" env_logger = "0.10.0"
mio = { version = "0.8", features = ["os-ext"] }
libc = "0.2.148" libc = "0.2.148"
serde_json = "1.0.107" serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
async-trait = "0.1.73"
futures-core = "0.3.28"
futures = "0.3.28"
clap = { version="4.4.11", features = ["derive"] }
[target.'cfg(unix)'.dependencies] [target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version="0.30.2", optional = true } wayland-client = { version="0.30.2", optional = true }
wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true } wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true } wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true } wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true } wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true }
mio-signals = "0.2.0"
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_8"], optional = true } ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_3"], optional = true } reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true }
[target.'cfg(unix)'.dependencies]
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_6"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] } winapi = { version = "0.3.9", features = ["winuser"] }
[target.'cfg(unix)'.build-dependencies]
glib-build-tools = "0.18.0"
[features] [features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei"] default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"] wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"]
x11 = ["dep:x11"] x11 = ["dep:x11"]
xdg_desktop_portal = [] xdg_desktop_portal = ["dep:ashpd"]
libei = [] libei = ["dep:reis", "dep:ashpd"]
gtk = ["dep:gtk", "dep:adw"] gtk = ["dep:gtk", "dep:adw"]

236
README.md
View File

@@ -1,47 +1,46 @@
# Lan Mouse Share # Lan Mouse
- _Now with a gtk frontend_
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple pcs with a single set of mouse and keyboard.
The primary target is Wayland on Linux but Windows and MacOS have partial support as well (see below for more details).
![Screenshot from 2023-12-09 01-48-12](https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df#gh-dark-mode-only)
![Screenshot from 2023-12-09 01-48-19](https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709#gh-light-mode-only)
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/). Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... . Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
Of course ***blazingly fast™*** and stable, because it's written in rust. ***blazingly fast™*** because it's written in rust.
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap). For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
## Configuration ## OS Support
Configuration is done through the file `config.toml`,
which must be located in the current working directory when
executing lan-mouse.
### Example config The following table shows support for input emulation (to emulate events received from other clients) and
A minimal config file could look like this: input capture (to send events *to* other clients) on different operating systems:
```toml | Backend | input emulation | input capture |
[left] |---------------------------|--------------------------|--------------------------------------|
host_name = "my-laptop" | Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
``` | Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | WIP |
| X11 | (WIP) | WIP |
| Windows | ( :heavy_check_mark: ) | WIP |
| MacOS | ( :heavy_check_mark: ) | WIP |
Where `left` can be either `left`, `right`, `top` or `bottom`. Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
### Additional options
Additionally
- a preferred backend
- a port override for the default port (4242)
can be specified.
Supported backends currently include "wlroots", "x11" and "windows".
These two options can also be specified via the commandline
options `--backend` and `--port` respectively.
## Build and Run ## Build and Run
Build only Build in release mode:
```sh ```sh
cargo build --release cargo build --release
``` ```
Run Run directly:
```sh ```sh
cargo run --release cargo run --release
``` ```
@@ -65,83 +64,77 @@ an executable with just support for wayland:
cargo build --no-default-features --features wayland cargo build --no-default-features --features wayland
``` ```
## OS Support ## Usage
### Gtk Frontend
By default the gtk frontend will open when running `lan-mouse`.
The following table shows support for Event receiving and event Emitting To add a new connection, simply click the `Add` button on *both* devices,
on different operating systems: enter the corresponding hostname and activate it.
| Backend | Event Receiving | Event Emitting | If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
|---------------------------|--------------------------|--------------------------------------| opened up in your firewall.
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | WIP | :heavy_check_mark: |
| Wayland (Gnome) | TODO (libei support) | TODO (wlr-layer-shell not supported) |
| X11 | WIP | TODO |
| Windows | needs improvements | TODO |
| MacOS | TODO (I dont own a Mac) | TODO (I dont own a Mac) |
## Wayland compositor support ### Command Line Interface
### Input Emulation (for receiving events) The cli interface can be enabled using `--frontend cli` as commandline arguments.
On wayland input-emulation is in an early/unstable state as of writing this. Type `help` to list the available commands.
Different compositors have different ways of enabling input emulation: E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
Most wlroots-based compositors like Hyprland and Sway support the following ### Daemon
unstable wayland protocols for keyboard and mouse emulation: Lan Mouse can be launched in daemon mode to keep it running in the background.
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1) To do so, add `--daemon` to the commandline args:
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) are used to emulate input on wlroots compositors
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)), it is however not exposed to ```sh
third party apps, so the recommended way of enabling input emulation in KDE is the $ cargo run --release -- --daemon
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop). ```
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation, ## Configuration
which has the goal to become the general approach for emulating Input on wayland. To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
| Required Protocols (Event Receiving) | Sway | Kwin | Gnome | To create this file you can copy the following example config:
|----------------------------------------|--------------------|----------------------|----------------------|
| wlr-virtual-pointer-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| virtual-keyboard-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| ~fake-input~ | :x: | ~:heavy_check_mark:~ | :x: |
### Input capture ### Example config
To capture mouse and keyboard input, a few things are necessary: ```toml
- Displaying an immovable surface at screen edges # example configuration
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome | # optional port (defaults to 4242)
|----------------------------------------|--------------------|----------------------|----------------------| port = 4242
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | # # optional frontend -> defaults to gtk if available
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | # # possible values are "cli" and "gtk"
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | # frontend = "gtk"
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required # define a client on the right side with host name "iridium"
to display surfaces on screen edges and used to display the immovable window on [right]
both wlroots based compositors and KDE. # hostname
host_name = "iridium"
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
Gnome unfortunately does not support this protocol # define a client on the left side with IP address 192.168.178.189
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141). [left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
host_name = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242
```
So there is currently no way of doing this in Wayland, aside from a custom Gnome-Shell Where `left` can be either `left`, `right`, `top` or `bottom`.
extension, which is not a very elegant solution.
This is to be looked into in the future. ## Roadmap
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)
## Windows support
Currently windows can receive mouse and keyboard events, however unlike
with the wlroots back-end,
the scancodes are not translated between keyboard layouts.
Event emitting is WIP.
## TODOS
- [x] Capture the actual mouse events on the server side via a wayland client and send them to the client - [x] Capture the actual mouse events on the server side via a wayland client and send them to the client
- [x] Mouse grabbing - [x] Mouse grabbing
- [x] Window with absolute position -> wlr\_layer\_shell - [x] Window with absolute position -> wlr\_layer\_shell
@@ -151,14 +144,13 @@ Event emitting is WIP.
- [x] Button support - [x] Button support
- [ ] Latency measurement + logging - [ ] Latency measurement + logging
- [ ] Bandwidth usage approximation + logging - [ ] Bandwidth usage approximation + logging
- [ ] Multiple IP addresses -> check which one is reachable - [x] Multiple IP addresses -> check which one is reachable
- [x] Merge server and client -> Both client and server can send and receive events depending on what mouse is used where - [x] Merge server and client -> Both client and server can send and receive events depending on what mouse is used where
- [ ] Liveness tracking (automatically ungrab mouse when client unreachable) - [x] Liveness tracking (automatically ungrab mouse when client unreachable)
- [ ] Clipboard support - [ ] Clipboard support
- [ ] Graphical frontend (gtk?) - [x] Graphical frontend (gtk?)
- [ ] *Encrytion* - [ ] *Encryption*
- [ ] Gnome Shell Extension (layer shell is not supported) - [x] respect xdg-config-home for config file location.
- [ ] respect xdg-config-home for config file location.
## Protocol ## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons. Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
@@ -207,3 +199,55 @@ would be a better choice for the future and could also help for WIFI connections
Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes. Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes.
- There should be an encryption layer below the application to enable a secure link. - There should be an encryption layer below the application to enable a secure link.
- The encryption keys could be generated by the graphical frontend. - The encryption keys could be generated by the graphical frontend.
## Wayland support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
For this reason a suitable backend is chosen based on the active desktop environment / compositor.
Different compositors have different ways of enabling input emulation:
#### Wlroots
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1)
#### KDE
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)),
it is however not exposed to third party applications.
The recommended way to emulate input on KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
#### Gnome (TODO)
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation,
which has the goal to become the general approach for emulating Input on wayland.
### Input capture
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)

9
build.rs Normal file
View File

@@ -0,0 +1,9 @@
fn main() {
// composite_templates
#[cfg(unix)]
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresource.xml",
"lan-mouse.gresource",
);
}

View File

@@ -2,19 +2,22 @@
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# define a client on the right side with host name "iridium" # define a client on the right side with host name "iridium"
[right] [right]
# hostname # hostname
host_name = "iridium" host_name = "iridium"
# optional list of (known) ip addresses # optional list of (known) ip addresses
ips = ["192.168.178.141"] ips = ["192.168.178.156"]
# optional port (defaults to 4242)
port = 4242
# define a client on the left side with IP address 192.168.178.189 # define a client on the left side with IP address 192.168.178.189
[left] [left]
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
host_name = "thorium" host_name = "thorium"
ips = ["192.168.178.189"] # ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242

14
de.feschber.LanMouse.yml Normal file
View File

@@ -0,0 +1,14 @@
app-id: de.feschber.LanMouse
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: target/release/lan-mouse
modules:
- name: hello
buildsystem: simple
build-commands:
- cargo build --release
- install -D lan-mouse /app/bin/lan-mouse
sources:
- type: file
path: target/release/lan-mouse

74
resources/client_row.ui Normal file
View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ClientRow" parent="AdwExpanderRow">
<property name="title">hostname</property>
<!-- enabled -->
<child type="prefix">
<object class="GtkSwitch" id="enable_switch">
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
<!-- host -->
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<property name="subtitle">port</property>
<!-- hostname -->
<child>
<object class="GtkEntry" id="hostname">
<!-- <property name="title" translatable="yes">hostname</property> -->
<property name="xalign">0.5</property>
<property name="valign">center</property>
<property name="placeholder-text">hostname</property>
<property name="width-chars">-1</property>
</object>
</child>
<!-- port -->
<child>
<object class="GtkEntry" id="port">
<!-- <property name="title" translatable="yes">port</property> -->
<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
</object>
</child>
</object>
</child>
<!-- position -->
<child>
<object class="AdwComboRow" id="position">
<property name="title" translatable="yes">position</property>
<property name="model">
<object class="GtkStringList">
<items>
<item>Left</item>
<item>Right</item>
<item>Top</item>
<item>Bottom</item>
</items>
</object>
</property>
</object>
</child>
<!-- delete button -->
<child>
<object class="AdwActionRow" id="delete_row">
<property name="title">delete this client</property>
<child>
<object class="GtkButton" id="delete_button">
<signal name="activate" handler="handle_client_delete" object="delete_row" swapped="true"/>
<property name="icon-name">user-trash-symbolic</property>
<property name="valign">center</property>
<property name="halign">center</property>
<property name="name">delete-button</property>
</object>
</child>
</object>
</child>
</template>
</interface>

171
resources/mouse-icon.svg Normal file
View File

@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="mouse-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="22.737887"
inkscape:cx="19.54887"
inkscape:cy="26.167778"
inkscape:window-width="2560"
inkscape:window-height="1374"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g20"
transform="translate(1.1586889,0.39019296)">
<g
id="g8"
transform="translate(-0.11519282,-3.9659242)">
<g
id="g6"
transform="translate(0.67275315,0.39959697)">
<g
id="g5">
<rect
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="rect4"
width="1.3032579"
height="1.3032579"
x="1.7199994"
y="7.5408325"
ry="0.3373504" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="rect4-2"
width="1.3032579"
height="1.3032579"
x="3.8428385"
y="7.5408325"
ry="0.3373504" />
</g>
<rect
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="rect4-3"
width="1.3032579"
height="1.3032579"
x="2.781419"
y="5.1382394"
ry="0.3373504" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 1.1519282,7.3907619 H 7.059674"
id="path5" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 4.1058009,6.8410941 V 7.3907617"
id="path6" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="m 5.1672204,7.9404294 2e-7,-0.5496677"
id="path7" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 3.0443815,7.9404294 V 7.3907617"
id="path8" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 6.9444811,3.4248375 Z"
id="path9" />
<path
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 6.9840449,3.4464199 c -0.072714,-0.0035 -0.1209639,-0.2113583 -0.125,-0.1386718 -0.0035,0.072714 0.052314,0.1346357 0.125,0.1386718 0,0 0.6614057,0.034643 1.3535156,0.4765625 0.6921097,0.4419191 1.4111567,1.2803292 1.5136717,2.9433594 0.05132,0.832563 -0.07521,1.3855916 -0.279297,1.75 -0.20409,0.3644084 -0.482943,0.5482749 -0.777343,0.640625 -0.5888014,0.1847002 -1.2265629,-0.021484 -1.2265629,-0.021484 -0.069024,-0.023541 -0.144095,0.013122 -0.1679688,0.082031 -0.023366,0.069587 0.014295,0.1449093 0.083984,0.1679687 0,0 0.6961634,0.2406696 1.3886717,0.023437 C 9.2189712,9.4003039 9.5672292,9.1706004 9.8043572,8.7472012 10.041486,8.323802 10.170261,7.7150888 10.116858,6.8487637 10.009921,5.1140179 9.2320232,4.3532014 8.4801387,3.8731154 7.7282538,3.3930294 6.9840449,3.4464198 6.9840449,3.4464199 Z"
id="path18"
sodipodi:nodetypes="cccsssscccssssc" />
<g
id="g19"
transform="matrix(1.8148709,0,0,1.8148709,-4.1533763,-7.8818885)">
<g
id="g17"
transform="translate(0.01163623,0.23038484)">
<ellipse
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="path10"
cx="3.9823804"
cy="8.17869"
rx="0.49368349"
ry="0.62533247" />
<ellipse
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="ellipse17"
cx="3.9823804"
cy="8.17869"
rx="0.31096464"
ry="0.40317491" />
</g>
<path
id="path11"
style="stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round"
d="M 7.479305,9.4704944 C 7.4964603,9.9336885 6.9306558,9.9678313 5.3811502,10.087599 3.2109768,10.255341 2.4751992,9.6707727 2.4355055,9.5280908 2.3112754,9.0815374 3.8270232,8.4090748 5.3811502,8.4090748 c 1.5633309,0 2.0816988,0.6171052 2.0981548,1.0614196 z"
sodipodi:nodetypes="sssss" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="path12"
cx="3.5281858"
cy="9.0632057"
r="0.18513133" />
<g
id="g18"
transform="translate(0.01163623,0.23038484)">
<ellipse
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="path10-2"
cx="4.6085634"
cy="8.17869"
rx="0.49368349"
ry="0.62533247" />
<ellipse
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="ellipse16"
cx="4.6085634"
cy="8.17869"
rx="0.31096464"
ry="0.40317491" />
</g>
<ellipse
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.112226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="circle18"
cx="3.5003331"
cy="9.0344076"
rx="0.078639306"
ry="0.07816644" />
<ellipse
style="fill:#4f4f4f;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path19"
cx="2.4818404"
cy="9.4499254"
rx="0.05348238"
ry="0.11930636" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<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">mouse-icon.svg</file>
</gresource>
</gresources>

11
resources/style-dark.css Normal file
View File

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

11
resources/style.css Normal file
View File

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

145
resources/window.ui Normal file
View File

@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<menu id="main-menu">
<item>
<attribute name="label" translatable="yes">_Close window</attribute>
<attribute name="action">window.close</attribute>
</item>
</menu>
<template class="LanMouseWindow" parent="AdwApplicationWindow">
<property name="width-request">600</property>
<property name="height-request">700</property>
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<property name="content">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type ="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
</child>
<property name="content">
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">mouse-icon</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">600</property>
<property name="tightening-threshold">0</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">General</property>
<!--
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">enable</property>
<child type="suffix">
<object class="GtkSwitch">
<property name="valign">center</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
</object>
</child>
-->
<child>
<object class="AdwActionRow">
<property name="title">port</property>
<child>
<object class="GtkEntry" id="port_entry">
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
<signal name="changed" handler="handle_port_changed" swapped="true"/>
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
<!-- <property name="title" translatable="yes">port</property> -->
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<!-- <property name="show-apply-button">True</property> -->
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_apply">
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
<property name="icon-name">object-select-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-apply</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_cancel">
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
<property name="icon-name">process-stop-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-cancel</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Connections</property>
<property name="header-suffix">
<object class="GtkButton">
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</property>
<child>
<object class="GtkListBox" id="client_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow" id="client_placeholder">
<property name="title">No connections!</property>
<property name="subtitle">add a new client via the + button</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</property>
</object>
</property>
</template>
</interface>

View File

@@ -1,14 +1,17 @@
#[cfg(windows)] #[cfg(windows)]
pub mod windows; pub mod windows;
#[cfg(all(unix, feature="x11"))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11; pub mod x11;
#[cfg(all(unix, feature = "wayland"))] #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wlroots; pub mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal"))] #[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
pub mod xdg_desktop_portal; pub mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei"))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei; pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;

View File

@@ -1,18 +1,381 @@
use crate::consumer::EventConsumer; use std::{
collections::HashMap,
io,
os::{
fd::{FromRawFd, RawFd},
unix::net::UnixStream,
},
time::{SystemTime, UNIX_EPOCH},
};
pub struct LibeiConsumer {} use anyhow::{anyhow, Result};
use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop};
use async_trait::async_trait;
use futures::StreamExt;
use reis::{
ei::{self, button::ButtonState, handshake::ContextType, keyboard::KeyState},
tokio::EiEventStream,
PendingRequestResult,
};
use crate::{
client::{ClientEvent, ClientHandle},
consumer::EventConsumer,
event::Event,
};
pub struct LibeiConsumer {
handshake: bool,
context: ei::Context,
events: EiEventStream,
pointer: Option<(ei::Device, ei::Pointer)>,
has_pointer: bool,
scroll: Option<(ei::Device, ei::Scroll)>,
has_scroll: bool,
button: Option<(ei::Device, ei::Button)>,
has_button: bool,
keyboard: Option<(ei::Device, ei::Keyboard)>,
has_keyboard: bool,
capabilities: HashMap<String, u64>,
capability_mask: u64,
sequence: u32,
serial: u32,
}
async fn get_ei_fd() -> Result<RawFd, ashpd::Error> {
let proxy = RemoteDesktop::new().await?;
let session = proxy.create_session().await?;
// I HATE EVERYTHING, THIS TOOK 8 HOURS OF DEBUGGING
proxy
.select_devices(
&session,
DeviceType::Pointer | DeviceType::Keyboard | DeviceType::Touchscreen,
)
.await?;
proxy
.start(&session, &ashpd::WindowIdentifier::default())
.await?
.response()?;
proxy.connect_to_eis(&session).await
}
impl LibeiConsumer { impl LibeiConsumer {
pub fn new() -> Self { Self { } } pub async fn new() -> Result<Self> {
// fd is owned by the message, so we need to dup it
let eifd = get_ei_fd().await?;
let eifd = unsafe {
let ret = libc::dup(eifd);
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret)
}
}?;
let stream = unsafe { UnixStream::from_raw_fd(eifd) };
// let stream = UnixStream::connect("/run/user/1000/eis-0")?;
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
context.flush()?;
let events = EiEventStream::new(context.clone())?;
return Ok(Self {
handshake: false,
context,
events,
pointer: None,
button: None,
scroll: None,
keyboard: None,
has_pointer: false,
has_button: false,
has_scroll: false,
has_keyboard: false,
capabilities: HashMap::new(),
capability_mask: 0,
sequence: 0,
serial: 0,
});
}
} }
#[async_trait]
impl EventConsumer for LibeiConsumer { impl EventConsumer for LibeiConsumer {
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) { async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
log::error!("libei backend not yet implemented!"); let now = SystemTime::now()
todo!() .duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
match event {
Event::Pointer(p) => match p {
crate::event::PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if !self.has_pointer {
return;
}
if let Some((d, p)) = self.pointer.as_mut() {
p.motion_relative(relative_x as f32, relative_y as f32);
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Button {
time: _,
button,
state,
} => {
if !self.has_button {
return;
}
if let Some((d, b)) = self.button.as_mut() {
b.button(
button,
match state {
0 => ButtonState::Released,
_ => ButtonState::Press,
},
);
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Axis {
time: _,
axis,
value,
} => {
if !self.has_scroll {
return;
}
if let Some((d, s)) = self.scroll.as_mut() {
match axis {
0 => s.scroll(0., value as f32),
_ => s.scroll(value as f32, 0.),
}
d.frame(self.serial, now);
}
}
crate::event::PointerEvent::Frame {} => {}
},
Event::Keyboard(k) => match k {
crate::event::KeyboardEvent::Key {
time: _,
key,
state,
} => {
if !self.has_keyboard {
return;
}
if let Some((d, k)) = &mut self.keyboard {
k.key(
key,
match state {
0 => KeyState::Released,
_ => KeyState::Press,
},
);
d.frame(self.serial, now);
}
}
crate::event::KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
self.context.flush().unwrap();
} }
fn notify(&mut self, _: crate::client::ClientEvent) { async fn dispatch(&mut self) -> Result<()> {
todo!() let event = match self.events.next().await {
Some(e) => e?,
None => return Err(anyhow!("libei connection lost")),
};
let event = match event {
PendingRequestResult::Request(result) => result,
PendingRequestResult::ProtocolError(e) => {
return Err(anyhow!("libei protocol violation: {e}"))
}
PendingRequestResult::InvalidObject(e) => return Err(anyhow!("invalid object {e}")),
};
match event {
ei::Event::Handshake(handshake, request) => match request {
ei::handshake::Event::HandshakeVersion { version } => {
if self.handshake {
return Ok(());
}
log::info!("libei version {}", version);
// sender means we are sending events _to_ the eis server
handshake.handshake_version(version); // FIXME
handshake.context_type(ContextType::Sender);
handshake.name("ei-demo-client");
handshake.interface_version("ei_connection", 1);
handshake.interface_version("ei_callback", 1);
handshake.interface_version("ei_pingpong", 1);
handshake.interface_version("ei_seat", 1);
handshake.interface_version("ei_device", 2);
handshake.interface_version("ei_pointer", 1);
handshake.interface_version("ei_pointer_absolute", 1);
handshake.interface_version("ei_scroll", 1);
handshake.interface_version("ei_button", 1);
handshake.interface_version("ei_keyboard", 1);
handshake.interface_version("ei_touchscreen", 1);
handshake.finish();
self.handshake = true;
}
ei::handshake::Event::InterfaceVersion { name, version } => {
log::debug!("handshake: Interface {name} @ {version}");
}
ei::handshake::Event::Connection { serial, connection } => {
connection.sync(1);
self.serial = serial;
}
_ => unreachable!(),
},
ei::Event::Connection(_connection, request) => match request {
ei::connection::Event::Seat { seat } => {
log::debug!("connected to seat: {seat:?}");
}
ei::connection::Event::Ping { ping } => {
ping.done(0);
}
ei::connection::Event::Disconnected {
last_serial: _,
reason,
explanation,
} => {
log::debug!("ei - disconnected: reason: {reason:?}: {explanation}")
}
ei::connection::Event::InvalidObject {
last_serial,
invalid_id,
} => {
return Err(anyhow!(
"invalid object: id: {invalid_id}, serial: {last_serial}"
));
}
_ => unreachable!(),
},
ei::Event::Device(device, request) => match request {
ei::device::Event::Destroyed { serial } => {
log::debug!("device destroyed: {device:?} - serial: {serial}")
}
ei::device::Event::Name { name } => {
log::debug!("device name: {name}")
}
ei::device::Event::DeviceType { device_type } => {
log::debug!("device type: {device_type:?}")
}
ei::device::Event::Dimensions { width, height } => {
log::debug!("device dimensions: {width}x{height}")
}
ei::device::Event::Region {
offset_x,
offset_y,
width,
hight,
scale,
} => log::debug!(
"device region: {width}x{hight} @ ({offset_x},{offset_y}), scale: {scale}"
),
ei::device::Event::Interface { object } => {
log::debug!("device interface: {object:?}");
if object.interface().eq("ei_pointer") {
log::debug!("GOT POINTER DEVICE");
self.pointer.replace((device, object.downcast().unwrap()));
} else if object.interface().eq("ei_button") {
log::debug!("GOT BUTTON DEVICE");
self.button.replace((device, object.downcast().unwrap()));
} else if object.interface().eq("ei_scroll") {
log::debug!("GOT SCROLL DEVICE");
self.scroll.replace((device, object.downcast().unwrap()));
} else if object.interface().eq("ei_keyboard") {
log::debug!("GOT KEYBOARD DEVICE");
self.keyboard.replace((device, object.downcast().unwrap()));
}
}
ei::device::Event::Done => {
log::debug!("device: done {device:?}");
}
ei::device::Event::Resumed { serial } => {
self.serial = serial;
device.start_emulating(serial, self.sequence);
self.sequence += 1;
log::debug!("resumed: {device:?}");
if let Some((d, _)) = &mut self.pointer {
if d == &device {
log::debug!("pointer resumed {serial}");
self.has_pointer = true;
}
}
if let Some((d, _)) = &mut self.button {
if d == &device {
log::debug!("button resumed {serial}");
self.has_button = true;
}
}
if let Some((d, _)) = &mut self.scroll {
if d == &device {
log::debug!("scroll resumed {serial}");
self.has_scroll = true;
}
}
if let Some((d, _)) = &mut self.keyboard {
if d == &device {
log::debug!("keyboard resumed {serial}");
self.has_keyboard = true;
}
}
}
ei::device::Event::Paused { serial } => {
self.has_pointer = false;
self.has_button = false;
self.serial = serial;
}
ei::device::Event::StartEmulating { serial, sequence } => {
log::debug!("start emulating {serial}, {sequence}")
}
ei::device::Event::StopEmulating { serial } => {
log::debug!("stop emulating {serial}")
}
ei::device::Event::Frame { serial, timestamp } => {
log::debug!("frame: {serial}, {timestamp}");
}
ei::device::Event::RegionMappingId { mapping_id } => {
log::debug!("RegionMappingId {mapping_id}")
}
e => log::debug!("invalid event: {e:?}"),
},
ei::Event::Seat(seat, request) => match request {
ei::seat::Event::Destroyed { serial } => {
self.serial = serial;
log::debug!("seat destroyed: {seat:?}");
}
ei::seat::Event::Name { name } => {
log::debug!("seat name: {name}");
}
ei::seat::Event::Capability { mask, interface } => {
log::debug!("seat capabilities: {mask}, interface: {interface:?}");
self.capabilities.insert(interface, mask);
self.capability_mask |= mask;
}
ei::seat::Event::Done => {
log::debug!("seat done");
log::debug!("binding capabilities: {}", self.capability_mask);
seat.bind(self.capability_mask);
}
ei::seat::Event::Device { device } => {
log::debug!("seat: new device - {device:?}");
}
_ => todo!(),
},
e => log::debug!("unhandled event: {e:?}"),
}
self.context.flush()?;
Ok(())
} }
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
} }

View File

@@ -0,0 +1,221 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::consumer::EventConsumer;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::CGPoint;
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use std::ops::{Index, IndexMut};
pub struct MacOSConsumer {
pub event_source: CGEventSource,
button_state: ButtonState,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSConsumer {}
impl MacOSConsumer {
pub fn new() -> Result<Self> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
Ok(e) => e,
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
}
#[async_trait]
impl EventConsumer for MacOSConsumer {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return;
}
};
mouse_location.x += relative_x;
mouse_location.y += relative_y;
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return;
}
};
// store button state
self.button_state[mouse_button] = if state == 1 { true } else { false };
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32 / 10; // FIXME: high precision scroll events
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::LINE,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Frame { .. } => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { .. } => {
/*
let code = CGKeyCode::from_le(key as u16);
let event = match CGEvent::new_keyboard_event(
self.event_source.clone(),
code,
match state { 1 => true, _ => false }
) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return
}
};
event.post(CGEventTapLocation::HID);
*/
}
KeyboardEvent::Modifiers { .. } => {}
},
Event::Release() => {}
Event::Ping() => {}
Event::Pong() => {}
}
}
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
}

View File

@@ -1,15 +1,15 @@
use crate::{event::{KeyboardEvent, PointerEvent}, consumer::EventConsumer}; use crate::{
consumer::EventConsumer,
event::{KeyboardEvent, PointerEvent},
};
use async_trait::async_trait;
use winapi::{ use winapi::{
self, self,
um::winuser::{INPUT, INPUT_MOUSE, LPINPUT, MOUSEEVENTF_MOVE, MOUSEINPUT, um::winuser::{
MOUSEEVENTF_LEFTDOWN, INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_RIGHTDOWN, LPINPUT, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_LEFTUP, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEINPUT,
MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_MIDDLEUP,
MOUSEEVENTF_WHEEL,
MOUSEEVENTF_HWHEEL, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_SCANCODE, KEYEVENTF_KEYUP,
}, },
}; };
@@ -18,15 +18,17 @@ use crate::{
event::Event, event::Event,
}; };
pub struct WindowsConsumer {} pub struct WindowsConsumer {}
impl WindowsConsumer { impl WindowsConsumer {
pub fn new() -> Self { Self { } } pub fn new() -> Self {
Self {}
}
} }
#[async_trait]
impl EventConsumer for WindowsConsumer { impl EventConsumer for WindowsConsumer {
fn consume(&self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: ClientHandle) {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { PointerEvent::Motion {
@@ -36,21 +38,35 @@ impl EventConsumer for WindowsConsumer {
} => { } => {
rel_mouse(relative_x as i32, relative_y as i32); rel_mouse(relative_x as i32, relative_y as i32);
} }
PointerEvent::Button { time:_, button, state } => { mouse_button(button, state)} PointerEvent::Button {
PointerEvent::Axis { time:_, axis, value } => { scroll(axis, value) } time: _,
button,
state,
} => mouse_button(button, state),
PointerEvent::Axis {
time: _,
axis,
value,
} => scroll(axis, value),
PointerEvent::Frame {} => {} PointerEvent::Frame {} => {}
}, },
Event::Keyboard(keyboard_event) => match keyboard_event { Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { time:_, key, state } => { key_event(key, state) } KeyboardEvent::Key {
time: _,
key,
state,
} => key_event(key, state),
KeyboardEvent::Modifiers { .. } => {} KeyboardEvent::Modifiers { .. } => {}
}, },
_ => {} _ => {}
} }
} }
fn notify(&mut self, _: ClientEvent) { async fn notify(&mut self, _: ClientEvent) {
// nothing to do // nothing to do
} }
async fn destroy(&mut self) {}
} }
fn send_mouse_input(mi: MOUSEINPUT) { fn send_mouse_input(mi: MOUSEINPUT) {
@@ -86,18 +102,19 @@ fn mouse_button(button: u32, state: u32) {
0x110 => MOUSEEVENTF_LEFTUP, 0x110 => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP, 0x111 => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP, 0x112 => MOUSEEVENTF_MIDDLEUP,
_ => return _ => return,
} },
1 => match button { 1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN, 0x110 => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN, 0x111 => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN, 0x112 => MOUSEEVENTF_MIDDLEDOWN,
_ => return _ => return,
} },
_ => return _ => return,
}; };
let mi = MOUSEINPUT { let mi = MOUSEINPUT {
dx: 0, dy: 0, // no movement dx: 0,
dy: 0, // no movement
mouseData: 0, mouseData: 0,
dwFlags: dw_flags, dwFlags: dw_flags,
time: 0, time: 0,
@@ -110,10 +127,11 @@ fn scroll(axis: u8, value: f64) {
let event_type = match axis { let event_type = match axis {
0 => MOUSEEVENTF_WHEEL, 0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL, 1 => MOUSEEVENTF_HWHEEL,
_ => return _ => return,
}; };
let mi = MOUSEINPUT { let mi = MOUSEINPUT {
dx: 0, dy: 0, dx: 0,
dy: 0,
mouseData: (-value * 15.0) as i32 as u32, mouseData: (-value * 15.0) as i32 as u32,
dwFlags: event_type, dwFlags: event_type,
time: 0, time: 0,
@@ -126,11 +144,12 @@ fn key_event(key: u32, state: u8) {
let ki = KEYBDINPUT { let ki = KEYBDINPUT {
wVk: 0, wVk: 0,
wScan: key as u16, wScan: key as u16,
dwFlags: KEYEVENTF_SCANCODE | match state { dwFlags: KEYEVENTF_SCANCODE
0 => KEYEVENTF_KEYUP, | match state {
1 => 0u32, 0 => KEYEVENTF_KEYUP,
_ => return 1 => 0u32,
}, _ => return,
},
time: 0, time: 0,
dwExtraInfo: 0, dwExtraInfo: 0,
}; };

View File

@@ -1,14 +1,17 @@
use wayland_client::WEnum; use crate::client::{ClientEvent, ClientHandle};
use crate::client::{ClientHandle, ClientEvent};
use crate::consumer::EventConsumer; use crate::consumer::EventConsumer;
use async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use std::io;
use std::os::fd::OwnedFd; use std::os::fd::OwnedFd;
use std::os::unix::prelude::AsRawFd; use std::os::unix::prelude::AsRawFd;
use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use wayland_client::globals::BindError; use wayland_client::globals::BindError;
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{ use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager, zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
@@ -46,6 +49,7 @@ struct State {
// App State, implements Dispatch event handlers // App State, implements Dispatch event handlers
pub(crate) struct WlrootsConsumer { pub(crate) struct WlrootsConsumer {
last_flush_failed: bool,
state: State, state: State,
queue: EventQueue<State>, queue: EventQueue<State>,
} }
@@ -89,6 +93,7 @@ impl WlrootsConsumer {
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new(); let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
let mut consumer = WlrootsConsumer { let mut consumer = WlrootsConsumer {
last_flush_failed: false,
state: State { state: State {
keymap: None, keymap: None,
input_for_client, input_for_client,
@@ -99,7 +104,10 @@ impl WlrootsConsumer {
queue, queue,
}; };
while consumer.state.keymap.is_none() { while consumer.state.keymap.is_none() {
consumer.queue.blocking_dispatch(&mut consumer.state).unwrap(); consumer
.queue
.blocking_dispatch(&mut consumer.state)
.unwrap();
} }
// let fd = unsafe { &File::from_raw_fd(consumer.state.keymap.unwrap().1.as_raw_fd()) }; // let fd = unsafe { &File::from_raw_fd(consumer.state.keymap.unwrap().1.as_raw_fd()) };
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() }; // let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
@@ -136,17 +144,45 @@ impl State {
} }
} }
#[async_trait]
impl EventConsumer for WlrootsConsumer { impl EventConsumer for WlrootsConsumer {
fn consume(&self, event: Event, client_handle: ClientHandle) { async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) { if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) {
if self.last_flush_failed {
if let Err(WaylandError::Io(e)) = self.queue.flush() {
if e.kind() == io::ErrorKind::WouldBlock {
/*
* outgoing buffer is full - sending more events
* will overwhelm the output buffer and leave the
* wayland connection in a broken state
*/
log::warn!(
"can't keep up, discarding event: ({client_handle}) - {event:?}"
);
return;
}
}
}
virtual_input.consume_event(event).unwrap(); virtual_input.consume_event(event).unwrap();
if let Err(e) = self.queue.flush() { match self.queue.flush() {
log::error!("{}", e); Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
self.last_flush_failed = true;
log::warn!("can't keep up, retrying ...");
}
Err(WaylandError::Io(e)) => {
log::error!("{e}")
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland protocol violation: {e}")
}
Ok(()) => {
self.last_flush_failed = false;
}
} }
} }
} }
fn notify(&mut self, client_event: ClientEvent) { async fn notify(&mut self, client_event: ClientEvent) {
if let ClientEvent::Create(client, _) = client_event { if let ClientEvent::Create(client, _) = client_event {
self.state.add_client(client); self.state.add_client(client);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
@@ -154,8 +190,9 @@ impl EventConsumer for WlrootsConsumer {
} }
} }
} }
}
async fn destroy(&mut self) {}
}
enum VirtualInput { enum VirtualInput {
Wlroots { pointer: Vp, keyboard: Vk }, Wlroots { pointer: Vp, keyboard: Vk },
@@ -163,67 +200,76 @@ enum VirtualInput {
} }
impl VirtualInput { impl VirtualInput {
fn consume_event(&self, event: Event) -> Result<(),()> { fn consume_event(&self, event: Event) -> Result<(), ()> {
match event { match event {
Event::Pointer(e) => match e { Event::Pointer(e) => {
PointerEvent::Motion { match e {
time, PointerEvent::Motion {
relative_x, time,
relative_y, relative_x,
} => match self { relative_y,
VirtualInput::Wlroots { } => match self {
pointer,
keyboard: _,
} => {
pointer.motion(time, relative_x, relative_y);
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
}
},
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
match self {
VirtualInput::Wlroots { VirtualInput::Wlroots {
pointer, pointer,
keyboard: _, keyboard: _,
} => { } => {
pointer.button(time, button, state); pointer.motion(time, relative_x, relative_y);
} }
VirtualInput::Kde { fake_input } => { VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32); fake_input.pointer_motion(relative_y, relative_y);
}
},
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.button(time, button, state);
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
}
} }
} }
} PointerEvent::Axis { time, axis, value } => {
PointerEvent::Axis { time, axis, value } => { let axis: Axis = (axis as u32).try_into()?;
let axis: Axis = (axis as u32).try_into()?; match self {
match self { VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.axis(time, axis, value);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.axis(axis as u32, value);
}
}
}
PointerEvent::Frame {} => match self {
VirtualInput::Wlroots { VirtualInput::Wlroots {
pointer, pointer,
keyboard: _, keyboard: _,
} => { } => {
pointer.axis(time, axis, value);
pointer.frame(); pointer.frame();
} }
VirtualInput::Kde { fake_input } => { VirtualInput::Kde { fake_input: _ } => {}
fake_input.axis(axis as u32, value); },
}
}
} }
PointerEvent::Frame {} => match self { match self {
VirtualInput::Wlroots { VirtualInput::Wlroots { pointer, .. } => {
pointer, // insert a frame event after each mouse event
keyboard: _,
} => {
pointer.frame(); pointer.frame();
} }
VirtualInput::Kde { fake_input: _ } => {} _ => {}
}, }
}, }
Event::Keyboard(e) => match e { Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => match self { KeyboardEvent::Key { time, key, state } => match self {
VirtualInput::Wlroots { VirtualInput::Wlroots {
@@ -251,7 +297,7 @@ impl VirtualInput {
VirtualInput::Kde { fake_input: _ } => {} VirtualInput::Kde { fake_input: _ } => {}
}, },
}, },
event => panic!("unknown event type {event:?}"), _ => {}
} }
Ok(()) Ok(())
} }
@@ -288,7 +334,7 @@ impl Dispatch<WlKeyboard, ()> for State {
wl_keyboard::Event::Keymap { format, fd, size } => { wl_keyboard::Event::Keymap { format, fd, size } => {
state.keymap = Some((u32::from(format), fd, size)); state.keymap = Some((u32::from(format), fd, size));
} }
_ => {}, _ => {}
} }
} }
} }

View File

@@ -1,15 +1,15 @@
use async_trait::async_trait;
use std::ptr; use std::ptr;
use x11::{xlib, xtest}; use x11::{xlib, xtest};
use crate::{ use crate::{client::ClientHandle, consumer::EventConsumer, event::Event};
client::ClientHandle,
event::Event, consumer::EventConsumer,
};
pub struct X11Consumer { pub struct X11Consumer {
display: *mut xlib::Display, display: *mut xlib::Display,
} }
unsafe impl Send for X11Consumer {}
impl X11Consumer { impl X11Consumer {
pub fn new() -> Self { pub fn new() -> Self {
let display = unsafe { let display = unsafe {
@@ -30,8 +30,9 @@ impl X11Consumer {
} }
} }
#[async_trait]
impl EventConsumer for X11Consumer { impl EventConsumer for X11Consumer {
fn consume(&self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: ClientHandle) {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
crate::event::PointerEvent::Motion { crate::event::PointerEvent::Motion {
@@ -50,8 +51,9 @@ impl EventConsumer for X11Consumer {
} }
} }
fn notify(&mut self, _: crate::client::ClientEvent) { async fn notify(&mut self, _: crate::client::ClientEvent) {
// for our purposes it does not matter what client sent the event // for our purposes it does not matter what client sent the event
} }
}
async fn destroy(&mut self) {}
}

View File

@@ -1,15 +1,128 @@
use anyhow::Result;
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
Session,
},
WindowIdentifier,
};
use async_trait::async_trait;
use crate::consumer::EventConsumer; use crate::consumer::EventConsumer;
pub struct DesktopPortalConsumer {} pub struct DesktopPortalConsumer<'a> {
proxy: RemoteDesktop<'a>,
impl DesktopPortalConsumer { session: Session<'a>,
pub fn new() -> Self { Self { } }
} }
impl EventConsumer for DesktopPortalConsumer { impl<'a> DesktopPortalConsumer<'a> {
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) { pub async fn new() -> Result<DesktopPortalConsumer<'a>> {
log::error!("xdg_desktop_portal backend not yet implemented!"); let proxy = RemoteDesktop::new().await?;
let session = proxy.create_session().await?;
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
let _ = proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event {
crate::event::Event::Pointer(p) => {
match p {
crate::event::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}");
}
}
crate::event::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}");
}
}
crate::event::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}");
}
}
crate::event::PointerEvent::Frame {} => {}
}
}
crate::event::Event::Keyboard(k) => {
match k {
crate::event::KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await
{
log::warn!("{e}");
}
}
crate::event::KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
_ => {}
}
} }
fn notify(&mut self, _: crate::client::ClientEvent) {} async fn notify(&mut self, _client: crate::client::ClientEvent) {}
async fn destroy(&mut self) {
log::debug!("closing remote desktop session");
if let Err(e) = self.session.close().await {
log::error!("failed to close remote desktop session: {e}");
}
}
} }

View File

@@ -1,6 +1,10 @@
#[cfg(all(unix, feature = "wayland"))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland; pub mod wayland;
#[cfg(windows)] #[cfg(windows)]
pub mod windows; pub mod windows;
#[cfg(all(unix, feature = "x11"))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11; pub mod x11;

View File

@@ -0,0 +1,31 @@
use anyhow::Result;
use std::{io, task::Poll};
use futures_core::Stream;
use crate::{client::ClientHandle, event::Event, producer::EventProducer};
pub struct LibeiProducer {}
impl LibeiProducer {
pub fn new() -> Result<Self> {
Ok(Self {})
}
}
impl EventProducer for LibeiProducer {
fn notify(&mut self, _event: crate::client::ClientEvent) {}
fn release(&mut self) {}
}
impl Stream for LibeiProducer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

@@ -0,0 +1,28 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::event::Event;
use crate::producer::EventProducer;
use futures_core::Stream;
use std::task::{Context, Poll};
use std::{io, pin::Pin};
pub struct MacOSProducer;
impl MacOSProducer {
pub fn new() -> Self {
Self {}
}
}
impl Stream for MacOSProducer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
}
}
impl EventProducer for MacOSProducer {
fn notify(&mut self, _event: ClientEvent) {}
fn release(&mut self) {}
}

View File

@@ -1,9 +1,20 @@
use crate::{client::{ClientHandle, Position, ClientEvent}, producer::EventProducer}; use crate::{
use mio::{event::Source, unix::SourceFd}; client::{ClientEvent, ClientHandle, Position},
producer::EventProducer,
};
use std::{os::fd::RawFd, vec::Drain, io::ErrorKind};
use memmap::MmapOptions;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures_core::Stream;
use memmap::MmapOptions;
use std::{
collections::VecDeque,
env,
io::{self, ErrorKind},
os::fd::{OwnedFd, RawFd},
pin::Pin,
task::{ready, Context, Poll},
};
use tokio::io::unix::AsyncFd;
use std::{ use std::{
fs::File, fs::File,
@@ -12,18 +23,24 @@ use std::{
rc::Rc, rc::Rc,
}; };
use wayland_protocols::wp::{ use wayland_protocols::{
keyboard_shortcuts_inhibit::zv1::client::{ wp::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1, keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1, zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
},
pointer_constraints::zv1::client::{
zwp_locked_pointer_v1::ZwpLockedPointerV1,
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
},
relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
},
}, },
pointer_constraints::zv1::client::{ xdg::xdg_output::zv1::client::{
zwp_locked_pointer_v1::ZwpLockedPointerV1, zxdg_output_manager_v1::ZxdgOutputManagerV1,
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1}, zxdg_output_v1::{self, ZxdgOutputV1},
},
relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
}, },
}; };
@@ -33,14 +50,15 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
}; };
use wayland_client::{ use wayland_client::{
backend::{WaylandError, ReadEventsGuard}, backend::{ReadEventsGuard, WaylandError},
delegate_noop, delegate_noop,
globals::{registry_queue_init, GlobalListContents}, globals::{registry_queue_init, GlobalListContents},
protocol::{ protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_buffer, wl_compositor, wl_keyboard,
wl_shm_pool, wl_surface, wl_output::{self, WlOutput},
wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
}, },
Connection, Dispatch, DispatchError, QueueHandle, WEnum, EventQueue, Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
}; };
use tempfile; use tempfile;
@@ -55,6 +73,25 @@ struct Globals {
seat: wl_seat::WlSeat, seat: wl_seat::WlSeat,
shm: wl_shm::WlShm, shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1, layer_shell: ZwlrLayerShellV1,
outputs: Vec<wl_output::WlOutput>,
xdg_output_manager: ZxdgOutputManagerV1,
}
#[derive(Debug, Clone)]
struct OutputInfo {
name: String,
position: (i32, i32),
size: (i32, i32),
}
impl OutputInfo {
fn new() -> Self {
Self {
name: "".to_string(),
position: (0, 0),
size: (0, 0),
}
}
} }
struct State { struct State {
@@ -64,17 +101,26 @@ struct State {
client_for_window: Vec<(Rc<Window>, ClientHandle)>, client_for_window: Vec<(Rc<Window>, ClientHandle)>,
focused: Option<(Rc<Window>, ClientHandle)>, focused: Option<(Rc<Window>, ClientHandle)>,
g: Globals, g: Globals,
wayland_fd: RawFd, wayland_fd: OwnedFd,
read_guard: Option<ReadEventsGuard>, read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>, qh: QueueHandle<Self>,
pending_events: Vec<(ClientHandle, Event)>, pending_events: VecDeque<(ClientHandle, Event)>,
output_info: Vec<(WlOutput, OutputInfo)>,
} }
pub struct WaylandEventProducer { struct Inner {
state: State, state: State,
queue: EventQueue<State>, queue: EventQueue<State>,
} }
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.state.wayland_fd.as_raw_fd()
}
}
pub struct WaylandEventProducer(AsyncFd<Inner>);
struct Window { struct Window {
buffer: wl_buffer::WlBuffer, buffer: wl_buffer::WlBuffer,
surface: wl_surface::WlSurface, surface: wl_surface::WlSurface,
@@ -82,8 +128,19 @@ struct Window {
} }
impl Window { impl Window {
fn new(g: &Globals, qh: &QueueHandle<State>, pos: Position) -> Window { fn new(
let (width, height) = (1, 1440); state: &State,
qh: &QueueHandle<State>,
output: &WlOutput,
pos: Position,
size: (i32, i32),
) -> Window {
let g = &state.g;
let (width, height) = match pos {
Position::Left | Position::Right => (1, size.1 as u32),
Position::Top | Position::Bottom => (size.0 as u32, 1),
};
let mut file = tempfile::tempfile().unwrap(); let mut file = tempfile::tempfile().unwrap();
draw(&mut file, (width, height)); draw(&mut file, (width, height));
let pool = g let pool = g
@@ -102,8 +159,8 @@ impl Window {
let layer_surface = g.layer_shell.get_layer_surface( let layer_surface = g.layer_shell.get_layer_surface(
&surface, &surface,
None, Some(&output),
Layer::Top, Layer::Overlay,
"LAN Mouse Sharing".into(), "LAN Mouse Sharing".into(),
qh, qh,
(), (),
@@ -116,8 +173,8 @@ impl Window {
}; };
layer_surface.set_anchor(anchor); layer_surface.set_anchor(anchor);
layer_surface.set_size(1, 1440); layer_surface.set_size(width, height);
layer_surface.set_exclusive_zone(0); layer_surface.set_exclusive_zone(-1);
layer_surface.set_margin(0, 0, 0, 0); layer_surface.set_margin(0, 0, 0, 0);
surface.set_input_region(None); surface.set_input_region(None);
surface.commit(); surface.commit();
@@ -129,20 +186,85 @@ impl Window {
} }
} }
impl Drop for Window {
fn drop(&mut self) {
log::debug!("destroying window!");
self.layer_surface.destroy();
self.surface.destroy();
self.buffer.destroy();
}
}
fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput, i32)> {
outputs
.iter()
.map(|(o, i)| {
(
o.clone(),
match pos {
Position::Left => i.position.0,
Position::Right => i.position.0 + i.size.0,
Position::Top => i.position.1,
Position::Bottom => i.position.1 + i.size.1,
},
)
})
.collect()
}
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
// get all output edges corresponding to the position
let edges = get_edges(&state.output_info, pos);
let opposite_edges = get_edges(&state.output_info, pos.opposite());
// remove those edges that are at the same position
// as an opposite edge of a different output
let outputs: Vec<WlOutput> = edges
.iter()
.filter(|(_, edge)| {
opposite_edges
.iter()
.map(|(_, e)| *e)
.find(|e| e == edge)
.is_none()
})
.map(|(o, _)| o.clone())
.collect();
state
.output_info
.iter()
.filter(|(o, _)| outputs.contains(o))
.map(|(o, i)| (o.clone(), i.clone()))
.collect()
}
fn draw(f: &mut File, (width, height): (u32, u32)) { fn draw(f: &mut File, (width, height): (u32, u32)) {
let mut buf = BufWriter::new(f); let mut buf = BufWriter::new(f);
for _ in 0..height { for _ in 0..height {
for _ in 0..width { for _ in 0..width {
buf.write_all(&0x44FbF1C7u32.to_ne_bytes()).unwrap(); if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
// AARRGGBB
buf.write_all(&0xFF11d116u32.to_ne_bytes()).unwrap();
} else {
// AARRGGBB
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
}
} }
} }
} }
impl WaylandEventProducer { impl WaylandEventProducer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().expect("could not connect to wayland compositor"); let conn = match Connection::connect_to_env() {
let (g, queue) = Ok(c) => c,
registry_queue_init::<State>(&conn).expect("failed to initialize wl_registry"); Err(e) => return Err(anyhow!("could not connect to wayland compositor: {e:?}")),
};
let (g, mut queue) = match registry_queue_init::<State>(&conn) {
Ok(q) => q,
Err(e) => return Err(anyhow!("failed to initialize wl_registry: {e:?}")),
};
let qh = queue.handle(); let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) { let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) {
@@ -150,6 +272,11 @@ impl WaylandEventProducer {
Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")), Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")),
}; };
let xdg_output_manager: ZxdgOutputManagerV1 = match g.bind(&qh, 1..=3, ()) {
Ok(xdg_output_manager) => xdg_output_manager,
Err(_) => return Err(anyhow!("xdg_output not supported!")),
};
let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) { let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) {
Ok(wl_shm) => wl_shm, Ok(wl_shm) => wl_shm,
Err(_) => return Err(anyhow!("wl_shm v1 not supported")), Err(_) => return Err(anyhow!("wl_shm v1 not supported")),
@@ -175,10 +302,17 @@ impl WaylandEventProducer {
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")), Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
}; };
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = match g.bind(&qh, 1..=1, ()) { let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 =
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager, match g.bind(&qh, 1..=1, ()) {
Err(_) => return Err(anyhow!("zwp_keyboard_shortcuts_inhibit_manager_v1 not supported")), Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
}; Err(_) => {
return Err(anyhow!(
"zwp_keyboard_shortcuts_inhibit_manager_v1 not supported"
))
}
};
let outputs = vec![];
let g = Globals { let g = Globals {
compositor, compositor,
@@ -188,6 +322,8 @@ impl WaylandEventProducer {
pointer_constraints, pointer_constraints,
relative_pointer_manager, relative_pointer_manager,
shortcut_inhibit_manager, shortcut_inhibit_manager,
outputs,
xdg_output_manager,
}; };
// flush outgoing events // flush outgoing events
@@ -195,24 +331,53 @@ impl WaylandEventProducer {
// prepare reading wayland events // prepare reading wayland events
let read_guard = queue.prepare_read()?; let read_guard = queue.prepare_read()?;
let wayland_fd = read_guard.connection_fd().as_raw_fd(); let wayland_fd = read_guard.connection_fd().try_clone_to_owned().unwrap();
let read_guard = Some(read_guard); std::mem::drop(read_guard);
Ok(WaylandEventProducer { let mut state = State {
queue, g,
state: State { pointer_lock: None,
g, rel_pointer: None,
pointer_lock: None, shortcut_inhibitor: None,
rel_pointer: None, client_for_window: Vec::new(),
shortcut_inhibitor: None, focused: None,
client_for_window: Vec::new(), qh,
focused: None, wayland_fd,
qh, read_guard: None,
wayland_fd, pending_events: VecDeque::new(),
read_guard, output_info: vec![],
pending_events: vec![], };
}
}) // dispatch registry to () again, in order to read all wl_outputs
conn.display().get_registry(&state.qh, ());
log::debug!("==============> requested registry");
// roundtrip to read wl_output globals
queue.roundtrip(&mut state)?;
log::debug!("==============> roundtrip 1 done");
// read outputs
for output in state.g.outputs.iter() {
state
.g
.xdg_output_manager
.get_xdg_output(output, &state.qh, output.clone());
}
// roundtrip to read xdg_output events
queue.roundtrip(&mut state)?;
log::debug!("==============> roundtrip 2 done");
for i in &state.output_info {
log::debug!("{:#?}", i.1);
}
let read_guard = queue.prepare_read()?;
state.read_guard = Some(read_guard);
let inner = AsyncFd::new(Inner { queue, state })?;
Ok(WaylandEventProducer(inner))
} }
} }
@@ -300,35 +465,17 @@ impl State {
} }
fn add_client(&mut self, client: ClientHandle, pos: Position) { fn add_client(&mut self, client: ClientHandle, pos: Position) {
let window = Rc::new(Window::new(&self.g, &self.qh, pos)); let outputs = get_output_configuration(self, pos);
self.client_for_window.push((window, client));
outputs.iter().for_each(|(o, i)| {
let window = Window::new(&self, &self.qh, &o, pos, i.size);
let window = Rc::new(window);
self.client_for_window.push((window, client));
});
} }
} }
impl Source for WaylandEventProducer { impl Inner {
fn register(
&mut self,
registry: &mio::Registry,
token: mio::Token,
interests: mio::Interest,
) -> std::io::Result<()> {
SourceFd(&self.state.wayland_fd).register(registry, token, interests)
}
fn reregister(
&mut self,
registry: &mio::Registry,
token: mio::Token,
interests: mio::Interest,
) -> std::io::Result<()> {
SourceFd(&self.state.wayland_fd).reregister(registry, token, interests)
}
fn deregister(&mut self, registry: &mio::Registry) -> std::io::Result<()> {
SourceFd(&self.state.wayland_fd).deregister(registry)
}
}
impl WaylandEventProducer {
fn read(&mut self) -> bool { fn read(&mut self) -> bool {
match self.state.read_guard.take().unwrap().read() { match self.state.read_guard.take().unwrap().read() {
Ok(_) => true, Ok(_) => true,
@@ -381,46 +528,91 @@ impl WaylandEventProducer {
Err(e) => match e { Err(e) => match e {
WaylandError::Io(e) => { WaylandError::Io(e) => {
log::error!("error writing to wayland socket: {e}") log::error!("error writing to wayland socket: {e}")
}, }
WaylandError::Protocol(e) => { WaylandError::Protocol(e) => {
panic!("wayland protocol violation: {e}") panic!("wayland protocol violation: {e}")
}, }
}, },
} }
} }
} }
impl EventProducer for WaylandEventProducer { impl EventProducer for WaylandEventProducer {
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
// read events
while self.read() {
// prepare next read
self.prepare_read();
}
// dispatch the events
self.dispatch_events();
// flush outgoing events
self.flush_events();
// prepare for the next read
self.prepare_read();
// return the events
self.state.pending_events.drain(..)
}
fn notify(&mut self, client_event: ClientEvent) { fn notify(&mut self, client_event: ClientEvent) {
if let ClientEvent::Create(handle, pos) = client_event { match client_event {
self.state.add_client(handle, pos); ClientEvent::Create(handle, pos) => {
self.flush_events(); self.0.get_mut().state.add_client(handle, pos);
}
ClientEvent::Destroy(handle) => {
let inner = self.0.get_mut();
loop {
// remove all windows corresponding to this client
if let Some(i) = inner
.state
.client_for_window
.iter()
.position(|(_, c)| *c == handle)
{
inner.state.client_for_window.remove(i);
inner.state.focused = None;
} else {
break;
}
}
}
} }
let inner = self.0.get_mut();
inner.flush_events();
} }
fn release(&mut self) { fn release(&mut self) {
self.state.ungrab(); let inner = self.0.get_mut();
self.flush_events(); inner.state.ungrab();
inner.flush_events();
}
}
impl Stream for WaylandEventProducer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
log::trace!("producer.next()");
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
loop {
let mut guard = ready!(self.0.poll_read_ready_mut(cx))?;
{
let inner = guard.get_inner_mut();
// read events
while inner.read() {
// prepare next read
inner.prepare_read();
}
// dispatch the events
inner.dispatch_events();
// flush outgoing events
inner.flush_events();
// prepare for the next read
inner.prepare_read();
}
// clear read readiness for tokio read guard
// guard.clear_ready_matching(Ready::READABLE);
guard.clear_ready();
// if an event has been queued during dispatch_events() we return it
match guard.get_inner_mut().state.pending_events.pop_front() {
Some(event) => return Poll::Ready(Some(Ok(event))),
None => continue,
}
}
} }
} }
@@ -456,7 +648,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
_: &Connection, _: &Connection,
qh: &QueueHandle<Self>, qh: &QueueHandle<Self>,
) { ) {
match event { match event {
wl_pointer::Event::Enter { wl_pointer::Event::Enter {
serial, serial,
@@ -465,26 +656,26 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
surface_y: _, surface_y: _,
} => { } => {
// get client corresponding to the focused surface // get client corresponding to the focused surface
log::trace!("produce: enter()");
{ {
let (window, client) = app if let Some((window, client)) = app
.client_for_window .client_for_window
.iter() .iter()
.find(|(w, _c)| w.surface == surface) .find(|(w, _c)| w.surface == surface)
.unwrap(); {
app.focused = Some((window.clone(), *client)); app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial.clone(), qh); app.grab(&surface, pointer, serial.clone(), qh);
} else {
return;
}
} }
let (_, client) = app let (_, client) = app
.client_for_window .client_for_window
.iter() .iter()
.find(|(w, _c)| w.surface == surface) .find(|(w, _c)| w.surface == surface)
.unwrap(); .unwrap();
app.pending_events.push((*client, Event::Release())); app.pending_events.push_back((*client, Event::Release()));
} }
wl_pointer::Event::Leave { .. } => { wl_pointer::Event::Leave { .. } => {
log::trace!("produce: leave()");
app.ungrab(); app.ungrab();
} }
wl_pointer::Event::Button { wl_pointer::Event::Button {
@@ -493,9 +684,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
button, button,
state, state,
} => { } => {
log::trace!("produce: button()");
let (_, client) = app.focused.as_ref().unwrap(); let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push(( app.pending_events.push_back((
*client, *client,
Event::Pointer(PointerEvent::Button { Event::Pointer(PointerEvent::Button {
time, time,
@@ -505,9 +695,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
)); ));
} }
wl_pointer::Event::Axis { time, axis, value } => { wl_pointer::Event::Axis { time, axis, value } => {
log::trace!("produce: scroll()");
let (_, client) = app.focused.as_ref().unwrap(); let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push(( app.pending_events.push_back((
*client, *client,
Event::Pointer(PointerEvent::Axis { Event::Pointer(PointerEvent::Axis {
time, time,
@@ -517,12 +706,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
)); ));
} }
wl_pointer::Event::Frame {} => { wl_pointer::Event::Frame {} => {
log::trace!("produce: frame()"); // TODO properly handle frame events
let (_, client) = app.focused.as_ref().unwrap(); // we simply insert a frame event on the client side
app.pending_events.push(( // after each event for now
*client,
Event::Pointer(PointerEvent::Frame {}),
));
} }
_ => {} _ => {}
} }
@@ -550,7 +736,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
state, state,
} => { } => {
if let Some(client) = client { if let Some(client) = client {
app.pending_events.push(( app.pending_events.push_back((
*client, *client,
Event::Keyboard(KeyboardEvent::Key { Event::Keyboard(KeyboardEvent::Key {
time, time,
@@ -568,7 +754,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
group, group,
} => { } => {
if let Some(client) = client { if let Some(client) = client {
app.pending_events.push(( app.pending_events.push_back((
*client, *client,
Event::Keyboard(KeyboardEvent::Modifiers { Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed, mods_depressed,
@@ -615,10 +801,9 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
dy_unaccel: surface_y, dy_unaccel: surface_y,
} = event } = event
{ {
log::trace!("produce: motion()");
if let Some((_window, client)) = &app.focused { if let Some((_window, client)) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32; let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push(( app.pending_events.push_back((
*client, *client,
Event::Pointer(PointerEvent::Motion { Event::Pointer(PointerEvent::Motion {
time, time,
@@ -641,23 +826,23 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
_: &QueueHandle<Self>, _: &QueueHandle<Self>,
) { ) {
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event { if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
let (window, _client) = app if let Some((window, _client)) = app
.client_for_window .client_for_window
.iter() .iter()
.find(|(w, _c)| &w.layer_surface == layer_surface) .find(|(w, _c)| &w.layer_surface == layer_surface)
.unwrap(); {
// client corresponding to the layer_surface // client corresponding to the layer_surface
let surface = &window.surface; let surface = &window.surface;
let buffer = &window.buffer; let buffer = &window.buffer;
surface.attach(Some(&buffer), 0, 0); surface.attach(Some(&buffer), 0, 0);
layer_surface.ack_configure(serial); layer_surface.ack_configure(serial);
surface.commit(); surface.commit();
}
} }
} }
} }
// delegate wl_registry events to App itself // delegate wl_registry events to App itself
// delegate_dispatch!(App: [wl_registry::WlRegistry: GlobalListContents] => App);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State { impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event( fn event(
_state: &mut Self, _state: &mut Self,
@@ -670,6 +855,72 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
} }
} }
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_registry::Event::Global {
name,
interface,
version: _,
} => match interface.as_str() {
"wl_output" => {
log::debug!("wl_output global");
state
.g
.outputs
.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
}
_ => {}
},
wl_registry::Event::GlobalRemove { .. } => {}
_ => {}
}
}
}
impl Dispatch<ZxdgOutputV1, WlOutput> for State {
fn event(
state: &mut Self,
_: &ZxdgOutputV1,
event: <ZxdgOutputV1 as wayland_client::Proxy>::Event,
wl_output: &WlOutput,
_: &Connection,
_: &QueueHandle<Self>,
) {
log::debug!("xdg-output - {event:?}");
let output_info = match state.output_info.iter_mut().find(|(o, _)| o == wl_output) {
Some((_, c)) => c,
None => {
let output_info = OutputInfo::new();
state.output_info.push((wl_output.clone(), output_info));
&mut state.output_info.last_mut().unwrap().1
}
};
match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => {
output_info.position = (x, y);
}
zxdg_output_v1::Event::LogicalSize { width, height } => {
output_info.size = (width, height);
}
zxdg_output_v1::Event::Done => {}
zxdg_output_v1::Event::Name { name } => {
output_info.name = name;
}
zxdg_output_v1::Event::Description { .. } => {}
_ => {}
}
}
}
// don't emit any events // don't emit any events
delegate_noop!(State: wl_region::WlRegion); delegate_noop!(State: wl_region::WlRegion);
delegate_noop!(State: wl_shm_pool::WlShmPool); delegate_noop!(State: wl_shm_pool::WlShmPool);
@@ -680,6 +931,8 @@ delegate_noop!(State: ZwpKeyboardShortcutsInhibitManagerV1);
delegate_noop!(State: ZwpPointerConstraintsV1); delegate_noop!(State: ZwpPointerConstraintsV1);
// ignore events // ignore events
delegate_noop!(State: ignore wl_output::WlOutput);
delegate_noop!(State: ignore ZxdgOutputManagerV1);
delegate_noop!(State: ignore wl_shm::WlShm); delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_buffer::WlBuffer); delegate_noop!(State: ignore wl_buffer::WlBuffer);
delegate_noop!(State: ignore wl_surface::WlSurface); delegate_noop!(State: ignore wl_surface::WlSurface);

View File

@@ -1,58 +1,31 @@
use std::vec::Drain; use core::task::{Context, Poll};
use futures::Stream;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result; use std::io::Result;
use std::pin::Pin;
use crate::{ use crate::{
client::{ClientHandle, ClientEvent}, client::{ClientEvent, ClientHandle},
event::Event, event::Event,
producer::EventProducer, producer::EventProducer,
}; };
pub struct WindowsProducer { pub struct WindowsProducer {}
pending_events: Vec<(ClientHandle, Event)>,
}
impl Source for WindowsProducer {
fn register(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn reregister(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn deregister(&mut self, _registry: &Registry) -> Result<()> {
Ok(())
}
}
impl EventProducer for WindowsProducer { impl EventProducer for WindowsProducer {
fn notify(&mut self, _: ClientEvent) { } fn notify(&mut self, _: ClientEvent) {}
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> { fn release(&mut self) {}
self.pending_events.drain(..)
}
fn release(&mut self) { }
} }
impl WindowsProducer { impl WindowsProducer {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self { Self {}
pending_events: vec![], }
} }
impl Stream for WindowsProducer {
type Item = Result<(ClientHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
} }
} }

View File

@@ -1,55 +1,34 @@
use std::vec::Drain; use std::io;
use std::task::Poll;
use mio::{Token, Registry}; use futures_core::Stream;
use mio::event::Source;
use std::io::Result;
use crate::event::Event;
use crate::producer::EventProducer; use crate::producer::EventProducer;
use crate::{client::{ClientHandle, ClientEvent}, event::Event}; use crate::client::{ClientEvent, ClientHandle};
pub struct X11Producer { pub struct X11Producer {}
pending_events: Vec<(ClientHandle, Event)>,
}
impl X11Producer { impl X11Producer {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {}
pending_events: vec![],
}
}
}
impl Source for X11Producer {
fn register(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn reregister(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn deregister(&mut self, _registry: &Registry) -> Result<()> {
Ok(())
} }
} }
impl EventProducer for X11Producer { impl EventProducer for X11Producer {
fn notify(&mut self, _: ClientEvent) { } fn notify(&mut self, _: ClientEvent) {}
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
self.pending_events.drain(..)
}
fn release(&mut self) {} fn release(&mut self) {}
} }
impl Stream for X11Producer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

@@ -1,6 +1,11 @@
use std::{net::SocketAddr, collections::{HashSet, hash_set::Iter}, fmt::Display, time::{Instant, Duration}, iter::Cloned}; use std::{
collections::HashSet,
fmt::Display,
net::{IpAddr, SocketAddr},
time::Instant,
};
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position { pub enum Position {
@@ -9,20 +14,44 @@ pub enum Position {
Top, Top,
Bottom, Bottom,
} }
impl Default for Position {
fn default() -> Self {
Self::Left
}
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
Position::Left => Self::Right,
Position::Right => Self::Left,
Position::Top => Self::Bottom,
Position::Bottom => Self::Top,
}
}
}
impl Display for Position { impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self { write!(
Position::Left => "left", f,
Position::Right => "right", "{}",
Position::Top => "top", match self {
Position::Bottom => "bottom", Position::Left => "left",
}) Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
}
)
} }
} }
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client { pub struct Client {
/// handle to refer to the client. /// hostname of this client
pub hostname: Option<String>,
/// unique handle to refer to the client.
/// This way any event consumer / producer backend does not /// This way any event consumer / producer backend does not
/// need to know anything about a client other than its handle. /// need to know anything about a client other than its handle.
pub handle: ClientHandle, pub handle: ClientHandle,
@@ -34,6 +63,8 @@ pub struct Client {
/// e.g. Laptops usually have at least an ethernet and a wifi port /// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses /// which have different ip addresses
pub addrs: HashSet<SocketAddr>, pub addrs: HashSet<SocketAddr>,
/// 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 /// position of a client on screen
pub pos: Position, pub pos: Position,
} }
@@ -41,150 +72,122 @@ pub struct Client {
pub enum ClientEvent { pub enum ClientEvent {
Create(ClientHandle, Position), Create(ClientHandle, Position),
Destroy(ClientHandle), Destroy(ClientHandle),
UpdatePos(ClientHandle, Position),
AddAddr(ClientHandle, SocketAddr),
RemoveAddr(ClientHandle, SocketAddr),
} }
pub type ClientHandle = u32; pub type ClientHandle = u32;
#[derive(Debug, Clone)]
pub struct ClientState {
pub client: Client,
pub active: bool,
pub last_ping: Option<Instant>,
pub last_seen: Option<Instant>,
pub last_replied: Option<Instant>,
}
pub struct ClientManager { pub struct ClientManager {
/// probably not beneficial to use a hashmap here clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
clients: Vec<Client>,
last_ping: Vec<(ClientHandle, Option<Instant>)>,
last_seen: Vec<(ClientHandle, Option<Instant>)>,
last_replied: Vec<(ClientHandle, Option<Instant>)>,
next_client_id: u32,
} }
impl ClientManager { impl ClientManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { clients: vec![] }
clients: vec![],
next_client_id: 0,
last_ping: vec![],
last_seen: vec![],
last_replied: vec![],
}
} }
/// add a new client to this manager /// add a new client to this manager
pub fn add_client(&mut self, addrs: HashSet<SocketAddr>, pos: Position) -> ClientHandle { pub fn add_client(
let handle = self.next_id(); &mut self,
hostname: Option<String>,
addrs: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
// we dont know, which IP is initially active // we dont know, which IP is initially active
let active_addr = None; let active_addr = None;
// map ip addresses to socket addresses
let addrs = HashSet::from_iter(addrs.into_iter().map(|ip| SocketAddr::new(ip, port)));
// store the client // store the client
let client = Client { handle, active_addr, addrs, pos }; let client = Client {
self.clients.push(client); hostname,
self.last_ping.push((handle, None)); handle,
self.last_seen.push((handle, None)); active_addr,
self.last_replied.push((handle, None)); addrs,
port,
pos,
};
// client was never seen, nor pinged
let client_state = ClientState {
client,
last_ping: None,
last_seen: None,
last_replied: None,
active: false,
};
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 handle
} }
/// add a socket address to the given client /// find a client by its address
pub fn add_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.addrs.insert(addr);
}
}
/// remove socket address from the given client
pub fn remove_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.addrs.remove(&addr);
}
}
pub fn set_default_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.active_addr = Some(addr)
}
}
/// update the position of a client
pub fn update_pos(&mut self, client: ClientHandle, pos: Position) {
if let Some(client) = self.get_mut(client) {
client.pos = pos;
}
}
pub fn get_active_addr(&self, client: ClientHandle) -> Option<SocketAddr> {
self.get(client)?.active_addr
}
pub fn get_addrs(&self, client: ClientHandle) -> Option<Cloned<Iter<'_, SocketAddr>>> {
Some(self.get(client)?.addrs.iter().cloned())
}
pub fn last_ping(&self, client: ClientHandle) -> Option<Duration> {
let last_ping = self.last_ping
.iter()
.find(|(c,_)| *c == client)
.unwrap().1;
last_ping.map(|p| p.elapsed())
}
pub fn last_seen(&self, client: ClientHandle) -> Option<Duration> {
let last_seen = self.last_seen
.iter()
.find(|(c, _)| *c == client)
.unwrap().1;
last_seen.map(|t| t.elapsed())
}
pub fn last_replied(&self, client: ClientHandle) -> Option<Duration> {
let last_replied = self.last_replied
.iter()
.find(|(c, _)| *c == client)
.unwrap().1;
last_replied.map(|t| t.elapsed())
}
pub fn reset_last_ping(&mut self, client: ClientHandle) {
self.last_ping
.iter_mut()
.find(|(c, _)| *c == client)
.unwrap().1 = Some(Instant::now());
}
pub fn reset_last_seen(&mut self, client: ClientHandle) {
self.last_seen
.iter_mut()
.find(|(c, _)| *c == client)
.unwrap().1 = Some(Instant::now());
}
pub fn reset_last_replied(&mut self, client: ClientHandle) {
self.last_replied
.iter_mut()
.find(|(c, _)| *c == client)
.unwrap().1 = Some(Instant::now());
}
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> { pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
// since there shouldn't be more than a handful of clients at any given
// time this is likely faster than using a HashMap
self.clients self.clients
.iter() .iter()
.find(|c| c.addrs.contains(&addr)) .position(|c| {
.map(|c| c.handle) if let Some(c) = c {
c.active && c.client.addrs.contains(&addr)
} else {
false
}
})
.map(|p| p as ClientHandle)
} }
fn next_id(&mut self) -> ClientHandle { /// remove a client from the list
let handle = self.next_client_id; pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientState> {
self.next_client_id += 1; // remove id from occupied ids
handle self.clients.get_mut(client as usize)?.take()
} }
fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a Client> { /// 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");
}
// returns an immutable reference to the client state corresponding to `client`
pub fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a ClientState> {
self.clients.get(client as usize)?.as_ref()
}
/// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut ClientState> {
self.clients.get_mut(client as usize)?.as_mut()
}
pub fn enumerate(&self) -> Vec<(Client, bool)> {
self.clients self.clients
.iter() .iter()
.find(|c| c.handle == client) .filter_map(|s| s.as_ref())
} .map(|s| (s.client.clone(), s.active))
.collect()
fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut Client> {
self.clients
.iter_mut()
.find(|c| c.handle == client)
} }
} }

View File

@@ -1,14 +1,13 @@
use anyhow::Result;
use clap::Parser;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use core::fmt;
use std::collections::HashSet; use std::collections::HashSet;
use std::net::{IpAddr, SocketAddr};
use std::{error::Error, fs};
use std::env; use std::env;
use std::net::IpAddr;
use std::{error::Error, fs};
use toml; use toml;
use crate::client::Position; use crate::client::Position;
use crate::dns;
pub const DEFAULT_PORT: u16 = 4242; pub const DEFAULT_PORT: u16 = 4242;
@@ -29,65 +28,79 @@ pub struct Client {
pub port: Option<u16>, pub port: Option<u16>,
} }
#[derive(Debug, Clone)]
struct MissingParameter {
arg: &'static str,
}
impl fmt::Display for MissingParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Missing a parameter for argument: {}", self.arg)
}
}
impl Error for MissingParameter {}
impl ConfigToml { impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> { pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
let config = fs::read_to_string(path)?; let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?) Ok(toml::from_str::<_>(&config)?)
} }
} }
fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> { #[derive(Parser, Debug)]
let args: Vec<String> = env::args().collect(); #[command(author, version, about, long_about = None)]
struct CliArgs {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
for (i, arg) in args.iter().enumerate() { /// the frontend to use [cli | gtk]
if arg != key { #[arg(short, long)]
continue; frontend: Option<String>,
}
match args.get(i+1) { /// non-default config file location
None => return Err(MissingParameter { arg: key }), #[arg(short, long)]
Some(arg) => return Ok(Some(arg.clone())), config: Option<String>,
};
} /// run only the service as a daemon without the frontend
Ok(None) #[arg(short, long)]
daemon: bool,
} }
#[derive(Debug, PartialEq, Eq)]
pub enum Frontend { pub enum Frontend {
Gtk, Gtk,
Cli, Cli,
} }
#[derive(Debug)]
pub struct Config { pub struct Config {
pub frontend: Frontend, pub frontend: Frontend,
pub port: u16, pub port: u16,
pub clients: Vec<(Client, Position)>, pub clients: Vec<(Client, Position)>,
pub daemon: bool,
} }
impl Config { impl Config {
pub fn new() -> Result<Self, Box<dyn Error>> { pub fn new() -> Result<Self> {
let config_path = "config.toml"; let args = CliArgs::parse();
let config_toml = match ConfigToml::new(config_path) { let config_file = "config.toml";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/{config_file}")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\{config_file}")
};
// --config <file> overrules default location
let config_path = args.config.unwrap_or(config_path);
let config_toml = match ConfigToml::new(config_path.as_str()) {
Err(e) => { Err(e) => {
log::error!("config.toml: {e}"); log::error!("{config_path}: {e}");
log::warn!("Continuing without config file ..."); log::warn!("Continuing without config file ...");
None None
}, }
Ok(c) => Some(c), Ok(c) => Some(c),
}; };
let frontend = match find_arg("--frontend")? { let frontend = match args.frontend {
None => match &config_toml { None => match &config_toml {
Some(c) => c.frontend.clone(), Some(c) => c.frontend.clone(),
None => None, None => None,
@@ -96,20 +109,23 @@ impl Config {
}; };
let frontend = match frontend { let frontend = match frontend {
#[cfg(all(unix, feature = "gtk"))]
None => Frontend::Gtk,
#[cfg(any(not(feature = "gtk"), not(unix)))]
None => Frontend::Cli, None => Frontend::Cli,
Some(s) => match s.as_str() { Some(s) => match s.as_str() {
"cli" => Frontend::Cli, "cli" => Frontend::Cli,
"gtk" => Frontend::Gtk, "gtk" => Frontend::Gtk,
_ => Frontend::Cli, _ => Frontend::Cli,
} },
}; };
let port = match find_arg("--port")? { let port = match args.port {
Some(port) => port.parse::<u16>()?, Some(port) => port,
None => match &config_toml { None => match &config_toml {
Some(c) => c.port.unwrap_or(DEFAULT_PORT), Some(c) => c.port.unwrap_or(DEFAULT_PORT),
None => DEFAULT_PORT, None => DEFAULT_PORT,
} },
}; };
let mut clients: Vec<(Client, Position)> = vec![]; let mut clients: Vec<(Client, Position)> = vec![];
@@ -129,43 +145,29 @@ impl Config {
} }
} }
Ok(Config { frontend, clients, port }) let daemon = args.daemon;
Ok(Config {
daemon,
frontend,
clients,
port,
})
} }
pub fn get_clients(&self) -> Vec<(HashSet<SocketAddr>, Option<String>, Position)> { pub fn get_clients(&self) -> Vec<(HashSet<IpAddr>, Option<String>, u16, Position)> {
self.clients.iter().map(|(c,p)| { self.clients
let port = c.port.unwrap_or(DEFAULT_PORT); .iter()
// add ips from config .map(|(c, p)| {
let config_ips: Vec<IpAddr> = if let Some(ips) = c.ips.as_ref() { let port = c.port.unwrap_or(DEFAULT_PORT);
ips.iter().cloned().collect() let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
} else { HashSet::from_iter(ips.iter().cloned())
vec![] } else {
}; HashSet::new()
let host_name = c.host_name.clone(); };
// add ips from dns lookup let host_name = c.host_name.clone();
let dns_ips = match host_name.as_ref() { (ips, host_name, port, *p)
None => vec![], })
Some(host_name) => match dns::resolve(host_name) { .collect()
Err(e) => {
log::warn!("{host_name}: could not resolve host: {e}");
vec![]
}
Ok(l) if l.is_empty() => {
log::warn!("{host_name}: could not resolve host");
vec![]
}
Ok(l) => l,
}
};
if config_ips.is_empty() && dns_ips.is_empty() {
log::error!("no ips found for client {p:?}, ignoring!");
log::error!("You can manually specify ip addresses via the `ips` config option");
}
let ips = config_ips.into_iter().chain(dns_ips.into_iter());
// map ip addresses to socket addresses
let addrs: HashSet<SocketAddr> = ips.map(|ip| SocketAddr::new(ip, port)).collect();
(addrs, host_name, *p)
}).filter(|(a, _, _)| !a.is_empty()).collect()
} }
} }

View File

@@ -1,10 +1,17 @@
#[cfg(unix)] use async_trait::async_trait;
use std::future;
#[cfg(all(unix, not(target_os = "macos")))]
use std::env; use std::env;
use crate::{
backend::consumer,
client::{ClientEvent, ClientHandle},
event::Event,
};
use anyhow::Result; use anyhow::Result;
use crate::{backend::consumer, client::{ClientHandle, ClientEvent}, event::Event};
#[cfg(unix)] #[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug)] #[derive(Debug)]
enum Backend { enum Backend {
Wlroots, Wlroots,
@@ -13,19 +20,27 @@ enum Backend {
Libei, Libei,
} }
pub trait EventConsumer { #[async_trait]
/// Event corresponding to an abstract `client_handle` pub trait EventConsumer: Send {
fn consume(&self, event: Event, client_handle: ClientHandle); async fn consume(&mut self, event: Event, client_handle: ClientHandle);
async fn notify(&mut self, client_event: ClientEvent);
/// this function is waited on continuously and can be used to handle events
async fn dispatch(&mut self) -> Result<()> {
let _: () = future::pending().await;
Ok(())
}
/// Event corresponding to a configuration change async fn destroy(&mut self);
fn notify(&mut self, client_event: ClientEvent);
} }
pub fn create() -> Result<Box<dyn EventConsumer>> { pub async fn create() -> Result<Box<dyn EventConsumer>> {
#[cfg(windows)] #[cfg(windows)]
return Ok(Box::new(consumer::windows::WindowsConsumer::new())); return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
#[cfg(unix)] #[cfg(target_os = "macos")]
return Ok(Box::new(consumer::macos::MacOSConsumer::new()?));
#[cfg(all(unix, not(target_os = "macos")))]
let backend = match env::var("XDG_SESSION_TYPE") { let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() { Ok(session_type) => match session_type.as_str() {
"x11" => { "x11" => {
@@ -36,12 +51,14 @@ pub fn create() -> Result<Box<dyn EventConsumer>> {
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event consumer"); log::info!("XDG_SESSION_TYPE = wayland -> using wayland event consumer");
match env::var("XDG_CURRENT_DESKTOP") { match env::var("XDG_CURRENT_DESKTOP") {
Ok(current_desktop) => match current_desktop.as_str() { Ok(current_desktop) => match current_desktop.as_str() {
"gnome" => { "GNOME" => {
log::info!("XDG_CURRENT_DESKTOP = gnome -> using libei backend"); log::info!("XDG_CURRENT_DESKTOP = GNOME -> using libei backend");
Backend::Libei Backend::Libei
} }
"KDE" => { "KDE" => {
log::info!("XDG_CURRENT_DESKTOP = KDE -> using xdg_desktop_portal backend"); log::info!(
"XDG_CURRENT_DESKTOP = KDE -> using xdg_desktop_portal backend"
);
Backend::RemoteDesktopPortal Backend::RemoteDesktopPortal
} }
"sway" => { "sway" => {
@@ -53,10 +70,12 @@ pub fn create() -> Result<Box<dyn EventConsumer>> {
Backend::Wlroots Backend::Wlroots
} }
_ => { _ => {
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend"); log::warn!(
"unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend"
);
Backend::Wlroots Backend::Wlroots
} }
} },
// default to wlroots backend for now // default to wlroots backend for now
_ => { _ => {
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend"); log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
@@ -66,34 +85,38 @@ pub fn create() -> Result<Box<dyn EventConsumer>> {
} }
_ => panic!("unknown XDG_SESSION_TYPE"), _ => panic!("unknown XDG_SESSION_TYPE"),
}, },
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"), Err(_) => {
panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!")
}
}; };
#[cfg(unix)] #[cfg(all(unix, not(target_os = "macos")))]
match backend { match backend {
Backend::Libei => { Backend::Libei => {
#[cfg(not(feature = "libei"))] #[cfg(not(feature = "libei"))]
panic!("feature libei not enabled"); panic!("feature libei not enabled");
#[cfg(feature = "libei")] #[cfg(feature = "libei")]
Ok(Box::new(consumer::libei::LibeiConsumer::new())) Ok(Box::new(consumer::libei::LibeiConsumer::new().await?))
}, }
Backend::RemoteDesktopPortal => { Backend::RemoteDesktopPortal => {
#[cfg(not(feature = "xdg_desktop_portal"))] #[cfg(not(feature = "xdg_desktop_portal"))]
panic!("feature xdg_desktop_portal not enabled"); panic!("feature xdg_desktop_portal not enabled");
#[cfg(feature = "xdg_desktop_portal")] #[cfg(feature = "xdg_desktop_portal")]
Ok(Box::new(consumer::xdg_desktop_portal::DesktopPortalConsumer::new())) Ok(Box::new(
}, consumer::xdg_desktop_portal::DesktopPortalConsumer::new().await?,
))
}
Backend::Wlroots => { Backend::Wlroots => {
#[cfg(not(feature = "wayland"))] #[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled"); panic!("feature wayland not enabled");
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
Ok(Box::new(consumer::wlroots::WlrootsConsumer::new()?)) Ok(Box::new(consumer::wlroots::WlrootsConsumer::new()?))
}, }
Backend::X11 => { Backend::X11 => {
#[cfg(not(feature = "x11"))] #[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled"); panic!("feature x11 not enabled");
#[cfg(feature = "x11")] #[cfg(feature = "x11")]
Ok(Box::new(consumer::x11::X11Consumer::new())) Ok(Box::new(consumer::x11::X11Consumer::new()))
}, }
} }
} }

View File

@@ -1,9 +1,23 @@
use anyhow::Result;
use std::{error::Error, net::IpAddr}; use std::{error::Error, net::IpAddr};
use trust_dns_resolver::Resolver; use trust_dns_resolver::TokioAsyncResolver;
pub fn resolve(host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> { pub(crate) struct DnsResolver {
log::info!("resolving {host} ..."); resolver: TokioAsyncResolver,
let response = Resolver::from_system_conf()?.lookup_ip(host)?; }
Ok(response.iter().collect()) impl DnsResolver {
pub(crate) async fn new() -> Result<Self> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
Ok(Self { resolver })
}
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = self.resolver.lookup_ip(host).await?;
for ip in response.iter() {
log::info!("{host}: adding ip {ip}");
}
Ok(response.iter().collect())
}
} }

View File

@@ -1,6 +1,12 @@
use std::{error::Error, fmt::{self, Display}}; use std::{
error::Error,
fmt::{self, Display},
};
pub mod server; // FIXME
pub(crate) const BTN_LEFT: u32 = 0x110;
pub(crate) const BTN_RIGHT: u32 = 0x111;
pub(crate) const BTN_MIDDLE: u32 = 0x112;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum PointerEvent { pub enum PointerEvent {
@@ -49,10 +55,22 @@ pub enum Event {
impl Display for PointerEvent { impl Display for PointerEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
PointerEvent::Motion { time: _ , relative_x, relative_y } => write!(f, "motion({relative_x},{relative_y})"), PointerEvent::Motion {
PointerEvent::Button { time: _ , button, state } => write!(f, "button({button}, {state})"), time: _,
PointerEvent::Axis { time: _, axis, value } => write!(f, "scroll({axis}, {value})"), relative_x,
PointerEvent::Frame { } => write!(f, "frame()"), relative_y,
} => write!(f, "motion({relative_x},{relative_y})"),
PointerEvent::Button {
time: _,
button,
state,
} => write!(f, "button({button}, {state})"),
PointerEvent::Axis {
time: _,
axis,
value,
} => write!(f, "scroll({axis}, {value})"),
PointerEvent::Frame {} => write!(f, "frame()"),
} }
} }
} }
@@ -60,8 +78,20 @@ impl Display for PointerEvent {
impl Display for KeyboardEvent { impl Display for KeyboardEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
KeyboardEvent::Key { time: _, key, state } => write!(f, "key({key}, {state})"), KeyboardEvent::Key {
KeyboardEvent::Modifiers { mods_depressed, mods_latched, mods_locked, group } => write!(f, "modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"), time: _,
key,
state,
} => write!(f, "key({key}, {state})"),
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => write!(
f,
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
),
} }
} }
} }

View File

@@ -1,282 +0,0 @@
use std::{error::Error, io::Result, collections::HashSet, time::Duration};
use log;
use mio::{Events, Poll, Interest, Token, net::UdpSocket};
#[cfg(not(windows))]
use mio_signals::{Signals, Signal, SignalSet};
use std::{net::SocketAddr, io::ErrorKind};
use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}};
use super::Event;
/// keeps track of state to prevent a feedback loop
/// of continuously sending and receiving the same event.
#[derive(Eq, PartialEq)]
enum State {
Sending,
Receiving,
}
pub struct Server {
poll: Poll,
socket: UdpSocket,
producer: Box<dyn EventProducer>,
consumer: Box<dyn EventConsumer>,
#[cfg(not(windows))]
signals: Signals,
frontend: FrontendAdapter,
client_manager: ClientManager,
state: State,
}
const UDP_RX: Token = Token(0);
const FRONTEND_RX: Token = Token(1);
const PRODUCER_RX: Token = Token(2);
#[cfg(not(windows))]
const SIGNAL: Token = Token(3);
impl Server {
pub fn new(
port: u16,
mut producer: Box<dyn EventProducer>,
consumer: Box<dyn EventConsumer>,
mut frontend: FrontendAdapter,
) -> Result<Self> {
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
let mut socket = UdpSocket::bind(listen_addr)?;
// register event sources
let poll = Poll::new()?;
// hand signal handling over to the event loop
#[cfg(not(windows))]
let mut signals = Signals::new(SignalSet::all())?;
#[cfg(not(windows))]
poll.registry().register(&mut signals, SIGNAL, Interest::READABLE)?;
poll.registry().register(&mut socket, UDP_RX, Interest::READABLE | Interest::WRITABLE)?;
poll.registry().register(&mut producer, PRODUCER_RX, Interest::READABLE)?;
poll.registry().register(&mut frontend, FRONTEND_RX, Interest::READABLE)?;
// create client manager
let client_manager = ClientManager::new();
Ok(Server {
poll, socket, consumer, producer,
#[cfg(not(windows))]
signals, frontend,
client_manager,
state: State::Receiving,
})
}
pub fn run(&mut self) -> Result<()> {
let mut events = Events::with_capacity(10);
loop {
match self.poll.poll(&mut events, None) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
for event in &events {
if !event.is_readable() { continue }
match event.token() {
UDP_RX => self.handle_udp_rx(),
PRODUCER_RX => self.handle_producer_rx(),
FRONTEND_RX => if self.handle_frontend_rx() { return Ok(()) },
#[cfg(not(windows))]
SIGNAL => if self.handle_signal() { return Ok(()) },
_ => panic!("what happened here?")
}
}
}
}
pub fn add_client(&mut self, addr: HashSet<SocketAddr>, pos: Position) {
let client = self.client_manager.add_client(addr, pos);
self.producer.notify(ClientEvent::Create(client, pos));
self.consumer.notify(ClientEvent::Create(client, pos));
}
fn handle_udp_rx(&mut self) {
loop {
let (event, addr) = match self.receive_event() {
Ok(e) => e,
Err(e) => {
if e.is::<std::io::Error>() {
if let ErrorKind::WouldBlock = e.downcast_ref::<std::io::Error>()
.unwrap()
.kind() {
return
}
}
log::error!("{}", e);
continue
}
};
log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
// get handle for addr
let handle = match self.client_manager.get_client(addr) {
Some(a) => a,
None => {
log::warn!("ignoring event from client {addr:?}");
continue
}
};
// reset ttl for client and set addr as new default for this client
self.client_manager.reset_last_seen(handle);
self.client_manager.set_default_addr(handle, addr);
match (event, addr) {
(Event::Pong(), _) => {},
(Event::Ping(), addr) => {
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
log::error!("udp send: {}", e);
}
}
(event, addr) => {
match self.state {
State::Sending => {
// in sending state, we dont want to process
// any events to avoid feedback loops,
// therefore we tell the event producer
// to release the pointer and move on
// first event -> release pointer
if let Event::Release() = event {
log::debug!("releasing pointer ...");
self.producer.release();
self.state = State::Receiving;
}
}
State::Receiving => {
// consume event
self.consumer.consume(event, handle);
// let the server know we are still alive once every second
let last_replied = self.client_manager.last_replied(handle);
if last_replied.is_none()
|| last_replied.is_some() && last_replied.unwrap() > Duration::from_secs(1) {
self.client_manager.reset_last_replied(handle);
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
log::error!("udp send: {}", e);
}
}
}
}
}
}
}
}
fn handle_producer_rx(&mut self) {
let events = self.producer.read_events();
for (c, e) in events.into_iter() {
// in receiving state, only release events
// must be transmitted
if let Event::Release() = e {
self.state = State::Sending;
}
// otherwise we should have an address to send to
// transmit events to the corrensponding client
if let Some(addr) = self.client_manager.get_active_addr(c) {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
if let Err(e) = Self::send_event(&self.socket, e, addr) {
log::error!("udp send: {}", e);
}
}
// if client last responded > 2 seconds ago
// and we have not sent a ping since 500 milliseconds,
// send a ping
let last_seen = self.client_manager.last_seen(c);
let last_ping = self.client_manager.last_ping(c);
if last_seen.is_some() && last_seen.unwrap() < Duration::from_secs(2) {
continue
}
// client last seen > 500ms ago
if last_ping.is_some() && last_ping.unwrap() < Duration::from_millis(500) {
continue
}
// last ping > 500ms ago -> ping all interfaces
self.client_manager.reset_last_ping(c);
if let Some(iter) = self.client_manager.get_addrs(c) {
for addr in iter {
log::debug!("pinging {addr}");
if let Err(e) = Self::send_event(&self.socket, Event::Ping(), addr) {
if e.kind() != ErrorKind::WouldBlock {
log::error!("udp send: {}", e);
}
}
}
} else {
// TODO should repeat dns lookup
}
}
}
fn handle_frontend_rx(&mut self) -> bool {
loop {
match self.frontend.read_event() {
Ok(event) => match event {
FrontendEvent::RequestPortChange(_) => todo!(),
FrontendEvent::RequestClientAdd(addr, pos) => {
self.add_client(HashSet::from_iter(&mut [addr].into_iter()), pos);
}
FrontendEvent::RequestClientDelete(_) => todo!(),
FrontendEvent::RequestClientUpdate(_) => todo!(),
FrontendEvent::RequestShutdown() => {
log::info!("terminating gracefully...");
return true;
},
}
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
Err(e) => {
log::error!("frontend: {e}");
}
}
}
}
#[cfg(not(windows))]
fn handle_signal(&mut self) -> bool {
#[cfg(windows)]
return false;
#[cfg(not(windows))]
loop {
match self.signals.receive() {
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
Err(e) => {
log::error!("error reading signal: {e}");
return false;
}
Ok(Some(Signal::Interrupt) | Some(Signal::Terminate)) => {
// terminate on SIG_INT or SIG_TERM
log::info!("terminating gracefully...");
return true;
},
Ok(Some(signal)) => {
log::info!("ignoring signal {signal:?}");
},
Ok(None) => return false,
}
}
}
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
let data: Vec<u8> = (&e).into();
// We are currently abusing a blocking send to get the lowest possible latency.
// It may be better to set the socket to non-blocking and only send when ready.
sock.send_to(&data[..], addr)
}
fn receive_event(&self) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
let mut buf = vec![0u8; 22];
match self.socket.recv_from(&mut buf) {
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
Err(e) => Err(Box::new(e)),
}
}
}

View File

@@ -1,19 +1,31 @@
use std::io::{Read, Result}; use anyhow::{anyhow, Result};
use std::{str, net::SocketAddr}; use std::{cmp::min, io::ErrorKind, str, time::Duration};
#[cfg(unix)] #[cfg(unix)]
use std::{env, path::{Path, PathBuf}}; use std::{
env,
path::{Path, PathBuf},
};
use mio::{Registry, Token, event::Source}; use tokio::io::ReadHalf;
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
#[cfg(unix)] #[cfg(unix)]
use mio::net::UnixListener; use tokio::net::UnixListener;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)] #[cfg(windows)]
use mio::net::TcpListener; use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
use crate::client::{Client, Position}; use crate::{
client::{Client, ClientHandle, Position},
config::{Config, Frontend},
};
/// cli frontend /// cli frontend
pub mod cli; pub mod cli;
@@ -22,94 +34,240 @@ pub mod cli;
#[cfg(all(unix, feature = "gtk"))] #[cfg(all(unix, feature = "gtk"))]
pub mod gtk; pub mod gtk;
pub fn run_frontend(config: &Config) -> Result<()> {
match config.frontend {
#[cfg(all(unix, feature = "gtk"))]
Frontend::Gtk => {
gtk::run();
}
#[cfg(any(not(feature = "gtk"), not(unix)))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
cli::run()?;
}
};
Ok(())
}
fn exponential_back_off(duration: &mut Duration) -> &Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
duration
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
pub fn wait_for_service() -> Result<std::os::unix::net::UnixStream> {
let socket_path = FrontendListener::socket_path()?;
let mut duration = Duration::from_millis(1);
loop {
use std::os::unix::net::UnixStream;
if let Ok(stream) = UnixStream::connect(&socket_path) {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
std::thread::sleep(*exponential_back_off(&mut duration));
}
}
#[cfg(windows)]
pub fn wait_for_service() -> Result<std::net::TcpStream> {
let mut duration = Duration::from_millis(1);
loop {
use std::net::TcpStream;
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
break Ok(stream);
}
std::thread::sleep(*exponential_back_off(&mut duration));
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendEvent { pub enum FrontendEvent {
RequestPortChange(u16), /// add a new client
RequestClientAdd(SocketAddr, Position), AddClient(Option<String>, u16, Position),
RequestClientDelete(Client), /// activate/deactivate client
RequestClientUpdate(Client), ActivateClient(ClientHandle, bool),
RequestShutdown(), /// change the listen port (recreate udp listener)
ChangePort(u16),
/// remove a client
DelClient(ClientHandle),
/// request an enumertaion of all clients
Enumerate(),
/// service shutdown
Shutdown(),
/// update a client (hostname, port, position)
UpdateClient(ClientHandle, Option<String>, u16, Position),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendNotify { pub enum FrontendNotify {
NotifyClientCreate(Client), NotifyClientCreate(ClientHandle, Option<String>, u16, Position),
NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
NotifyClientDelete(ClientHandle),
/// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>),
Enumerate(Vec<(Client, bool)>),
NotifyError(String), NotifyError(String),
} }
pub struct FrontendAdapter { pub struct FrontendListener {
#[cfg(windows)] #[cfg(windows)]
listener: TcpListener, listener: TcpListener,
#[cfg(unix)] #[cfg(unix)]
listener: UnixListener, listener: UnixListener,
#[cfg(unix)] #[cfg(unix)]
socket_path: PathBuf, socket_path: PathBuf,
#[cfg(unix)]
tx_streams: Vec<WriteHalf<UnixStream>>,
#[cfg(windows)]
tx_streams: Vec<WriteHalf<TcpStream>>,
} }
impl FrontendAdapter { impl FrontendListener {
pub fn new() -> std::result::Result<Self, Box<dyn std::error::Error>> { #[cfg(all(unix, not(target_os = "macos")))]
pub fn socket_path() -> Result<PathBuf> {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
};
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn socket_path() -> Result<PathBuf> {
let home = match env::var("HOME") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
};
let home = Path::new(home.as_str());
let path = home
.join("Library")
.join("Caches")
.join("lan-mouse-socket.sock");
Ok(path)
}
pub async fn new() -> Option<Result<Self>> {
#[cfg(unix)] #[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock"); let (socket_path, listener) = {
#[cfg(unix)] let socket_path = match Self::socket_path() {
let listener = UnixListener::bind(&socket_path)?; Ok(path) => path,
Err(e) => return Some(Err(e)),
};
log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() {
// try to connect to see if some other instance
// of lan-mouse is already running
match UnixStream::connect(&socket_path).await {
// connected -> lan-mouse is already running
Ok(_) => return None,
// lan-mouse is not running but a socket was left behind
Err(e) => {
log::debug!("{socket_path:?}: {e} - removing left behind socket");
let _ = std::fs::remove_file(&socket_path);
}
}
}
let listener = match UnixListener::bind(&socket_path) {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
(socket_path, listener)
};
#[cfg(windows)] #[cfg(windows)]
let listener = TcpListener::bind("127.0.0.1:5252".parse().unwrap())?; // abuse tcp let listener = match TcpListener::bind("127.0.0.1:5252").await {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
let adapter = Self { let adapter = Self {
listener, listener,
#[cfg(unix)] #[cfg(unix)]
socket_path, socket_path,
tx_streams: vec![],
}; };
Ok(adapter) Some(Ok(adapter))
} }
pub fn read_event(&mut self) -> Result<FrontendEvent>{ #[cfg(unix)]
let (mut stream, _) = self.listener.accept()?; pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
let mut buf = [0u8; 128]; // FIXME log::trace!("frontend.accept()");
stream.read(&mut buf)?;
let json = str::from_utf8(&buf) let stream = self.listener.accept().await?.0;
.unwrap() let (rx, tx) = tokio::io::split(stream);
.trim_end_matches(char::from(0)); // remove trailing 0-bytes self.tx_streams.push(tx);
let event = serde_json::from_str(json).unwrap(); Ok(rx)
log::debug!("{:?}", event);
Ok(event)
} }
pub fn notify(&self, _event: FrontendNotify) { } #[cfg(windows)]
} pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
let stream = self.listener.accept().await?.0;
impl Source for FrontendAdapter { let (rx, tx) = tokio::io::split(stream);
fn register( self.tx_streams.push(tx);
&mut self, Ok(rx)
registry: &Registry,
token: Token,
interests: mio::Interest,
) -> Result<()> {
self.listener.register(registry, token, interests)
} }
fn reregister( pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
&mut self, // encode event
registry: &Registry, let json = serde_json::to_string(&notify).unwrap();
token: Token, let payload = json.as_bytes();
interests: mio::Interest, let len = payload.len().to_be_bytes();
) -> Result<()> { log::debug!("json: {json}, len: {}", payload.len());
self.listener.reregister(registry, token, interests)
}
fn deregister(&mut self, registry: &Registry) -> Result<()> { let mut keep = vec![];
self.listener.deregister(registry)
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if let Err(_) = tx.write(&len).await {
keep.push(false);
continue;
}
if let Err(_) = tx.write(payload).await {
keep.push(false);
continue;
}
keep.push(true);
}
// could not find a better solution because async
let mut keep = keep.into_iter();
self.tx_streams.retain(|_| keep.next().unwrap());
Ok(())
} }
} }
#[cfg(unix)] #[cfg(unix)]
impl Drop for FrontendAdapter { impl Drop for FrontendListener {
fn drop(&mut self) { fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path); log::debug!("remove socket: {:?}", self.socket_path);
std::fs::remove_file(&self.socket_path).unwrap(); let _ = std::fs::remove_file(&self.socket_path);
} }
} }
pub trait Frontend { } #[cfg(unix)]
pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEvent> {
let len = stream.read_u64().await?;
assert!(len <= 256);
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;
Ok(serde_json::from_slice(&buf[..len as usize])?)
}
#[cfg(windows)]
pub async fn read_event(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendEvent> {
let len = stream.read_u64().await?;
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;
Ok(serde_json::from_slice(&buf[..len as usize])?)
}

View File

@@ -1,85 +1,257 @@
use anyhow::Result; use anyhow::{anyhow, Context, Result};
use std::{thread, io::Write, net::SocketAddr};
#[cfg(windows)] #[cfg(windows)]
use std::net::SocketAddrV4; use std::net::SocketAddrV4;
use std::{
io::{ErrorKind, Read, Write},
str::SplitWhitespace,
thread,
};
#[cfg(unix)]
use std::{os::unix::net::UnixStream, path::Path, env};
#[cfg(windows)] #[cfg(windows)]
use std::net::TcpStream; use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use crate::client::Position; use crate::{client::Position, config::DEFAULT_PORT};
use super::{FrontendEvent, Frontend}; use super::{FrontendEvent, FrontendNotify};
pub struct CliFrontend; pub fn run() -> Result<()> {
#[cfg(unix)]
let socket_path = super::FrontendListener::socket_path()?;
impl Frontend for CliFrontend {} #[cfg(unix)]
let Ok(mut tx) = UnixStream::connect(&socket_path) else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
impl CliFrontend { #[cfg(windows)]
pub fn new() -> Result<CliFrontend> { let Ok(mut tx) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
#[cfg(unix)] return Err(anyhow!("Could not connect to lan-mouse-socket"));
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock"); };
thread::Builder::new()
.name("cli-frontend".to_string()) let mut rx = tx.try_clone()?;
.spawn(move || {
let reader = thread::Builder::new()
.name("cli-frontend".to_string())
.spawn(move || {
// all further prompts
prompt();
loop { loop {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
let mut buf = String::new(); let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) { match std::io::stdin().read_line(&mut buf) {
Ok(0) => break,
Ok(len) => { Ok(len) => {
if let Some(event) = parse_event(buf, len) { if let Some(events) = parse_cmd(buf, len) {
#[cfg(unix)] for event in events.iter() {
let Ok(mut stream) = UnixStream::connect(&socket_path) else { let json = serde_json::to_string(&event).unwrap();
log::error!("Could not connect to lan-mouse-socket"); let bytes = json.as_bytes();
continue; let len = bytes.len().to_be_bytes();
}; if let Err(e) = tx.write(&len) {
#[cfg(windows)] log::error!("error sending message: {e}");
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else { };
log::error!("Could not connect to lan-mouse-server"); if let Err(e) = tx.write(bytes) {
continue; log::error!("error sending message: {e}");
}; };
let json = serde_json::to_string(&event).unwrap(); if *event == FrontendEvent::Shutdown() {
if let Err(e) = stream.write(json.as_bytes()) { return;
log::error!("error sending message: {e}"); }
};
if event == FrontendEvent::RequestShutdown() {
break;
} }
// prompt is printed after the server response is received
} else {
prompt();
} }
} }
Err(e) => { Err(e) => {
log::error!("{e:?}"); log::error!("error reading from stdin: {e}");
break break;
} }
} }
} }
}).unwrap(); })?;
Ok(Self {})
let writer = 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::NotifyClientCreate(client, host, port, pos) => {
log::info!(
"new client ({client}): {}:{port} - {pos}",
host.as_deref().unwrap_or("")
);
}
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
log::info!(
"client ({client}) updated: {}:{port} - {pos}",
host.as_deref().unwrap_or("")
);
}
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
.addrs
.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}");
}
} }
match writer.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!("writer thread paniced: {msg}");
}
}
Ok(())
} }
fn parse_event(s: String, len: usize) -> Option<FrontendEvent> { fn prompt() {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
}
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
if len == 0 { if len == 0 {
return Some(FrontendEvent::RequestShutdown()) return Some(vec![FrontendEvent::Shutdown()]);
} }
let mut l = s.split_whitespace(); let mut l = s.split_whitespace();
let cmd = l.next()?; let cmd = l.next()?;
match cmd { let res = match cmd {
"connect" => { "help" => {
let addr = match l.next()?.parse() { log::info!("list list clients");
Ok(addr) => SocketAddr::V4(addr), log::info!("connect <host> left|right|top|bottom [port] add a new client");
Err(e) => { log::info!("disconnect <client> remove a client");
log::error!("parse error: {e}"); log::info!("activate <client> activate a client");
return None; log::info!("deactivate <client> deactivate a client");
} log::info!("exit exit lan-mouse");
}; log::info!("setport <port> change port");
Some(FrontendEvent::RequestClientAdd(addr, Position::Left )) 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}"); log::error!("unknown command: {s}");
None None
} }
};
match res {
Some(Ok(e)) => Some(e),
Some(Err(e)) => {
log::warn!("{e}");
None
}
_ => None,
} }
} }
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)])
}

198
src/frontend/gtk.rs Normal file
View File

@@ -0,0 +1,198 @@
mod client_object;
mod client_row;
mod window;
use std::{
env,
io::{ErrorKind, Read},
process, str,
};
use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
use adw::Application;
use gtk::{
gdk::Display,
gio::{SimpleAction, SimpleActionGroup},
glib::{clone, MainContext, Priority},
prelude::*,
subclass::prelude::ObjectSubclassIsExt,
CssProvider, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use super::FrontendNotify;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
let ret = gtk_main();
log::debug!("frontend exited");
ret
}
fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.lan-mouse")
.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 icon_theme =
IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket");
let mut rx = match super::wait_for_service() {
Ok(stream) => stream,
Err(e) => {
log::error!("could not connect to lan-mouse-socket: {e}");
process::exit(1);
}
};
let tx = match rx.try_clone() {
Ok(sock) => sock,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
log::debug!("connected to lan-mouse-socket");
let (sender, receiver) = MainContext::channel::<FrontendNotify>(Priority::default());
gio::spawn_blocking(move || {
match loop {
// read length
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
let len = usize::from_be_bytes(len);
// read payload
let mut buf = vec![0u8; len];
match rx.read_exact(&mut buf) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
// parse json
let json = str::from_utf8(&buf).unwrap();
match serde_json::from_str(json) {
Ok(notify) => sender.send(notify).unwrap(),
Err(e) => log::error!("{e}"),
}
} {
Ok(()) => {}
Err(e) => log::error!("{e}"),
}
});
let window = Window::new(app);
window.imp().stream.borrow_mut().replace(tx);
receiver.attach(None, clone!(@weak window => @default-return glib::ControlFlow::Break,
move |notify| {
match notify {
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
window.new_client(client, hostname, port, position, false);
},
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
}
FrontendNotify::NotifyError(e) => {
// TODO
log::error!("{e}");
},
FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client);
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients {
if window.client_idx(client.handle).is_some() {
continue
}
window.new_client(
client.handle,
client.hostname,
client.addrs
.iter()
.next()
.map(|s| s.port())
.unwrap_or(DEFAULT_PORT),
client.pos,
active,
);
}
},
FrontendNotify::NotifyPortChange(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
}
glib::ControlFlow::Continue
}
));
let action_request_client_update =
SimpleAction::new("request-client-update", Some(&u32::static_variant_type()));
// remove client
let action_client_delete =
SimpleAction::new("request-client-delete", Some(&u32::static_variant_type()));
// update client state
action_request_client_update.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("request-client-update");
let index = param.unwrap()
.get::<u32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let idx = param.unwrap()
.get::<u32>()
.unwrap();
window.request_client_delete(idx);
}));
let actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_request_client_update);
actions.add_action(&action_client_delete);
window.present();
}

View File

@@ -0,0 +1,41 @@
mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::client::ClientHandle;
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(
handle: ClientHandle,
hostname: Option<String>,
port: u32,
position: String,
active: bool,
) -> Self {
Object::builder()
.property("handle", handle)
.property("hostname", hostname)
.property("port", port)
.property("active", active)
.property("position", position)
.build()
}
pub fn get_data(&self) -> ClientData {
self.imp().data.borrow().clone()
}
}
#[derive(Default, Clone)]
pub struct ClientData {
pub handle: ClientHandle,
pub hostname: Option<String>,
pub port: u32,
pub active: bool,
pub position: String,
}

View File

@@ -0,0 +1,30 @@
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use crate::client::ClientHandle;
use super::ClientData;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
#[property(name = "hostname", get, set, type = String, member = hostname)]
#[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)]
pub data: RefCell<ClientData>,
}
#[glib::object_subclass]
impl ObjectSubclass for ClientObject {
const NAME: &'static str = "ClientObject";
type Type = super::ClientObject;
}
#[glib::derived_properties]
impl ObjectImpl for ClientObject {}

View File

@@ -0,0 +1,119 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::config::DEFAULT_PORT;
use super::ClientObject;
glib::wrapper! {
pub struct ClientRow(ObjectSubclass<imp::ClientRow>)
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl ClientRow {
pub fn new(_client_object: &ClientObject) -> Self {
Object::builder().build()
}
pub fn bind(&self, client_object: &ClientObject) {
let mut bindings = self.imp().bindings.borrow_mut();
let active_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "state")
.bidirectional()
.sync_create()
.build();
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("".to_string())
}
})
.transform_from(|_, v: String| {
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create()
.build();
let title_binding = client_object
.bind_property("hostname", self, "title")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
}
})
.sync_create()
.build();
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v == "" {
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.transform_to(|_, v: u32| {
if v == 4242 {
Some("".to_string())
} else {
Some(v.to_string())
}
})
.bidirectional()
.sync_create()
.build();
let subtitle_binding = client_object
.bind_property("port", self, "subtitle")
.sync_create()
.build();
let position_binding = client_object
.bind_property("position", &self.imp().position.get(), "selected")
.transform_from(|_, v: u32| match v {
1 => Some("right"),
2 => Some("top"),
3 => Some("bottom"),
_ => Some("left"),
})
.transform_to(|_, v: String| match v.as_str() {
"right" => Some(1),
"top" => Some(2u32),
"bottom" => Some(3u32),
_ => Some(0u32),
})
.bidirectional()
.sync_create()
.build();
bindings.push(active_binding);
bindings.push(hostname_binding);
bindings.push(title_binding);
bindings.push(port_binding);
bindings.push(subtitle_binding);
bindings.push(position_binding);
}
pub fn unbind(&self) {
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}

View File

@@ -0,0 +1,81 @@
use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::{glib, Button, CompositeTemplate, Switch};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
pub struct ClientRow {
#[template_child]
pub enable_switch: TemplateChild<gtk::Switch>,
#[template_child]
pub hostname: TemplateChild<gtk::Entry>,
#[template_child]
pub port: TemplateChild<gtk::Entry>,
#[template_child]
pub position: TemplateChild<ComboRow>,
#[template_child]
pub delete_row: TemplateChild<ActionRow>,
#[template_child]
pub delete_button: TemplateChild<gtk::Button>,
pub bindings: RefCell<Vec<Binding>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ClientRow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "ClientRow";
type Type = super::ClientRow;
type ParentType = adw::ExpanderRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ClientRow {
fn constructed(&self) {
self.parent_constructed();
self.delete_button
.connect_clicked(clone!(@weak self as row => move |button| {
row.handle_client_delete(button);
}));
}
}
#[gtk::template_callbacks]
impl ClientRow {
#[template_callback]
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
let idx = self.obj().index() as u32;
switch
.activate_action("win.request-client-update", Some(&idx.to_variant()))
.unwrap();
switch.set_state(state);
true // dont run default handler
}
#[template_callback]
fn handle_client_delete(&self, button: &Button) {
log::debug!("delete button pressed");
let idx = self.obj().index() as u32;
button
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
.unwrap();
}
}
impl WidgetImpl for ClientRow {}
impl BoxImpl for ClientRow {}
impl ListBoxRowImpl for ClientRow {}
impl PreferencesRowImpl for ClientRow {}
impl ExpanderRowImpl for ClientRow {}

179
src/frontend/gtk/window.rs Normal file
View File

@@ -0,0 +1,179 @@
mod imp;
use std::io::Write;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{gio, glib, NoSelection};
use crate::{
client::{ClientHandle, Position},
config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent},
};
use super::client_row::ClientRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub(crate) fn new(app: &adw::Application) -> Self {
Object::builder().property("application", app).build()
}
pub fn clients(&self) -> gio::ListStore {
self.imp()
.clients
.borrow()
.clone()
.expect("Could not get clients")
}
fn setup_clients(&self) {
let model = gio::ListStore::new::<ClientObject>();
self.imp().clients.replace(Some(model));
let selection_model = NoSelection::new(Some(self.clients()));
self.imp().client_list.bind_model(
Some(&selection_model),
clone!(@weak self as window => @default-panic, move |obj| {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.upcast()
})
);
}
/// workaround for a bug in libadwaita that shows an ugly line beneath
/// the last element if a placeholder is set.
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
pub fn set_placeholder_visible(&self, visible: bool) {
let placeholder = self.imp().client_placeholder.get();
self.imp().client_list.set_placeholder(match visible {
true => Some(&placeholder),
false => None,
});
}
fn setup_icon(&self) {
self.set_icon_name(Some("mouse-icon"));
}
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
let row = ClientRow::new(client_object);
row.bind(client_object);
row
}
pub fn new_client(
&self,
handle: ClientHandle,
hostname: Option<String>,
port: u16,
position: Position,
active: bool,
) {
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
self.clients().append(&client);
self.set_placeholder_visible(false);
}
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients()
.iter::<ClientObject>()
.position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
false
}
})
.map(|p| p as usize)
}
pub fn delete_client(&self, handle: ClientHandle) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
}
}
pub fn request_client_create(&self) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
self.imp().set_port(DEFAULT_PORT);
self.request(event);
}
pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = u16::from_str_radix(port.as_str(), 10) {
self.request(FrontendEvent::ChangePort(port));
} else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));
}
}
pub fn request_client_update(&self, client: &ClientObject) {
let data = client.get_data();
let position = match data.position.as_str() {
"left" => Position::Left,
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => {
log::error!("invalid position: {}", data.position);
return;
}
};
let hostname = data.hostname;
let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
self.request(event);
}
pub fn request_client_delete(&self, idx: u32) {
if let Some(obj) = self.clients().item(idx) {
let client_object: &ClientObject = obj
.downcast_ref()
.expect("Expected object of type `ClientObject`.");
let handle = client_object.handle();
let event = FrontendEvent::DelClient(handle);
self.request(event);
}
}
fn request(&self, event: FrontendEvent) {
let json = serde_json::to_string(&event).unwrap();
log::debug!("requesting {json}");
let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = stream.write(bytes) {
log::error!("error sending message: {e}");
};
}
pub fn show_toast(&self, msg: &str) {
let toast = adw::Toast::new(msg);
let toast_overlay = &self.imp().toast_overlay;
toast_overlay.add_toast(toast);
}
}

View File

@@ -0,0 +1,105 @@
use std::{
cell::{Cell, RefCell},
os::unix::net::UnixStream,
};
use adw::subclass::prelude::*;
use adw::{
prelude::{EditableExt, WidgetExt},
ActionRow, ToastOverlay,
};
use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
use crate::config::DEFAULT_PORT;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
#[template_child]
pub port_edit_apply: TemplateChild<Button>,
#[template_child]
pub port_edit_cancel: TemplateChild<Button>,
#[template_child]
pub client_list: TemplateChild<ListBox>,
#[template_child]
pub client_placeholder: TemplateChild<ActionRow>,
#[template_child]
pub port_entry: TemplateChild<Entry>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>,
pub stream: RefCell<Option<UnixStream>>,
pub port: Cell<u16>,
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LanMouseWindow";
type Type = super::Window;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn handle_add_client_pressed(&self, _button: &Button) {
self.obj().request_client_create();
}
#[template_callback]
fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true);
self.port_edit_cancel.set_visible(true);
}
#[template_callback]
fn handle_port_edit_apply(&self) {
self.obj().request_port_change();
}
#[template_callback]
fn handle_port_edit_cancel(&self) {
log::debug!("cancel port edit");
self.port_entry
.set_text(self.port.get().to_string().as_str());
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
pub fn set_port(&self, port: u16) {
self.port.set(port);
if port == DEFAULT_PORT {
self.port_entry.set_text("");
} else {
self.port_entry.set_text(format!("{port}").as_str());
}
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
}
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
self.set_port(DEFAULT_PORT);
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}
impl AdwApplicationWindowImpl for Window {}

View File

@@ -2,9 +2,8 @@ use std::io::{self, Write};
use crate::client::Position; use crate::client::Position;
pub fn ask_confirmation(default: bool) -> Result<bool, io::Error> { pub fn ask_confirmation(default: bool) -> Result<bool, io::Error> {
eprint!("{}", if default {" [Y,n] "} else { " [y,N] "}); eprint!("{}", if default { " [Y,n] " } else { " [y,N] " });
io::stderr().flush()?; io::stderr().flush()?;
let answer = loop { let answer = loop {
let mut buffer = String::new(); let mut buffer = String::new();
@@ -18,7 +17,7 @@ pub fn ask_confirmation(default: bool) -> Result<bool, io::Error> {
_ => { _ => {
eprint!("Enter y for Yes or n for No: "); eprint!("Enter y for Yes or n for No: ");
io::stderr().flush()?; io::stderr().flush()?;
continue continue;
} }
} }
}; };
@@ -41,7 +40,7 @@ pub fn ask_position() -> Result<Position, io::Error> {
_ => { _ => {
eprint!("Invalid position: {answer} - enter top (t) | bottom (b) | left(l) | right(r): "); eprint!("Invalid position: {answer} - enter top (t) | bottom (b) | left(l) | right(r): ");
io::stderr().flush()?; io::stderr().flush()?;
continue continue;
} }
}; };
}; };

View File

@@ -2,6 +2,7 @@ pub mod client;
pub mod config; pub mod config;
pub mod dns; pub mod dns;
pub mod event; pub mod event;
pub mod server;
pub mod consumer; pub mod consumer;
pub mod producer; pub mod producer;

View File

@@ -1,14 +1,18 @@
use std::{process, error::Error}; use anyhow::Result;
use std::process::{self, Child, Command};
use env_logger::Env; use env_logger::Env;
use lan_mouse::{ use lan_mouse::{
consumer, producer, config::Config,
config::{Config, Frontend::{Gtk, Cli}}, event::server::Server, consumer,
frontend::{FrontendAdapter, cli::CliFrontend}, frontend::{self, FrontendListener},
producer,
server::Server,
}; };
pub fn main() { use tokio::{join, task::LocalSet};
pub fn main() {
// init logging // init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info"); let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
env_logger::init_from_env(env); env_logger::init_from_env(env);
@@ -19,47 +23,64 @@ pub fn main() {
} }
} }
pub fn run() -> Result<(), Box<dyn Error>> { pub fn start_service() -> Result<Child> {
// parse config file let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("--daemon")
.spawn()?;
Ok(child)
}
pub fn run() -> Result<()> {
// parse config file + cli args
let config = Config::new()?; let config = Config::new()?;
log::debug!("{config:?}");
// start producing and consuming events if config.daemon {
let producer = producer::create()?; // if daemon is specified we run the service
let consumer = consumer::create()?; run_service(&config)?;
} else {
// otherwise start the service as a child process and
// run a frontend
start_service()?;
frontend::run_frontend(&config)?;
}
// create frontend communication adapter anyhow::Ok(())
let frontend_adapter = FrontendAdapter::new()?; }
// start sending and receiving events fn run_service(config: &Config) -> Result<()> {
let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?; // create single threaded tokio runtime
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
// add clients form config // run async event loop
config.get_clients().into_iter().for_each(|(c, h, p)| { runtime.block_on(LocalSet::new().run_until(async {
let host_name = match h { // create frontend communication adapter
Some(h) => format!(" '{}'", h), let frontend_adapter = match FrontendListener::new().await {
None => "".to_owned(), Some(Err(e)) => return Err(e),
Some(Ok(f)) => f,
None => {
// none means some other instance is already running
log::info!("service already running, exiting");
return anyhow::Ok(());
}
}; };
if c.len() == 0 {
log::warn!("ignoring client{} with 0 assigned ips!", host_name);
}
log::info!("adding client [{}]{} @ {:?}", p, host_name, c);
event_server.add_client(c, p);
});
// any threads need to be started after event_server sets up signal handling // create event producer and consumer
match config.frontend { let (producer, consumer) = join!(producer::create(), consumer::create(),);
Gtk => { let (producer, consumer) = (producer?, consumer?);
#[cfg(all(unix, feature = "gtk"))]
frontend::gtk::create();
#[cfg(not(feature = "gtk"))]
panic!("gtk frontend requested but feature not enabled!");
},
Cli => Box::new(CliFrontend::new()?),
};
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse"); // create server
// run event loop let mut event_server = Server::new(config, frontend_adapter, consumer, producer).await?;
event_server.run()?; log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
// run event loop
event_server.run().await?;
log::debug!("service exiting");
anyhow::Ok(())
}))?;
Ok(()) Ok(())
} }

View File

@@ -1,38 +1,65 @@
use mio::event::Source; use anyhow::Result;
use std::{error::Error, vec::Drain}; use std::io;
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
use crate::backend::producer;
#[cfg(unix)] use futures_core::Stream;
use crate::backend::producer;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
#[cfg(all(unix, not(target_os = "macos")))]
use std::env; use std::env;
#[cfg(unix)] #[cfg(all(unix, not(target_os = "macos")))]
enum Backend { enum Backend {
Wayland, LayerShell,
Libei,
X11, X11,
} }
pub fn create() -> Result<Box<dyn EventProducer>, Box<dyn Error>> { pub async fn create() -> Result<Box<dyn EventProducer>> {
#[cfg(target_os = "macos")]
return Ok(Box::new(producer::macos::MacOSProducer::new()));
#[cfg(windows)] #[cfg(windows)]
return Ok(Box::new(producer::windows::WindowsProducer::new())); return Ok(Box::new(producer::windows::WindowsProducer::new()));
#[cfg(unix)] #[cfg(all(unix, not(target_os = "macos")))]
let backend = match env::var("XDG_SESSION_TYPE") { let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() { Ok(session_type) => match session_type.as_str() {
"x11" => { "x11" => {
log::info!("XDG_SESSION_TYPE = x11 -> using X11 event producer"); log::info!("XDG_SESSION_TYPE = x11 -> using X11 event producer");
Backend::X11 Backend::X11
}, }
"wayland" => { "wayland" => {
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event producer"); log::info!("XDG_SESSION_TYPE = wayland -> using wayland event producer");
Backend::Wayland match env::var("XDG_CURRENT_DESKTOP") {
Ok(desktop) => match desktop.as_str() {
"GNOME" => {
log::info!("XDG_CURRENT_DESKTOP = GNOME -> using libei backend");
Backend::Libei
}
d => {
log::info!("XDG_CURRENT_DESKTOP = {d} -> using layer_shell backend");
Backend::LayerShell
}
},
Err(_) => {
log::warn!("XDG_CURRENT_DESKTOP not set! Assuming layer_shell support -> using layer_shell backend");
Backend::LayerShell
}
}
} }
_ => panic!("unknown XDG_SESSION_TYPE"), _ => panic!("unknown XDG_SESSION_TYPE"),
}, },
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"), Err(_) => {
panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!")
}
}; };
#[cfg(unix)] #[cfg(all(unix, not(target_os = "macos")))]
match backend { match backend {
Backend::X11 => { Backend::X11 => {
#[cfg(not(feature = "x11"))] #[cfg(not(feature = "x11"))]
@@ -40,24 +67,25 @@ pub fn create() -> Result<Box<dyn EventProducer>, Box<dyn Error>> {
#[cfg(feature = "x11")] #[cfg(feature = "x11")]
Ok(Box::new(producer::x11::X11Producer::new())) Ok(Box::new(producer::x11::X11Producer::new()))
} }
Backend::Wayland => { Backend::LayerShell => {
#[cfg(not(feature = "wayland"))] #[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled"); panic!("feature wayland not enabled");
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
Ok(Box::new(producer::wayland::WaylandEventProducer::new()?)) Ok(Box::new(producer::wayland::WaylandEventProducer::new()?))
} }
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
Ok(Box::new(producer::libei::LibeiProducer::new()?))
}
} }
} }
pub trait EventProducer: Source { pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
/// notify event producer of configuration changes /// notify event producer of configuration changes
fn notify(&mut self, event: ClientEvent); fn notify(&mut self, event: ClientEvent);
/// read an event
/// this function must be invoked to retrieve an Event after
/// the eventfd indicates a pending Event
fn read_events(&mut self) -> Drain<(ClientHandle, Event)>;
/// release mouse /// release mouse
fn release(&mut self); fn release(&mut self);
} }

553
src/server.rs Normal file
View File

@@ -0,0 +1,553 @@
use futures::stream::StreamExt;
use log;
use std::{
collections::HashSet,
error::Error,
io::Result,
net::IpAddr,
time::{Duration, Instant},
};
use tokio::{
io::ReadHalf,
net::UdpSocket,
signal,
sync::mpsc::{Receiver, Sender},
};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
use std::{io::ErrorKind, net::SocketAddr};
use crate::event::Event;
use crate::{
client::{ClientEvent, ClientHandle, ClientManager, Position},
config::Config,
consumer::EventConsumer,
dns::{self, DnsResolver},
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
producer::EventProducer,
};
/// keeps track of state to prevent a feedback loop
/// of continuously sending and receiving the same event.
#[derive(Eq, PartialEq)]
enum State {
Sending,
Receiving,
}
pub struct Server {
resolver: DnsResolver,
client_manager: ClientManager,
state: State,
frontend: FrontendListener,
consumer: Box<dyn EventConsumer>,
producer: Box<dyn EventProducer>,
socket: UdpSocket,
frontend_rx: Receiver<FrontendEvent>,
frontend_tx: Sender<FrontendEvent>,
}
impl Server {
pub async fn new(
config: &Config,
frontend: FrontendListener,
consumer: Box<dyn EventConsumer>,
producer: Box<dyn EventProducer>,
) -> anyhow::Result<Self> {
// create dns resolver
let resolver = dns::DnsResolver::new().await?;
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), config.port);
let socket = UdpSocket::bind(listen_addr).await?;
let (frontend_tx, frontend_rx) = tokio::sync::mpsc::channel(1);
// create client manager
let client_manager = ClientManager::new();
let mut server = Server {
frontend,
consumer,
producer,
resolver,
socket,
client_manager,
state: State::Receiving,
frontend_rx,
frontend_tx,
};
// add clients from config
for (c, h, port, p) in config.get_clients().into_iter() {
server.add_client(h, c, port, p).await;
}
Ok(server)
}
pub async fn run(&mut self) -> anyhow::Result<()> {
loop {
log::trace!("polling ...");
tokio::select! {
// safety: cancellation safe
udp_event = receive_event(&self.socket) => {
log::trace!("-> receive_event");
match udp_event {
Ok(e) => self.handle_udp_rx(e).await,
Err(e) => log::error!("error reading event: {e}"),
}
}
// safety: cancellation safe
res = self.producer.next() => {
log::trace!("-> producer.next()");
match res {
Some(Ok((client, event))) => {
self.handle_producer_event(client,event).await;
},
Some(Err(e)) => log::error!("error reading from event producer: {e}"),
_ => break,
}
}
// safety: cancellation safe
stream = self.frontend.accept() => {
log::trace!("-> frontend.accept()");
match stream {
Ok(s) => self.handle_frontend_stream(s).await,
Err(e) => log::error!("error connecting to frontend: {e}"),
}
}
// safety: cancellation safe
frontend_event = self.frontend_rx.recv() => {
log::trace!("-> frontend.recv()");
if let Some(event) = frontend_event {
if self.handle_frontend_event(event).await {
break;
}
}
}
// safety: cancellation safe
e = self.consumer.dispatch() => {
log::trace!("-> consumer.dispatch()");
if let Err(e) = e {
return Err(e);
}
}
// safety: cancellation safe
_ = signal::ctrl_c() => {
log::info!("terminating gracefully ...");
break;
}
}
}
// destroy consumer
self.consumer.destroy().await;
Ok(())
}
pub async fn add_client(
&mut self,
hostname: Option<String>,
mut addr: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
let ips = if let Some(hostname) = hostname.as_ref() {
match self.resolver.resolve(hostname.as_str()).await {
Ok(ips) => HashSet::from_iter(ips.iter().cloned()),
Err(e) => {
log::warn!("could not resolve host: {e}");
HashSet::new()
}
}
} else {
HashSet::new()
};
addr.extend(ips.iter());
log::info!(
"adding client [{}]{} @ {:?}",
pos,
hostname.as_deref().unwrap_or(""),
&ips
);
let client = self
.client_manager
.add_client(hostname.clone(), addr, port, pos);
log::debug!("add_client {client}");
let notify = FrontendNotify::NotifyClientCreate(client, hostname, port, pos);
if let Err(e) = self.frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
};
client
}
pub async fn activate_client(&mut self, client: ClientHandle, active: bool) {
if let Some(state) = self.client_manager.get_mut(client) {
state.active = active;
if state.active {
self.producer
.notify(ClientEvent::Create(client, state.client.pos));
self.consumer
.notify(ClientEvent::Create(client, state.client.pos))
.await;
} else {
self.producer.notify(ClientEvent::Destroy(client));
self.consumer.notify(ClientEvent::Destroy(client)).await;
}
}
}
pub async fn remove_client(&mut self, client: ClientHandle) -> Option<ClientHandle> {
self.producer.notify(ClientEvent::Destroy(client));
self.consumer.notify(ClientEvent::Destroy(client)).await;
if let Some(client) = self
.client_manager
.remove_client(client)
.map(|s| s.client.handle)
{
let notify = FrontendNotify::NotifyClientDelete(client);
log::debug!("{notify:?}");
if let Err(e) = self.frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
}
Some(client)
} else {
None
}
}
pub async fn update_client(
&mut self,
client: ClientHandle,
hostname: Option<String>,
port: u16,
pos: Position,
) {
// retrieve state
let Some(state) = self.client_manager.get_mut(client) else {
return;
};
// update pos
state.client.pos = pos;
if state.active {
self.producer.notify(ClientEvent::Destroy(client));
self.consumer.notify(ClientEvent::Destroy(client)).await;
self.producer.notify(ClientEvent::Create(client, pos));
self.consumer.notify(ClientEvent::Create(client, pos)).await;
}
// update port
if state.client.port != port {
state.client.port = port;
state.client.addrs = state
.client
.addrs
.iter()
.cloned()
.map(|mut a| {
a.set_port(port);
a
})
.collect();
state
.client
.active_addr
.map(|a| SocketAddr::new(a.ip(), port));
}
// update hostname
if state.client.hostname != hostname {
state.client.addrs = HashSet::new();
state.client.active_addr = None;
state.client.hostname = hostname;
if let Some(hostname) = state.client.hostname.as_ref() {
match self.resolver.resolve(hostname.as_str()).await {
Ok(ips) => {
let addrs = ips.iter().map(|i| SocketAddr::new(*i, port));
state.client.addrs = HashSet::from_iter(addrs);
}
Err(e) => {
log::warn!("could not resolve host: {e}");
}
}
}
}
log::debug!("client updated: {:?}", state);
}
async fn handle_udp_rx(&mut self, event: (Event, SocketAddr)) {
let (event, addr) = event;
// get handle for addr
let handle = match self.client_manager.get_client(addr) {
Some(a) => a,
None => {
log::warn!("ignoring event from client {addr:?}");
return;
}
};
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
let state = match self.client_manager.get_mut(handle) {
Some(s) => s,
None => {
log::error!("unknown handle");
return;
}
};
// reset ttl for client and
state.last_seen = Some(Instant::now());
// set addr as new default for this client
state.client.active_addr = Some(addr);
match (event, addr) {
(Event::Pong(), _) => {}
(Event::Ping(), addr) => {
if let Err(e) = send_event(&self.socket, Event::Pong(), addr).await {
log::error!("udp send: {}", e);
}
// we release the mouse here,
// since its very likely, that we wont get a release event
self.producer.release();
}
(event, addr) => match self.state {
State::Sending => {
// in sending state, we dont want to process
// any events to avoid feedback loops,
// therefore we tell the event producer
// to release the pointer and move on
// first event -> release pointer
if let Event::Release() = event {
log::debug!("releasing pointer ...");
self.producer.release();
self.state = State::Receiving;
}
}
State::Receiving => {
// consume event
self.consumer.consume(event, handle).await;
// let the server know we are still alive once every second
let last_replied = state.last_replied;
if last_replied.is_none()
|| last_replied.is_some()
&& last_replied.unwrap().elapsed() > Duration::from_secs(1)
{
state.last_replied = Some(Instant::now());
if let Err(e) = send_event(&self.socket, Event::Pong(), addr).await {
log::error!("udp send: {}", e);
}
}
}
},
}
}
async fn handle_producer_event(&mut self, c: ClientHandle, e: Event) {
let mut should_release = false;
// in receiving state, only release events
// must be transmitted
if let Event::Release() = e {
self.state = State::Sending;
}
log::trace!("producer: ({c}) {e:?}");
let state = match self.client_manager.get_mut(c) {
Some(state) => state,
None => {
log::warn!("unknown client!");
return;
}
};
// otherwise we should have an address to send to
// transmit events to the corrensponding client
if let Some(addr) = state.client.active_addr {
if let Err(e) = send_event(&self.socket, e, addr).await {
log::error!("udp send: {}", e);
}
}
// if client last responded > 2 seconds ago
// and we have not sent a ping since 500 milliseconds,
// send a ping
if state.last_seen.is_some() && state.last_seen.unwrap().elapsed() < Duration::from_secs(2)
{
return;
}
// client last seen > 500ms ago
if state.last_ping.is_some()
&& state.last_ping.unwrap().elapsed() < Duration::from_millis(500)
{
return;
}
// release mouse if client didnt respond to the first ping
if state.last_ping.is_some() && state.last_ping.unwrap().elapsed() < Duration::from_secs(1)
{
should_release = true;
}
// last ping > 500ms ago -> ping all interfaces
state.last_ping = Some(Instant::now());
for addr in state.client.addrs.iter() {
log::debug!("pinging {addr}");
if let Err(e) = send_event(&self.socket, Event::Ping(), *addr).await {
if e.kind() != ErrorKind::WouldBlock {
log::error!("udp send: {}", e);
}
}
// send additional release event, in case client is still in sending mode
if let Err(e) = send_event(&self.socket, Event::Release(), *addr).await {
if e.kind() != ErrorKind::WouldBlock {
log::error!("udp send: {}", e);
}
}
}
if should_release && self.state != State::Receiving {
log::info!("client not responding - releasing pointer");
self.producer.release();
self.state = State::Receiving;
}
}
#[cfg(unix)]
async fn handle_frontend_stream(&mut self, mut stream: ReadHalf<UnixStream>) {
use std::io;
let tx = self.frontend_tx.clone();
tokio::task::spawn_local(async move {
loop {
let event = frontend::read_event(&mut stream).await;
match event {
Ok(event) => tx.send(event).await.unwrap(),
Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() {
if e.kind() == ErrorKind::UnexpectedEof {
return;
}
}
log::error!("error reading frontend event: {e}");
}
}
}
});
self.enumerate().await;
}
#[cfg(windows)]
async fn handle_frontend_stream(&mut self, mut stream: ReadHalf<TcpStream>) {
let tx = self.frontend_tx.clone();
tokio::task::spawn_local(async move {
loop {
let event = frontend::read_event(&mut stream).await;
match event {
Ok(event) => tx.send(event).await.unwrap(),
Err(e) => log::error!("error reading frontend event: {e}"),
}
}
});
self.enumerate().await;
}
async fn handle_frontend_event(&mut self, event: FrontendEvent) -> bool {
log::debug!("frontend: {event:?}");
match event {
FrontendEvent::AddClient(hostname, port, pos) => {
self.add_client(hostname, HashSet::new(), port, pos).await;
}
FrontendEvent::ActivateClient(client, active) => {
self.activate_client(client, active).await
}
FrontendEvent::ChangePort(port) => {
let current_port = self.socket.local_addr().unwrap().port();
if current_port == port {
if let Err(e) = self
.frontend
.notify_all(FrontendNotify::NotifyPortChange(port, None))
.await
{
log::warn!("error notifying frontend: {e}");
}
return false;
}
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
match UdpSocket::bind(listen_addr).await {
Ok(socket) => {
self.socket = socket;
if let Err(e) = self
.frontend
.notify_all(FrontendNotify::NotifyPortChange(port, None))
.await
{
log::warn!("error notifying frontend: {e}");
}
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = self.socket.local_addr().unwrap().port();
if let Err(e) = self
.frontend
.notify_all(FrontendNotify::NotifyPortChange(
port,
Some(format!("could not change port: {e}")),
))
.await
{
log::error!("error notifying frontend: {e}");
}
}
}
}
FrontendEvent::DelClient(client) => {
self.remove_client(client).await;
}
FrontendEvent::Enumerate() => self.enumerate().await,
FrontendEvent::Shutdown() => {
log::info!("terminating gracefully...");
return true;
}
FrontendEvent::UpdateClient(client, hostname, port, pos) => {
self.update_client(client, hostname, port, pos).await
}
}
false
}
async fn enumerate(&mut self) {
let clients = self.client_manager.enumerate();
if let Err(e) = self
.frontend
.notify_all(FrontendNotify::Enumerate(clients))
.await
{
log::error!("error notifying frontend: {e}");
}
}
}
async fn receive_event(
socket: &UdpSocket,
) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
log::trace!("receive_event");
let mut buf = vec![0u8; 22];
match socket.recv_from(&mut buf).await {
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
Err(e) => Err(Box::new(e)),
}
}
async fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let data: Vec<u8> = (&e).into();
// We are currently abusing a blocking send to get the lowest possible latency.
// It may be better to set the socket to non-blocking and only send when ready.
sock.send_to(&data[..], addr).await
}