Compare commits

...

21 Commits

Author SHA1 Message Date
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
Ferdinand Schober
4a61ed82a9 chore: Release lan-mouse version 0.2.0 2023-09-19 19:41:44 +02:00
Ferdinand Schober
a534f366b4 update dependencies 2023-09-19 19:41:05 +02:00
Ferdinand Schober
16311f8ae6 fix interrupted syscall when waking from suspend (#23) 2023-09-19 19:33:04 +02:00
Ferdinand Schober
1a4d0e05be Epoll (#20)
major update:
- remove threading overhead by resorting to an event driven design with mio as a backend for epoll
- Clients can now have an arbitrary amount of ip adresses and lan-mouse will automatically choose the correct one
- -> seemless switching between ethernet and wifi
- cli frontend + frontend adapter for future frontends
2023-09-19 19:12:47 +02:00
Ferdinand Schober
22e6c531af hotfix: Oneshot seems to crash Hyprland (#22)
closes #21
2023-09-17 14:32:47 +02:00
Ferdinand Schober
31eead5f8e continue without keymap (#18) 2023-09-12 12:17:44 +02:00
42 changed files with 3898 additions and 1184 deletions

View File

@@ -18,6 +18,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build
run: cargo build --release
- name: Upload build artifact

View File

@@ -20,6 +20,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Build
run: cargo build --verbose
- name: Run tests

View File

@@ -14,6 +14,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build
run: cargo build --release
- name: Upload build artifact

1013
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.1.1-alpha.1"
version = "0.3.2"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -13,13 +13,17 @@ strip = true
lto = "fat"
[dependencies]
tempfile = "3.6"
trust-dns-resolver = "0.22"
tempfile = "3.8"
trust-dns-resolver = "0.23"
memmap = "0.7"
toml = "0.7"
serde = "1.0"
serde_derive = "1.0"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71"
log = "0.4.20"
env_logger = "0.10.0"
mio = { version = "0.8", features = ["os-ext"] }
libc = "0.2.148"
serde_json = "1.0.107"
[target.'cfg(unix)'.dependencies]
wayland-client = { version="0.30.2", optional = true }
@@ -27,15 +31,32 @@ wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable
wayland-protocols-wlr = { 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 }
mio-signals = "0.2.0"
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
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(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
[target.'cfg(unix)'.build-dependencies]
glib-build-tools = "0.18.0"
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"]
x11 = ["dep:x11"]
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" ]
x11 = [ "dep:x11" ]
xdg_desktop_portal = []
libei = []
gtk = ["dep:gtk", "dep:adw"]

View File

@@ -1,4 +1,8 @@
# Lan Mouse Share
![image](https://github.com/ferdinandschober/lan-mouse/assets/40996949/ccb33815-4357-4c8d-a5d2-8897ab626a08)
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, ... .
@@ -7,6 +11,8 @@ Of course ***blazingly fast™*** and stable, because it's written in rust.
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
_Now with a gtk frontend_
## Configuration
Configuration is done through the file `config.toml`,
which must be located in the current working directory when
@@ -16,32 +22,45 @@ executing lan-mouse.
A minimal config file could look like this:
```toml
# example configuration
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
[left]
host_name = "my-laptop"
# 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"]
# optional port
port = 4242
```
Where `left` can be either `left`, `right`, `top` or `bottom`.
> :warning: Note that clients from the config
> file are currently ignored when using the gtk frontend!
### 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 only
Build in release mode:
```sh
cargo build --release
```
Run
Run directly:
```sh
cargo run --release
```
@@ -133,12 +152,8 @@ This is to be looked into in the future.
(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.
Currently windows can receive mouse and keyboard events,
event producing on windows is WIP.
## TODOS
@@ -151,11 +166,11 @@ Event emitting is WIP.
- [x] Button support
- [ ] Latency measurement + 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
- [ ] Liveness tracking (automatically ungrab mouse when client unreachable)
- [x] Liveness tracking (automatically ungrab mouse when client unreachable)
- [ ] Clipboard support
- [ ] Graphical frontend (gtk?)
- [x] Graphical frontend (gtk?)
- [ ] *Encrytion*
- [ ] Gnome Shell Extension (layer shell is not supported)
- [ ] respect xdg-config-home for config file location.

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

@@ -1,22 +1,23 @@
# example configuration
# optional port
# optional port (defaults to 4242)
port = 4242
# optional backend override
backend = "wlroots"
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
# optional ip address
ip = "192.168.178.141"
# optional port (defaults to 4242)
port = 4242
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
#
# when an IP address is specified, it takes priority
# and host_name can be omitted
[left]
ip = "192.168.178.189"
# 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

272
deny.toml Normal file
View File

@@ -0,0 +1,272 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
"MIT",
"BSD-3-Clause",
"ISC",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"Unicode-DFS-2016",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overriden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overriden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#name = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = [""]
# 1 or more gitlab.com organizations to allow git sources for
gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
bitbucket = [""]

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>

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

@@ -0,0 +1,3 @@
#delete-button {
color: @red_1;
}

3
resources/style.css Normal file
View File

@@ -0,0 +1,3 @@
#delete-button {
color: @red_3;
}

121
resources/window.ui Normal file
View File

@@ -0,0 +1,121 @@
<?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="GtkApplicationWindow">
<property name="width-request">800</property>
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<child type="titlebar">
<object class="GtkHeaderBar">
<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>
<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">300</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">
<!-- <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>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Connections</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 below</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="GtkButton" id="add_client_button">
<property name="halign">center</property>
<property name="valign">center</property>
<property name="tooltip-text">connect a new computer</property>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add</property>
<property name="use-underline">True</property>
</object>
</property>
<!-- <signal name="clicked" handler="handle_add_client" swapped="true"/> -->
<style>
<class name="pill"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</template>
</interface>

View File

@@ -1,9 +1,18 @@
use std::sync::mpsc::Receiver;
use crate::consumer::EventConsumer;
use crate::{event::Event, client::{ClientHandle, Client}};
pub struct LibeiConsumer {}
pub(crate) fn run(_consume_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
todo!()
impl LibeiConsumer {
pub fn new() -> Self { Self { } }
}
impl EventConsumer for LibeiConsumer {
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) {
log::error!("libei backend not yet implemented!");
todo!()
}
fn notify(&mut self, _: crate::client::ClientEvent) {
todo!()
}
}

View File

@@ -1,6 +1,4 @@
use std::sync::mpsc::Receiver;
use crate::event::{KeyboardEvent, PointerEvent};
use crate::{event::{KeyboardEvent, PointerEvent}, consumer::EventConsumer};
use winapi::{
self,
um::winuser::{INPUT, INPUT_MOUSE, LPINPUT, MOUSEEVENTF_MOVE, MOUSEINPUT,
@@ -16,10 +14,45 @@ use winapi::{
};
use crate::{
client::{Client, ClientHandle},
client::{ClientEvent, ClientHandle},
event::Event,
};
pub struct WindowsConsumer {}
impl WindowsConsumer {
pub fn new() -> Self { Self { } }
}
impl EventConsumer for WindowsConsumer {
fn consume(&self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
rel_mouse(relative_x as i32, relative_y as i32);
}
PointerEvent::Button { time:_, button, state } => { mouse_button(button, state)}
PointerEvent::Axis { time:_, axis, value } => { scroll(axis, value) }
PointerEvent::Frame {} => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { time:_, key, state } => { key_event(key, state) }
KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
}
fn notify(&mut self, _: ClientEvent) {
// nothing to do
}
}
fn send_mouse_input(mi: MOUSEINPUT) {
unsafe {
let mut input = INPUT {
@@ -114,27 +147,3 @@ fn send_keyboard_input(ki: KEYBDINPUT) {
winapi::um::winuser::SendInput(1 as u32, &mut input, std::mem::size_of::<INPUT>() as i32);
}
}
pub fn run(event_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
loop {
match event_rx.recv().expect("event receiver unavailable").0 {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
rel_mouse(relative_x as i32, relative_y as i32);
}
PointerEvent::Button { time:_, button, state } => { mouse_button(button, state)}
PointerEvent::Axis { time:_, axis, value } => { scroll(axis, value) }
PointerEvent::Frame {} => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { time:_, key, state } => { key_event(key, state) }
KeyboardEvent::Modifiers { .. } => {}
},
Event::Release() => {}
}
}
}

View File

@@ -1,16 +1,15 @@
use crate::client::{Client, ClientHandle};
use crate::request::{self, Request};
use wayland_client::WEnum;
use crate::client::{ClientHandle, ClientEvent};
use crate::consumer::EventConsumer;
use std::collections::HashMap;
use std::sync::mpsc::Receiver;
use std::time::Duration;
use std::{io, thread};
use std::{
io::{BufWriter, Write},
os::unix::prelude::AsRawFd,
};
use std::os::fd::OwnedFd;
use std::os::unix::prelude::AsRawFd;
use anyhow::{Result, anyhow};
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_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1 as Vp,
@@ -30,8 +29,6 @@ use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle,
};
use tempfile;
use crate::event::{Event, KeyboardEvent, PointerEvent};
enum VirtualInputManager {
@@ -39,27 +36,31 @@ enum VirtualInputManager {
Kde { fake_input: OrgKdeKwinFakeInput },
}
// App State, implements Dispatch event handlers
struct App {
struct State {
keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<ClientHandle, VirtualInput>,
seat: wl_seat::WlSeat,
event_rx: Receiver<(Event, ClientHandle)>,
virtual_input_manager: VirtualInputManager,
queue: EventQueue<Self>,
qh: QueueHandle<Self>,
}
pub fn run(event_rx: Receiver<(Event, ClientHandle)>, clients: Vec<Client>) {
let mut app = App::new(event_rx, clients);
app.run();
// App State, implements Dispatch event handlers
pub(crate) struct WlrootsConsumer {
state: State,
queue: EventQueue<State>,
}
impl App {
pub fn new(event_rx: Receiver<(Event, ClientHandle)>, clients: Vec<Client>) -> Self {
impl WlrootsConsumer {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().unwrap();
let (globals, queue) = registry_queue_init::<App>(&conn).unwrap();
let (globals, queue) = registry_queue_init::<State>(&conn).unwrap();
let qh = queue.handle();
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) {
Ok(wl_seat) => wl_seat,
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
};
let vpm: Result<VpManager, BindError> = globals.bind(&qh, 1..=1, ());
let vkm: Result<VkManager, BindError> = globals.bind(&qh, 1..=1, ());
let fake_input: Result<OrgKdeKwinFakeInput, BindError> = globals.bind(&qh, 4..=4, ());
@@ -74,10 +75,11 @@ impl App {
VirtualInputManager::Kde { fake_input }
}
(Err(e1), Err(e2), Err(e3)) => {
eprintln!("zwlr_virtual_pointer_v1: {e1}");
eprintln!("zwp_virtual_keyboard_v1: {e2}");
eprintln!("org_kde_kwin_fake_input: {e3}");
panic!("neither wlroots nor kde input emulation protocol supported!")
log::warn!("zwlr_virtual_pointer_v1: {e1}");
log::warn!("zwp_virtual_keyboard_v1: {e2}");
log::warn!("org_kde_kwin_fake_input: {e3}");
log::error!("neither wlroots nor kde input emulation protocol supported!");
return Err(anyhow!("could not create event consumer"));
}
_ => {
panic!()
@@ -85,86 +87,76 @@ impl App {
};
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
let seat: wl_seat::WlSeat = globals.bind(&qh, 7..=8, ()).unwrap();
let mut app = App {
input_for_client,
seat,
event_rx,
virtual_input_manager,
let mut consumer = WlrootsConsumer {
state: State {
keymap: None,
input_for_client,
seat,
virtual_input_manager,
qh,
},
queue,
qh,
};
for client in clients {
app.add_client(client);
while consumer.state.keymap.is_none() {
consumer.queue.blocking_dispatch(&mut consumer.state).unwrap();
}
app
// let fd = unsafe { &File::from_raw_fd(consumer.state.keymap.unwrap().1.as_raw_fd()) };
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
// log::debug!("{:?}", &mmap[..100]);
Ok(consumer)
}
}
pub fn run(&mut self) {
loop {
let (event, client) = self.event_rx.recv().expect("event receiver unavailable");
if let Some(virtual_input) = self.input_for_client.get(&client) {
virtual_input.consume_event(event).unwrap();
if let Err(e) = self.queue.flush() {
eprintln!("{}", e);
}
}
}
}
fn add_client(&mut self, client: Client) {
impl State {
fn add_client(&mut self, client: ClientHandle) {
// create virtual input devices
match &self.virtual_input_manager {
VirtualInputManager::Wlroots { vpm, vkm } => {
let pointer: Vp = vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
// receive keymap from device
eprint!("\rconnecting to {} ", client.addr);
let mut attempts = 0;
let data = loop {
let result = request::request_data(client.addr, Request::KeyMap);
eprint!("\rconnecting to {} ", client.addr);
for _ in 0..attempts {
eprint!(".");
}
match result {
Ok(data) => break data,
Err(e) => {
eprint!(" - {}", e);
}
}
io::stderr().flush().unwrap();
thread::sleep(Duration::from_millis(500));
attempts += 1;
};
eprint!("\rconnecting to {} ", client.addr);
for _ in 0..attempts {
eprint!(".");
// TODO: use server side keymap
if let Some((format, fd, size)) = self.keymap.as_ref() {
keyboard.keymap(*format, fd.as_raw_fd(), *size);
} else {
panic!("no keymap");
}
eprintln!(" done! ");
// TODO use shm_open
let f = tempfile::tempfile().unwrap();
let mut buf = BufWriter::new(&f);
buf.write_all(&data[..]).unwrap();
buf.flush().unwrap();
keyboard.keymap(1, f.as_raw_fd(), data.len() as u32);
let vinput = VirtualInput::Wlroots { pointer, keyboard };
self.input_for_client.insert(client.handle, vinput);
self.input_for_client.insert(client, vinput);
}
VirtualInputManager::Kde { fake_input } => {
let fake_input = fake_input.clone();
let vinput = VirtualInput::Kde { fake_input };
self.input_for_client.insert(client.handle, vinput);
self.input_for_client.insert(client, vinput);
}
}
}
}
impl EventConsumer for WlrootsConsumer {
fn consume(&self, event: Event, client_handle: ClientHandle) {
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) {
virtual_input.consume_event(event).unwrap();
if let Err(e) = self.queue.flush() {
log::error!("{}", e);
}
}
}
fn notify(&mut self, client_event: ClientEvent) {
if let ClientEvent::Create(client, _) = client_event {
self.state.add_client(client);
if let Err(e) = self.queue.flush() {
log::error!("{}", e);
}
}
}
}
enum VirtualInput {
Wlroots { pointer: Vp, keyboard: Vk },
Kde { fake_input: OrgKdeKwinFakeInput },
@@ -184,7 +176,6 @@ impl VirtualInput {
keyboard: _,
} => {
pointer.motion(time, relative_x, relative_y);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
@@ -202,7 +193,6 @@ impl VirtualInput {
keyboard: _,
} => {
pointer.button(time, button, state);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
@@ -261,36 +251,64 @@ impl VirtualInput {
VirtualInput::Kde { fake_input: _ } => {}
},
},
Event::Release() => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.modifiers(77, 0, 0, 0);
keyboard.modifiers(0, 0, 0, 0);
}
VirtualInput::Kde { fake_input: _ } => {}
},
_ => {},
}
Ok(())
}
}
delegate_noop!(App: Vp);
delegate_noop!(App: Vk);
delegate_noop!(App: VpManager);
delegate_noop!(App: VkManager);
delegate_noop!(App: wl_seat::WlSeat);
delegate_noop!(App: OrgKdeKwinFakeInput);
delegate_noop!(State: Vp);
delegate_noop!(State: Vk);
delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager);
delegate_noop!(State: OrgKdeKwinFakeInput);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for App {
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_: &mut App,
_: &mut State,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<App>,
_: &QueueHandle<State>,
) {
}
}
impl Dispatch<WlKeyboard, ()> for State {
fn event(
state: &mut Self,
_: &WlKeyboard,
event: <WlKeyboard as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
wl_keyboard::Event::Keymap { format, fd, size } => {
state.keymap = Some((u32::from(format), fd, size));
}
_ => {},
}
}
}
impl Dispatch<WlSeat, ()> for State {
fn event(
_: &mut Self,
seat: &WlSeat,
event: <WlSeat as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qhandle: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qhandle, ());
}
}
}
}

View File

@@ -1,49 +1,57 @@
use std::{ptr, sync::mpsc::Receiver};
use std::ptr;
use x11::{xlib, xtest};
use crate::{
client::{Client, ClientHandle},
event::Event,
client::ClientHandle,
event::Event, consumer::EventConsumer,
};
fn open_display() -> Option<*mut xlib::Display> {
unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None,
display => Some(display),
pub struct X11Consumer {
display: *mut xlib::Display,
}
impl X11Consumer {
pub fn new() -> Self {
let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None,
display => Some(display),
}
};
let display = display.expect("could not open display");
Self { display }
}
fn relative_motion(&self, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
xlib::XFlush(self.display);
}
}
}
fn relative_motion(display: *mut xlib::Display, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(display, dx, dy, 0, 0);
xlib::XFlush(display);
}
}
pub fn run(event_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
let display = match open_display() {
None => panic!("could not open display!"),
Some(display) => display,
};
loop {
match event_rx.recv().expect("event receiver unavailable").0 {
impl EventConsumer for X11Consumer {
fn consume(&self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
crate::event::PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
relative_motion(display, relative_x as i32, relative_y as i32);
self.relative_motion(relative_x as i32, relative_y as i32);
}
crate::event::PointerEvent::Button { .. } => {}
crate::event::PointerEvent::Axis { .. } => {}
crate::event::PointerEvent::Frame {} => {}
},
Event::Keyboard(_) => {}
Event::Release() => {}
_ => {}
}
}
fn notify(&mut self, _: crate::client::ClientEvent) {
// for our purposes it does not matter what client sent the event
}
}

View File

@@ -1,9 +1,15 @@
use std::sync::mpsc::Receiver;
use crate::consumer::EventConsumer;
use crate::{event::Event, client::{ClientHandle, Client}};
pub struct DesktopPortalConsumer {}
pub(crate) fn run(_consume_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
todo!()
impl DesktopPortalConsumer {
pub fn new() -> Self { Self { } }
}
impl EventConsumer for DesktopPortalConsumer {
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) {
log::error!("xdg_desktop_portal backend not yet implemented!");
}
fn notify(&mut self, _: crate::client::ClientEvent) {}
}

View File

@@ -1,21 +1,18 @@
use crate::{
client::{Client, ClientHandle, Position},
request,
};
use crate::{client::{ClientHandle, Position, ClientEvent}, producer::EventProducer};
use mio::{event::Source, unix::SourceFd};
use std::{os::fd::RawFd, vec::Drain, io::ErrorKind};
use memmap::MmapOptions;
use anyhow::{anyhow, Result};
use std::{
fs::File,
io::{BufWriter, Write},
os::unix::prelude::{AsRawFd, FromRawFd},
rc::Rc,
sync::mpsc::SyncSender,
thread,
time::Duration,
};
use wayland_protocols::wp::{
use wayland_protocols::{wp::{
keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
@@ -28,7 +25,7 @@ use wayland_protocols::wp::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
},
};
}, xdg::xdg_output::zv1::client::{zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::{self, ZxdgOutputV1}}};
use wayland_protocols_wlr::layer_shell::v1::client::{
zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1},
@@ -36,14 +33,14 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
};
use wayland_client::{
backend::WaylandError,
backend::{WaylandError, ReadEventsGuard},
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm,
wl_shm_pool, wl_surface,
wl_shm_pool, wl_surface, wl_output::{self, WlOutput},
},
Connection, Dispatch, DispatchError, QueueHandle, WEnum,
Connection, Dispatch, DispatchError, QueueHandle, WEnum, EventQueue,
};
use tempfile;
@@ -58,19 +55,44 @@ struct Globals {
seat: wl_seat::WlSeat,
shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1,
outputs: Vec<wl_output::WlOutput>,
xdg_output_manager: ZxdgOutputManagerV1,
}
struct App {
running: bool,
#[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 {
pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
client_for_window: Vec<(Rc<Window>, ClientHandle)>,
focused: Option<(Rc<Window>, ClientHandle)>,
g: Globals,
tx: SyncSender<(Event, ClientHandle)>,
server: request::Server,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: Vec<(ClientHandle, Event)>,
output_info: Vec<(WlOutput, OutputInfo)>,
}
pub struct WaylandEventProducer {
state: State,
queue: EventQueue<State>,
}
struct Window {
@@ -80,8 +102,13 @@ struct Window {
}
impl Window {
fn new(g: &Globals, qh: &QueueHandle<App>, pos: Position) -> Window {
let (width, height) = (1, 1440);
fn new(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();
draw(&mut file, (width, height));
let pool = g
@@ -100,8 +127,8 @@ impl Window {
let layer_surface = g.layer_shell.get_layer_surface(
&surface,
None,
Layer::Top,
Some(&output),
Layer::Overlay,
"LAN Mouse Sharing".into(),
qh,
(),
@@ -114,9 +141,9 @@ impl Window {
};
layer_surface.set_anchor(anchor);
layer_surface.set_size(1, 1440);
layer_surface.set_exclusive_zone(0);
layer_surface.set_margin(0, 0, 0, 0);
layer_surface.set_size(width, height);
layer_surface.set_exclusive_zone(-1);
layer_surface.set_margin(0,0,0,0);
surface.set_input_region(None);
surface.commit();
Window {
@@ -127,96 +154,178 @@ impl Window {
}
}
pub fn run(tx: SyncSender<(Event, ClientHandle)>, server: request::Server, clients: Vec<Client>) {
let conn = Connection::connect_to_env().expect("could not connect to wayland compositor");
let (g, mut queue) =
registry_queue_init::<App>(&conn).expect("failed to initialize wl_registry");
let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = g
.bind(&qh, 4..=5, ())
.expect("wl_compositor >= v4 not supported");
let shm: wl_shm::WlShm = g.bind(&qh, 1..=1, ()).expect("wl_shm v1 not supported");
let layer_shell: ZwlrLayerShellV1 = g
.bind(&qh, 3..=4, ())
.expect("zwlr_layer_shell_v1 >= v3 not supported - required to display a surface at the edge of the screen");
let seat: wl_seat::WlSeat = g.bind(&qh, 7..=8, ()).expect("wl_seat >= v7 not supported");
let pointer_constraints: ZwpPointerConstraintsV1 = g
.bind(&qh, 1..=1, ())
.expect("zwp_pointer_constraints_v1 not supported");
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
.bind(&qh, 1..=1, ())
.expect("zwp_relative_pointer_manager_v1 not supported");
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = g
.bind(&qh, 1..=1, ())
.expect("zwp_keyboard_shortcuts_inhibit_manager_v1 not supported");
let g = Globals {
compositor,
shm,
layer_shell,
seat,
pointer_constraints,
relative_pointer_manager,
shortcut_inhibit_manager,
};
let client_for_window = Vec::new();
let mut app = App {
running: true,
g,
pointer_lock: None,
rel_pointer: None,
shortcut_inhibitor: None,
client_for_window,
focused: None,
tx,
server,
qh,
};
for client in clients {
app.add_client(client.handle, client.pos);
impl Drop for Window {
fn drop(&mut self) {
log::debug!("destroying window!");
self.layer_surface.destroy();
self.surface.destroy();
self.buffer.destroy();
}
}
while app.running {
match queue.blocking_dispatch(&mut app) {
Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => {
eprintln!("Wayland Error: {}", e);
thread::sleep(Duration::from_millis(500));
}
Err(DispatchError::Backend(e)) => {
panic!("{}", e);
}
Err(DispatchError::BadMessage {
sender_id,
interface,
opcode,
}) => {
panic!("bad message {}, {} , {}", sender_id, interface, opcode);
}
}
}
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)) {
let mut buf = BufWriter::new(f);
for _ in 0..height {
for _ in 0..width {
buf.write_all(&0x44FbF1C7u32.to_ne_bytes()).unwrap();
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
}
}
}
impl App {
impl WaylandEventProducer {
pub fn new() -> Result<Self> {
let conn = match Connection::connect_to_env() {
Ok(c) => c,
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 compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) {
Ok(compositor) => compositor,
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, ()) {
Ok(wl_shm) => wl_shm,
Err(_) => return Err(anyhow!("wl_shm v1 not supported")),
};
let layer_shell: ZwlrLayerShellV1 = match g.bind(&qh, 3..=4, ()) {
Ok(layer_shell) => layer_shell,
Err(_) => return Err(anyhow!("zwlr_layer_shell_v1 >= v3 not supported - required to display a surface at the edge of the screen")),
};
let seat: wl_seat::WlSeat = match g.bind(&qh, 7..=8, ()) {
Ok(wl_seat) => wl_seat,
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
};
let pointer_constraints: ZwpPointerConstraintsV1 = match g.bind(&qh, 1..=1, ()) {
Ok(pointer_constraints) => pointer_constraints,
Err(_) => return Err(anyhow!("zwp_pointer_constraints_v1 not supported")),
};
let relative_pointer_manager: ZwpRelativePointerManagerV1 = match g.bind(&qh, 1..=1, ()) {
Ok(relative_pointer_manager) => relative_pointer_manager,
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
};
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = match g.bind(&qh, 1..=1, ()) {
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
Err(_) => return Err(anyhow!("zwp_keyboard_shortcuts_inhibit_manager_v1 not supported")),
};
let outputs = vec![];
let g = Globals {
compositor,
shm,
layer_shell,
seat,
pointer_constraints,
relative_pointer_manager,
shortcut_inhibit_manager,
outputs,
xdg_output_manager,
};
// flush outgoing events
queue.flush()?;
// prepare reading wayland events
let read_guard = queue.prepare_read()?;
let wayland_fd = read_guard.connection_fd().as_raw_fd();
std::mem::drop(read_guard);
let mut state = State {
g,
pointer_lock: None,
rel_pointer: None,
shortcut_inhibitor: None,
client_for_window: Vec::new(),
focused: None,
qh,
wayland_fd,
read_guard: None,
pending_events: vec![],
output_info: 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);
Ok(WaylandEventProducer { queue, state })
}
}
impl State {
fn grab(
&mut self,
surface: &wl_surface::WlSurface,
pointer: &wl_pointer::WlPointer,
serial: u32,
qh: &QueueHandle<App>,
qh: &QueueHandle<State>,
) {
let (window, _) = self.focused.as_ref().unwrap();
@@ -235,7 +344,7 @@ impl App {
surface,
pointer,
None,
Lifetime::Oneshot,
Lifetime::Persistent,
qh,
(),
));
@@ -263,7 +372,10 @@ impl App {
fn ungrab(&mut self) {
// get focused client
let (window, _client) = self.focused.as_ref().unwrap();
let (window, _client) = match self.focused.as_ref() {
Some(focused) => focused,
None => return,
};
// ungrab surface
window
@@ -271,7 +383,7 @@ impl App {
.set_keyboard_interactivity(KeyboardInteractivity::None);
window.surface.commit();
// release pointer
// destroy pointer lock
if let Some(pointer_lock) = &self.pointer_lock {
pointer_lock.destroy();
self.pointer_lock = None;
@@ -283,7 +395,7 @@ impl App {
self.rel_pointer = None;
}
// release shortcut inhibitor
// destroy shortcut inhibitor
if let Some(shortcut_inhibitor) = &self.shortcut_inhibitor {
shortcut_inhibitor.destroy();
self.shortcut_inhibitor = None;
@@ -291,12 +403,149 @@ impl App {
}
fn add_client(&mut self, client: ClientHandle, pos: Position) {
let window = Rc::new(Window::new(&self.g, &self.qh, pos));
self.client_for_window.push((window, client));
let outputs = get_output_configuration(self, pos);
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 Dispatch<wl_seat::WlSeat, ()> for App {
impl Source for WaylandEventProducer {
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 {
match self.state.read_guard.take().unwrap().read() {
Ok(_) => true,
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => false,
Err(WaylandError::Io(e)) => {
log::error!("error reading from wayland socket: {e}");
false
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland protocol violation: {e}")
}
}
}
fn prepare_read(&mut self) {
match self.queue.prepare_read() {
Ok(r) => self.state.read_guard = Some(r),
Err(WaylandError::Io(e)) => {
log::error!("error preparing read from wayland socket: {e}")
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland Protocol violation: {e}")
}
};
}
fn dispatch_events(&mut self) {
match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => {
log::error!("Wayland Error: {}", e);
}
Err(DispatchError::Backend(e)) => {
panic!("backend error: {}", e);
}
Err(DispatchError::BadMessage {
sender_id,
interface,
opcode,
}) => {
panic!("bad message {}, {} , {}", sender_id, interface, opcode);
}
}
}
fn flush_events(&mut self) {
// flush outgoing events
match self.queue.flush() {
Ok(_) => (),
Err(e) => match e {
WaylandError::Io(e) => {
log::error!("error writing to wayland socket: {e}")
},
WaylandError::Protocol(e) => {
panic!("wayland protocol violation: {e}")
},
},
}
}
}
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) {
if let ClientEvent::Create(handle, pos) = client_event {
self.state.add_client(handle, pos);
}
if let ClientEvent::Destroy(handle) = client_event {
loop {
// remove all windows corresponding to this client
if let Some(i) = self.state.client_for_window.iter().position(|(_,c)| *c == handle) {
self.state.client_for_window.remove(i);
self.state.focused = None;
} else {
break
}
}
}
self.flush_events();
}
fn release(&mut self) {
self.state.ungrab();
self.flush_events();
}
}
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event(
_: &mut Self,
seat: &wl_seat::WlSeat,
@@ -319,7 +568,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for App {
}
}
impl Dispatch<wl_pointer::WlPointer, ()> for App {
impl Dispatch<wl_pointer::WlPointer, ()> for State {
fn event(
app: &mut Self,
pointer: &wl_pointer::WlPointer,
@@ -328,6 +577,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for App {
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_pointer::Event::Enter {
serial,
@@ -336,24 +586,27 @@ impl Dispatch<wl_pointer::WlPointer, ()> for App {
surface_y: _,
} => {
// get client corresponding to the focused surface
log::trace!("produce: enter()");
{
let (window, client) = app
if let Some((window, client)) = app
.client_for_window
.iter()
.find(|(w, _c)| w.surface == surface)
.unwrap();
app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial.clone(), qh);
.find(|(w, _c)| w.surface == surface) {
app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial.clone(), qh);
} else {
return;
}
}
let (_, client) = app
.client_for_window
.iter()
.find(|(w, _c)| w.surface == surface)
.unwrap();
app.tx.send((Event::Release(), *client)).unwrap();
app.pending_events.push((*client, Event::Release()));
}
wl_pointer::Event::Leave { .. } => {
log::trace!("produce: leave()");
app.ungrab();
}
wl_pointer::Event::Button {
@@ -362,43 +615,43 @@ impl Dispatch<wl_pointer::WlPointer, ()> for App {
button,
state,
} => {
log::trace!("produce: button()");
let (_, client) = app.focused.as_ref().unwrap();
app.tx
.send((
Event::Pointer(PointerEvent::Button {
time,
button,
state: u32::from(state),
}),
*client,
))
.unwrap();
app.pending_events.push((
*client,
Event::Pointer(PointerEvent::Button {
time,
button,
state: u32::from(state),
}),
));
}
wl_pointer::Event::Axis { time, axis, value } => {
log::trace!("produce: scroll()");
let (_, client) = app.focused.as_ref().unwrap();
app.tx
.send((
Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
}),
*client,
))
.unwrap();
app.pending_events.push((
*client,
Event::Pointer(PointerEvent::Axis {
time,
axis: u32::from(axis) as u8,
value,
}),
));
}
wl_pointer::Event::Frame {} => {
log::trace!("produce: frame()");
let (_, client) = app.focused.as_ref().unwrap();
app.tx
.send((Event::Pointer(PointerEvent::Frame {}), *client))
.unwrap();
app.pending_events.push((
*client,
Event::Pointer(PointerEvent::Frame {}),
));
}
_ => {}
}
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for App {
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
fn event(
app: &mut Self,
_: &wl_keyboard::WlKeyboard,
@@ -419,16 +672,14 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for App {
state,
} => {
if let Some(client) = client {
app.tx
.send((
Event::Keyboard(KeyboardEvent::Key {
time,
key,
state: u32::from(state) as u8,
}),
*client,
))
.unwrap();
app.pending_events.push((
*client,
Event::Keyboard(KeyboardEvent::Key {
time,
key,
state: u32::from(state) as u8,
}),
));
}
}
wl_keyboard::Event::Modifiers {
@@ -439,17 +690,15 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for App {
group,
} => {
if let Some(client) = client {
app.tx
.send((
Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
}),
*client,
))
.unwrap();
app.pending_events.push((
*client,
Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
}),
));
}
if mods_depressed == 77 {
// ctrl shift super alt
@@ -462,15 +711,15 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for App {
size: _,
} => {
let fd = unsafe { &File::from_raw_fd(fd.as_raw_fd()) };
let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
app.server.offer_data(request::Request::KeyMap, mmap);
let _mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
// TODO keymap
}
_ => (),
}
}
}
impl Dispatch<ZwpRelativePointerV1, ()> for App {
impl Dispatch<ZwpRelativePointerV1, ()> for State {
fn event(
app: &mut Self,
_: &ZwpRelativePointerV1,
@@ -488,24 +737,23 @@ impl Dispatch<ZwpRelativePointerV1, ()> for App {
dy_unaccel: surface_y,
} = event
{
log::trace!("produce: motion()");
if let Some((_window, client)) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.tx
.send((
Event::Pointer(PointerEvent::Motion {
time,
relative_x: surface_x,
relative_y: surface_y,
}),
*client,
))
.unwrap();
app.pending_events.push((
*client,
Event::Pointer(PointerEvent::Motion {
time,
relative_x: surface_x,
relative_y: surface_y,
}),
));
}
}
}
}
impl Dispatch<ZwlrLayerSurfaceV1, ()> for App {
impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
fn event(
app: &mut Self,
layer_surface: &ZwlrLayerSurfaceV1,
@@ -515,25 +763,23 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for App {
_: &QueueHandle<Self>,
) {
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
let (window, _client) = app
if let Some((window, _client)) = app
.client_for_window
.iter()
.find(|(w, _c)| &w.layer_surface == layer_surface)
.unwrap();
// client corresponding to the layer_surface
let surface = &window.surface;
let buffer = &window.buffer;
surface.commit();
layer_surface.ack_configure(serial);
surface.attach(Some(&buffer), 0, 0);
surface.commit();
.find(|(w, _c)| &w.layer_surface == layer_surface) {
// client corresponding to the layer_surface
let surface = &window.surface;
let buffer = &window.buffer;
surface.attach(Some(&buffer), 0, 0);
layer_surface.ack_configure(serial);
surface.commit();
}
}
}
}
// delegate wl_registry events to App itself
// delegate_dispatch!(App: [wl_registry::WlRegistry: GlobalListContents] => App);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for App {
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_state: &mut Self,
_proxy: &wl_registry::WlRegistry,
@@ -545,18 +791,84 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for App {
}
}
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
delegate_noop!(App: wl_region::WlRegion);
delegate_noop!(App: wl_shm_pool::WlShmPool);
delegate_noop!(App: wl_compositor::WlCompositor);
delegate_noop!(App: ZwlrLayerShellV1);
delegate_noop!(App: ZwpRelativePointerManagerV1);
delegate_noop!(App: ZwpKeyboardShortcutsInhibitManagerV1);
delegate_noop!(App: ZwpPointerConstraintsV1);
delegate_noop!(State: wl_region::WlRegion);
delegate_noop!(State: wl_shm_pool::WlShmPool);
delegate_noop!(State: wl_compositor::WlCompositor);
delegate_noop!(State: ZwlrLayerShellV1);
delegate_noop!(State: ZwpRelativePointerManagerV1);
delegate_noop!(State: ZwpKeyboardShortcutsInhibitManagerV1);
delegate_noop!(State: ZwpPointerConstraintsV1);
// ignore events
delegate_noop!(App: ignore wl_shm::WlShm);
delegate_noop!(App: ignore wl_buffer::WlBuffer);
delegate_noop!(App: ignore wl_surface::WlSurface);
delegate_noop!(App: ignore ZwpKeyboardShortcutsInhibitorV1);
delegate_noop!(App: ignore ZwpLockedPointerV1);
delegate_noop!(State: ignore wl_output::WlOutput);
delegate_noop!(State: ignore ZxdgOutputManagerV1);
delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_buffer::WlBuffer);
delegate_noop!(State: ignore wl_surface::WlSurface);
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
delegate_noop!(State: ignore ZwpLockedPointerV1);

View File

@@ -1,11 +1,58 @@
use std::sync::mpsc::SyncSender;
use std::vec::Drain;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result;
use crate::{
client::{Client, ClientHandle},
client::{ClientHandle, ClientEvent},
event::Event,
request::Server,
producer::EventProducer,
};
pub fn run(_produce_tx: SyncSender<(Event, ClientHandle)>, _server: Server, _clients: Vec<Client>) {
todo!();
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 {
fn notify(&mut self, _: ClientEvent) { }
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
self.pending_events.drain(..)
}
fn release(&mut self) { }
}
impl WindowsProducer {
pub(crate) fn new() -> Self {
Self {
pending_events: vec![],
}
}
}

View File

@@ -1,9 +1,55 @@
use std::sync::mpsc::SyncSender;
use std::vec::Drain;
use crate::client::Client;
use crate::event::Event;
use crate::request::Server;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result;
pub fn run(_produce_tx: SyncSender<(Event, u32)>, _request_server: Server, _clients: Vec<Client>) {
todo!()
use crate::producer::EventProducer;
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
pub struct X11Producer {
pending_events: Vec<(ClientHandle, Event)>,
}
impl X11Producer {
pub fn new() -> 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 {
fn notify(&mut self, _: ClientEvent) { }
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
self.pending_events.drain(..)
}
fn release(&mut self) {}
}

View File

@@ -1,8 +1,8 @@
use std::{net::SocketAddr, error::Error, fmt::Display, sync::{Arc, atomic::{AtomicBool, Ordering, AtomicU32}, RwLock}};
use std::{net::SocketAddr, collections::{HashSet, hash_set::Iter}, fmt::Display, time::{Instant, Duration}, iter::Cloned};
use crate::{config::{self, DEFAULT_PORT}, dns};
use serde::{Serialize, Deserialize};
#[derive(Eq, Hash, PartialEq, Clone, Copy)]
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
Left,
Right,
@@ -10,97 +10,202 @@ pub enum Position {
Bottom,
}
#[derive(Clone, Copy)]
pub struct Client {
pub addr: SocketAddr,
pub pos: Position,
pub handle: ClientHandle,
}
impl Client {
pub fn handle(&self) -> ClientHandle {
return self.handle;
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,
}
}
}
pub enum ClientEvent {
Create(Client),
Destroy(Client),
impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
})
}
}
pub struct ClientManager {
next_id: AtomicU32,
clients: RwLock<Vec<Client>>,
subscribers: RwLock<Vec<Arc<AtomicBool>>>,
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client {
/// handle to refer to the client.
/// This way any event consumer / producer backend does not
/// need to know anything about a client other than its handle.
pub handle: ClientHandle,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// all socket addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub addrs: HashSet<SocketAddr>,
/// position of a client on screen
pub pos: Position,
}
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
UpdatePos(ClientHandle, Position),
AddAddr(ClientHandle, SocketAddr),
RemoveAddr(ClientHandle, SocketAddr),
}
pub type ClientHandle = u32;
#[derive(Debug)]
struct ClientConfigError;
impl Display for ClientConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "neither ip nor hostname specified")
}
pub struct ClientManager {
/// probably not beneficial to use a hashmap here
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 Error for ClientConfigError {}
impl ClientManager {
fn add_client(&self, client: &config::Client, pos: Position) -> Result<(), Box<dyn Error>> {
let ip = match client.ip {
Some(ip) => ip,
None => match &client.host_name {
Some(host_name) => dns::resolve(host_name)?,
None => return Err(Box::new(ClientConfigError{})),
},
};
let addr = SocketAddr::new(ip, client.port.unwrap_or(DEFAULT_PORT));
self.register_client(addr, pos);
Ok(())
}
fn notify(&self) {
for subscriber in self.subscribers.read().unwrap().iter() {
subscriber.store(true, Ordering::SeqCst);
pub fn new() -> Self {
Self {
clients: vec![],
next_client_id: 0,
last_ping: vec![],
last_seen: vec![],
last_replied: vec![],
}
}
fn new_id(&self) -> ClientHandle {
let id = self.next_id.load(Ordering::Acquire);
self.next_id.store(id + 1, Ordering::Release);
id as ClientHandle
/// add a new client to this manager
pub fn add_client(&mut self, addrs: HashSet<SocketAddr>, pos: Position) -> ClientHandle {
let handle = self.next_id();
// we dont know, which IP is initially active
let active_addr = None;
// store the client
let client = Client { handle, active_addr, addrs, pos };
self.clients.push(client);
self.last_ping.push((handle, None));
self.last_seen.push((handle, None));
self.last_replied.push((handle, None));
handle
}
pub fn new(config: &config::Config) -> Result<Self, Box<dyn Error>> {
let client_manager = ClientManager {
next_id: AtomicU32::new(0),
clients: RwLock::new(Vec::new()),
subscribers: RwLock::new(vec![]),
};
// add clients from config
for (client, pos) in config.clients.iter() {
client_manager.add_client(&client, *pos)?;
/// add a socket address to the given client
pub fn add_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.addrs.insert(addr);
}
Ok(client_manager)
}
pub fn register_client(&self, addr: SocketAddr, pos: Position) {
let handle = self.new_id();
let client = Client { addr, pos, handle };
self.clients.write().unwrap().push(client);
self.notify();
/// 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 get_clients(&self) -> Vec<Client> {
self.clients.read().unwrap().clone()
pub fn set_default_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.active_addr = Some(addr)
}
}
pub fn subscribe(&self, subscriber: Arc<AtomicBool>) {
self.subscribers.write().unwrap().push(subscriber);
/// 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)?.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)?.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)?.1;
last_replied.map(|t| t.elapsed())
}
pub fn reset_last_ping(&mut self, client: ClientHandle) {
if let Some(c) = self.last_ping
.iter_mut()
.find(|(c, _)| *c == client) {
c.1 = Some(Instant::now());
}
}
pub fn reset_last_seen(&mut self, client: ClientHandle) {
if let Some(c) = self.last_seen
.iter_mut()
.find(|(c, _)| *c == client) {
c.1 = Some(Instant::now());
}
}
pub fn reset_last_replied(&mut self, client: ClientHandle) {
if let Some(c) = self.last_replied
.iter_mut()
.find(|(c, _)| *c == client) {
c.1 = Some(Instant::now());
}
}
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
self.clients
.iter()
.find(|c| c.addrs.contains(&addr))
.map(|c| c.handle)
}
pub fn remove_client(&mut self, client: ClientHandle) {
if let Some(i) = self.clients.iter().position(|c| c.handle == client) {
self.clients.remove(i);
self.last_ping.remove(i);
self.last_seen.remove(i);
self.last_replied.remove(i);
}
}
fn next_id(&mut self) -> ClientHandle {
let handle = self.next_client_id;
self.next_client_id += 1;
handle
}
fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a Client> {
self.clients
.iter()
.find(|c| c.handle == client)
}
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,19 +1,21 @@
use serde_derive::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use core::fmt;
use std::net::IpAddr;
use std::collections::HashSet;
use std::net::{IpAddr, SocketAddr};
use std::{error::Error, fs};
use std::env;
use toml;
use crate::client::Position;
use crate::dns;
pub const DEFAULT_PORT: u16 = 4242;
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml {
pub port: Option<u16>,
pub backend: Option<String>,
pub frontend: Option<String>,
pub left: Option<Client>,
pub right: Option<Client>,
pub top: Option<Client>,
@@ -23,7 +25,7 @@ pub struct ConfigToml {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Client {
pub host_name: Option<String>,
pub ip: Option<IpAddr>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
}
@@ -62,8 +64,14 @@ fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> {
Ok(None)
}
#[derive(PartialEq, Eq)]
pub enum Frontend {
Gtk,
Cli,
}
pub struct Config {
pub backend: Option<String>,
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(Client, Position)>,
}
@@ -73,19 +81,31 @@ impl Config {
let config_path = "config.toml";
let config_toml = match ConfigToml::new(config_path) {
Err(e) => {
eprintln!("config.toml: {e}");
eprintln!("Continuing without config file ...");
log::error!("config.toml: {e}");
log::warn!("Continuing without config file ...");
None
},
Ok(c) => Some(c),
};
let backend = match find_arg("--backend")? {
let frontend = match find_arg("--frontend")? {
None => match &config_toml {
Some(c) => c.backend.clone(),
Some(c) => c.frontend.clone(),
None => None,
},
backend => backend,
frontend => frontend,
};
let frontend = match frontend {
#[cfg(all(unix, feature = "gtk"))]
None => Frontend::Gtk,
#[cfg(any(not(feature = "gtk"), not(unix)))]
None => Frontend::Cli,
Some(s) => match s.as_str() {
"cli" => Frontend::Cli,
"gtk" => Frontend::Gtk,
_ => Frontend::Cli,
}
};
let port = match find_arg("--port")? {
@@ -113,6 +133,43 @@ impl Config {
}
}
Ok(Config { backend, clients, port })
Ok(Config { frontend, clients, port })
}
pub fn get_clients(&self) -> Vec<(HashSet<SocketAddr>, Option<String>, Position)> {
self.clients.iter().map(|(c,p)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
// add ips from config
let config_ips: Vec<IpAddr> = if let Some(ips) = c.ips.as_ref() {
ips.iter().cloned().collect()
} else {
vec![]
};
let host_name = c.host_name.clone();
// add ips from dns lookup
let dns_ips = match host_name.as_ref() {
None => vec![],
Some(host_name) => match dns::resolve(host_name) {
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,9 +1,8 @@
use std::{thread::{JoinHandle, self}, sync::mpsc::Receiver, error::Error};
#[cfg(unix)]
use std::env;
use crate::{backend::consumer, client::{Client, ClientHandle}, event::Event};
use anyhow::Result;
use crate::{backend::consumer, client::{ClientHandle, ClientEvent}, event::Event};
#[cfg(unix)]
#[derive(Debug)]
@@ -14,63 +13,87 @@ enum Backend {
Libei,
}
pub fn start(consume_rx: Receiver<(Event, ClientHandle)>, clients: Vec<Client>, backend: Option<String>) -> Result<JoinHandle<()>, Box<dyn Error>> {
pub trait EventConsumer {
/// Event corresponding to an abstract `client_handle`
fn consume(&self, event: Event, client_handle: ClientHandle);
/// Event corresponding to a configuration change
fn notify(&mut self, client_event: ClientEvent);
}
pub fn create() -> Result<Box<dyn EventConsumer>> {
#[cfg(windows)]
let _backend = backend;
return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
Ok(thread::Builder::new()
.name("event consumer".into())
.spawn(move || {
#[cfg(windows)]
consumer::windows::run(consume_rx, clients);
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => Backend::X11,
"wayland" => {
match backend {
Some(backend) => match backend.as_str() {
"wlroots" => Backend::Wlroots,
"libei" => Backend::Libei,
"xdg_desktop_portal" => Backend::RemoteDesktopPortal,
backend => panic!("invalid backend: {}", backend)
}
// default to wlroots backend for now
_ => Backend::Wlroots,
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => {
log::info!("XDG_SESSION_TYPE = x11 -> using x11 event consumer");
Backend::X11
}
"wayland" => {
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event consumer");
match env::var("XDG_CURRENT_DESKTOP") {
Ok(current_desktop) => match current_desktop.as_str() {
"gnome" => {
log::info!("XDG_CURRENT_DESKTOP = gnome -> using libei backend");
Backend::Libei
}
"KDE" => {
log::info!("XDG_CURRENT_DESKTOP = KDE -> using xdg_desktop_portal backend");
Backend::RemoteDesktopPortal
}
"sway" => {
log::info!("XDG_CURRENT_DESKTOP = sway -> using wlroots backend");
Backend::Wlroots
}
"Hyprland" => {
log::info!("XDG_CURRENT_DESKTOP = Hyprland -> using wlroots backend");
Backend::Wlroots
}
_ => {
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
Backend::Wlroots
}
}
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[cfg(unix)]
match backend {
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
consumer::libei::run(consume_rx, clients);
},
Backend::RemoteDesktopPortal => {
#[cfg(not(feature = "xdg_desktop_portal"))]
panic!("feature xdg_desktop_portal not enabled");
#[cfg(feature = "xdg_desktop_portal")]
consumer::xdg_desktop_portal::run(consume_rx, clients);
},
Backend::Wlroots => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
consumer::wlroots::run(consume_rx, clients);
},
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
consumer::x11::run(consume_rx, clients);
},
// default to wlroots backend for now
_ => {
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
Backend::Wlroots
}
}
}
})?)
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[cfg(unix)]
match backend {
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
Ok(Box::new(consumer::libei::LibeiConsumer::new()))
},
Backend::RemoteDesktopPortal => {
#[cfg(not(feature = "xdg_desktop_portal"))]
panic!("feature xdg_desktop_portal not enabled");
#[cfg(feature = "xdg_desktop_portal")]
Ok(Box::new(consumer::xdg_desktop_portal::DesktopPortalConsumer::new()))
},
Backend::Wlroots => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
Ok(Box::new(consumer::wlroots::WlrootsConsumer::new()?))
},
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
Ok(Box::new(consumer::x11::X11Consumer::new()))
},
}
}

View File

@@ -1,27 +1,9 @@
use std::{error::Error, fmt::Display, net::IpAddr};
use std::{error::Error, net::IpAddr};
use trust_dns_resolver::Resolver;
#[derive(Debug, Clone)]
struct InvalidConfigError;
#[derive(Debug, Clone)]
struct DnsError {
host: String,
}
impl Error for DnsError {}
impl Display for DnsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "couldn't resolve host \"{}\"", self.host)
}
}
pub fn resolve(host: &String) -> Result<IpAddr, Box<dyn Error>> {
pub fn resolve(host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = Resolver::from_system_conf()?.lookup_ip(host)?;
match response.iter().next() {
Some(ip) => Ok(ip),
None => Err(DnsError { host: host.clone() }.into()),
}
Ok(response.iter().collect())
}

View File

@@ -1,7 +1,8 @@
use std::{error::Error, fmt};
use std::{error::Error, fmt::{self, Display}};
pub mod server;
#[derive(Debug, Clone, Copy)]
pub enum PointerEvent {
Motion {
time: u32,
@@ -21,6 +22,7 @@ pub enum PointerEvent {
Frame {},
}
#[derive(Debug, Clone, Copy)]
pub enum KeyboardEvent {
Key {
time: u32,
@@ -35,14 +37,46 @@ pub enum KeyboardEvent {
},
}
#[derive(Debug, Clone, Copy)]
pub enum Event {
Pointer(PointerEvent),
Keyboard(KeyboardEvent),
Release(),
Ping(),
Pong(),
}
unsafe impl Send for Event {}
unsafe impl Sync for Event {}
impl Display for PointerEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PointerEvent::Motion { time: _ , relative_x, 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()"),
}
}
}
impl Display for KeyboardEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyboardEvent::Key { 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})"),
}
}
}
impl Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k),
Event::Release() => write!(f, "release"),
Event::Ping() => write!(f, "ping"),
Event::Pong() => write!(f, "pong"),
}
}
}
impl Event {
fn event_type(&self) -> EventType {
@@ -50,6 +84,8 @@ impl Event {
Self::Pointer(_) => EventType::POINTER,
Self::Keyboard(_) => EventType::KEYBOARD,
Self::Release() => EventType::RELEASE,
Self::Ping() => EventType::PING,
Self::Pong() => EventType::PONG,
}
}
}
@@ -88,6 +124,8 @@ enum EventType {
POINTER,
KEYBOARD,
RELEASE,
PING,
PONG,
}
impl TryFrom<u8> for PointerEventType {
@@ -127,6 +165,8 @@ impl Into<Vec<u8>> for &Event {
Event::Pointer(p) => p.into(),
Event::Keyboard(k) => k.into(),
Event::Release() => vec![],
Event::Ping() => vec![],
Event::Pong() => vec![],
};
vec![event_id, event_data].concat()
}
@@ -153,6 +193,8 @@ impl TryFrom<Vec<u8>> for Event {
i if i == (EventType::POINTER as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::KEYBOARD as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::RELEASE as u8) => Ok(Event::Release()),
i if i == (EventType::PING as u8) => Ok(Event::Ping()),
i if i == (EventType::PONG as u8) => Ok(Event::Pong()),
_ => Err(Box::new(ProtocolError {
msg: format!("invalid event_id {}", event_id),
})),

View File

@@ -1,164 +1,320 @@
use anyhow::Result;
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::{
collections::HashMap,
error::Error,
net::{SocketAddr, UdpSocket},
sync::{
atomic::{AtomicBool, Ordering},
mpsc::{Receiver, SyncSender},
Arc,
},
thread::{self, JoinHandle},
};
use crate::{client::{ClientHandle, ClientManager}, ioutils::{ask_confirmation, ask_position}};
use std::{net::SocketAddr, io::ErrorKind};
use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}, dns};
use super::Event;
pub struct Server {
listen_addr: SocketAddr,
sending: Arc<AtomicBool>,
/// 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) -> Result<Self, Box<dyn Error>> {
let listen_addr = SocketAddr::new("0.0.0.0".parse()?, port);
let sending = Arc::new(AtomicBool::new(false));
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 {
listen_addr,
sending,
poll, socket, consumer, producer,
#[cfg(not(windows))]
signals, frontend,
client_manager,
state: State::Receiving,
})
}
pub fn run(
&self,
client_manager: Arc<ClientManager>,
produce_rx: Receiver<(Event, ClientHandle)>,
consume_tx: SyncSender<(Event, ClientHandle)>,
) -> Result<(JoinHandle<Result<()>>, JoinHandle<Result<()>>), Box<dyn Error>> {
let udp_socket = UdpSocket::bind(self.listen_addr)?;
let rx = udp_socket.try_clone()?;
let tx = udp_socket;
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?")
}
}
}
}
let sending = self.sending.clone();
let clients_updated = Arc::new(AtomicBool::new(true));
client_manager.subscribe(clients_updated.clone());
let client_manager_clone = client_manager.clone();
pub fn add_client(&mut self, addr: HashSet<SocketAddr>, pos: Position) {
let client = self.client_manager.add_client(addr, pos);
log::debug!("add_client {client}");
self.producer.notify(ClientEvent::Create(client, pos));
self.consumer.notify(ClientEvent::Create(client, pos));
}
let receiver = thread::Builder::new()
.name("event receiver".into())
.spawn(move || {
let mut client_for_socket = HashMap::new();
pub fn remove_client(&mut self, host: String, port: u16) {
if let Ok(ips) = dns::resolve(host.as_str()) {
if let Some(ip) = ips.iter().next() {
let addr = SocketAddr::new(*ip, port);
if let Some(handle) = self.client_manager.get_client(addr) {
log::debug!("remove_client {handle}");
self.client_manager.remove_client(handle);
self.producer.notify(ClientEvent::Destroy(handle));
self.consumer.notify(ClientEvent::Destroy(handle));
}
}
}
}
loop {
let (event, addr) = match Server::receive_event(&rx) {
Ok(e) => e,
Err(e) => {
eprintln!("{}", e);
continue;
}
};
if let Ok(_) = clients_updated.compare_exchange(
true,
false,
Ordering::SeqCst,
Ordering::SeqCst,
) {
clients_updated.store(false, Ordering::SeqCst);
client_for_socket.clear();
println!("updating clients: ");
for client in client_manager_clone.get_clients() {
println!("{}: {}", client.handle, client.addr);
client_for_socket.insert(client.addr, client.handle);
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());
let client_handle = match client_for_socket.get(&addr) {
Some(c) => *c,
None => {
eprint!("Allow connection from {:?}? ", addr);
if ask_confirmation(false)? {
client_manager_clone.register_client(addr, ask_position()?);
} else {
eprintln!("rejecting client: {:?}?", addr);
// 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);
}
// 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;
}
continue;
}
};
State::Receiving => {
// consume event
self.consumer.consume(event, handle);
// There is a race condition between loading this
// value and handling the event:
// In the meantime a event could be produced, which
// should theoretically disable receiving of events.
//
// This is however not a huge problem, as some
// events that make it through are not a large problem
if sending.load(Ordering::Acquire) {
// ignore received events when in sending state
// if release event is received, switch state to receiving
if let Event::Release() = event {
sending.store(false, Ordering::Release);
consume_tx
.send((event, client_handle))
.expect("event consumer unavailable");
// 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);
}
}
}
} else {
if let Event::Release() = event {
sending.store(false, Ordering::Release);
}
// we retrieve all events
consume_tx
.send((event, client_handle))
.expect("event consumer unavailable");
}
}
})?;
let sending = self.sending.clone();
let mut socket_for_client = HashMap::new();
for client in client_manager.get_clients() {
socket_for_client.insert(client.handle, client.addr);
}
}
let sender = thread::Builder::new()
.name("event sender".into())
.spawn(move || {
loop {
let (event, client_handle) =
produce_rx.recv().expect("event producer unavailable");
let addr = match socket_for_client.get(&client_handle) {
Some(addr) => addr,
None => continue,
};
if sending.load(Ordering::Acquire) {
Server::send_event(&tx, event, *addr);
} else {
// only accept enter event
if let Event::Release() = event {
// set state to sending, to ignore incoming events
// and enable sending of events
sending.store(true, Ordering::Release);
Server::send_event(&tx, event, *addr);
}
}
}
})?;
Ok((receiver, sender))
}
fn send_event(tx: &UdpSocket, e: Event, addr: SocketAddr) {
fn handle_producer_rx(&mut self) {
let events = self.producer.read_events();
let mut should_release = false;
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
}
// release mouse if client didnt respond to the first ping
if last_ping.is_some() && last_ping.unwrap() < Duration::from_secs(1) {
should_release = true;
}
// 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);
}
}
// send additional release event, in case client is still in sending mode
if let Err(e) = Self::send_event(&self.socket, Event::Release(), addr) {
if e.kind() != ErrorKind::WouldBlock {
log::error!("udp send: {}", e);
}
}
}
} else {
// TODO should repeat dns lookup
}
}
if should_release && self.state != State::Receiving {
log::info!("client not responding - releasing pointer");
self.producer.release();
self.state = State::Receiving;
}
}
fn handle_frontend_rx(&mut self) -> bool {
loop {
match self.frontend.read_event() {
Ok(event) => match event {
FrontendEvent::AddClient(host, port, pos) => {
if let Ok(ips) = dns::resolve(host.as_str()) {
let addrs = ips.iter().map(|i| SocketAddr::new(*i, port));
self.add_client(HashSet::from_iter(addrs), pos);
}
}
FrontendEvent::DelClient(host, port) => self.remove_client(host, port),
FrontendEvent::Shutdown() => {
log::info!("terminating gracefully...");
return true;
},
FrontendEvent::ChangePort(_) => todo!(),
FrontendEvent::AddIp(_, _) => todo!(),
}
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();
if let Err(e) = tx.send_to(&data[..], addr) {
eprintln!("{}", e);
}
// 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(rx: &UdpSocket) -> Result<(Event, SocketAddr), Box<dyn Error>> {
fn receive_event(&self) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
let mut buf = vec![0u8; 22];
match rx.recv_from(&mut buf) {
match self.socket.recv_from(&mut buf) {
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
Err(e) => Err(Box::new(e)),
}

121
src/frontend.rs Normal file
View File

@@ -0,0 +1,121 @@
use std::io::{Read, Result};
use std::net::IpAddr;
use std::str;
#[cfg(unix)]
use std::{env, path::{Path, PathBuf}};
use mio::{Registry, Token, event::Source};
#[cfg(unix)]
use mio::net::UnixListener;
#[cfg(windows)]
use mio::net::TcpListener;
use serde::{Serialize, Deserialize};
use crate::client::{Client, Position};
/// cli frontend
pub mod cli;
/// gtk frontend
#[cfg(all(unix, feature = "gtk"))]
pub mod gtk;
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
ChangePort(u16),
AddClient(String, u16, Position),
DelClient(String, u16),
AddIp(String, Option<IpAddr>),
Shutdown(),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendNotify {
NotifyClientCreate(Client),
NotifyError(String),
}
pub struct FrontendAdapter {
#[cfg(windows)]
listener: TcpListener,
#[cfg(unix)]
listener: UnixListener,
#[cfg(unix)]
socket_path: PathBuf,
}
impl FrontendAdapter {
pub fn new() -> std::result::Result<Self, Box<dyn std::error::Error>> {
#[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
#[cfg(unix)]
log::debug!("remove socket: {:?}", socket_path);
#[cfg(unix)]
if socket_path.exists() {
std::fs::remove_file(&socket_path).unwrap();
}
#[cfg(unix)]
let listener = UnixListener::bind(&socket_path)?;
#[cfg(windows)]
let listener = TcpListener::bind("127.0.0.1:5252".parse().unwrap())?; // abuse tcp
let adapter = Self {
listener,
#[cfg(unix)]
socket_path,
};
Ok(adapter)
}
pub fn read_event(&mut self) -> Result<FrontendEvent>{
let (mut stream, _) = self.listener.accept()?;
let mut buf = [0u8; 128]; // FIXME
stream.read(&mut buf)?;
let json = str::from_utf8(&buf)
.unwrap()
.trim_end_matches(char::from(0)); // remove trailing 0-bytes
log::debug!("{json}");
let event = serde_json::from_str(json).unwrap();
log::debug!("{:?}", event);
Ok(event)
}
pub fn notify(&self, _event: FrontendNotify) { }
}
impl Source for FrontendAdapter {
fn register(
&mut self,
registry: &Registry,
token: Token,
interests: mio::Interest,
) -> Result<()> {
self.listener.register(registry, token, interests)
}
fn reregister(
&mut self,
registry: &Registry,
token: Token,
interests: mio::Interest,
) -> Result<()> {
self.listener.reregister(registry, token, interests)
}
fn deregister(&mut self, registry: &Registry) -> Result<()> {
self.listener.deregister(registry)
}
}
#[cfg(unix)]
impl Drop for FrontendAdapter {
fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path);
std::fs::remove_file(&self.socket_path).unwrap();
}
}

99
src/frontend/cli.rs Normal file
View File

@@ -0,0 +1,99 @@
use anyhow::Result;
use std::{thread::{self, JoinHandle}, io::Write};
#[cfg(windows)]
use std::net::SocketAddrV4;
#[cfg(unix)]
use std::{os::unix::net::UnixStream, path::Path, env};
#[cfg(windows)]
use std::net::TcpStream;
use crate::{client::Position, config::DEFAULT_PORT};
use super::FrontendEvent;
pub fn start() -> Result<JoinHandle<()>> {
#[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
Ok(thread::Builder::new()
.name("cli-frontend".to_string())
.spawn(move || {
loop {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(len) => {
if let Some(event) = parse_cmd(buf, len) {
#[cfg(unix)]
let Ok(mut stream) = UnixStream::connect(&socket_path) else {
log::error!("Could not connect to lan-mouse-socket");
continue;
};
#[cfg(windows)]
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
log::error!("Could not connect to lan-mouse-server");
continue;
};
let json = serde_json::to_string(&event).unwrap();
if let Err(e) = stream.write(json.as_bytes()) {
log::error!("error sending message: {e}");
};
if event == FrontendEvent::Shutdown() {
break;
}
}
}
Err(e) => {
log::error!("{e:?}");
break
}
}
}
})?)
}
fn parse_cmd(s: String, len: usize) -> Option<FrontendEvent> {
if len == 0 {
return Some(FrontendEvent::Shutdown())
}
let mut l = s.split_whitespace();
let cmd = l.next()?;
match cmd {
"connect" => {
let host = l.next()?.to_owned();
let pos = match l.next()? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = match l.next() {
Some(p) => match p.parse() {
Ok(p) => p,
Err(e) => {
log::error!("{e}");
return None;
}
}
None => DEFAULT_PORT,
};
Some(FrontendEvent::AddClient(host, port, pos))
}
"disconnect" => {
let host = l.next()?.to_owned();
let port = match l.next()?.parse() {
Ok(p) => p,
Err(e) => {
log::error!("{e}");
return None;
}
};
Some(FrontendEvent::DelClient(host, port))
}
_ => {
log::error!("unknown command: {s}");
None
}
}
}

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

@@ -0,0 +1,93 @@
mod window;
mod client_object;
mod client_row;
use std::{io::Result, thread::{self, JoinHandle}};
use crate::frontend::gtk::window::Window;
use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::clone, CssProvider};
use adw::Application;
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
pub fn start() -> Result<JoinHandle<glib::ExitCode>> {
thread::Builder::new()
.name("gtk-thread".into())
.spawn(gtk_main)
}
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);
app.run()
}
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) {
let window = Window::new(app);
let action_client_activate = SimpleAction::new(
"activate-client",
Some(&i32::static_variant_type()),
);
let action_client_delete = SimpleAction::new(
"delete-client",
Some(&i32::static_variant_type()),
);
action_client_activate.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("activate-client");
let index = param.unwrap()
.get::<i32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.update_client(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let index = param.unwrap()
.get::<i32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.update_client(client);
window.clients().remove(index as u32);
if window.clients().n_items() == 0 {
window.set_placeholder_visible(true);
}
}));
let actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_client_activate);
actions.add_action(&action_client_delete);
window.present();
}

View File

@@ -0,0 +1,31 @@
mod imp;
use gtk::glib::{self, Object};
use adw::subclass::prelude::*;
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(hostname: String, port: u32, active: bool, position: String) -> Self {
Object::builder()
.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 hostname: String,
pub port: u32,
pub active: bool,
pub position: String,
}

View File

@@ -0,0 +1,27 @@
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use super::ClientData;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject {
#[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,98 @@
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_from(|_, v: String| {
if v == "" { Some("hostname".into()) } else { Some(v) }
})
.bidirectional()
.sync_create()
.build();
let title_binding = client_object
.bind_property("hostname", self, "title")
.build();
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v == "" {
Some(4242)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.bidirectional()
.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,76 @@
use std::cell::RefCell;
use glib::{Binding, subclass::InitializingObject};
use adw::{prelude::*, ComboRow, ActionRow};
use adw::subclass::prelude::*;
use gtk::glib::clone;
use gtk::{glib, CompositeTemplate, Switch, Button};
#[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();
switch.activate_action("win.activate-client", 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();
button.activate_action("win.delete-client", 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 {}

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

@@ -0,0 +1,127 @@
mod imp;
use std::{path::{Path, PathBuf}, env, process, os::unix::net::UnixStream, io::Write};
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::{glib, gio, NoSelection};
use glib::{clone, Object};
use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, config::DEFAULT_PORT, client::Position};
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
}
fn new_client(&self) {
let client = ClientObject::new(String::from(""), DEFAULT_PORT as u32, false, "left".into());
self.clients().append(&client);
}
pub fn update_client(&self, client: &ClientObject) {
let data = client.get_data();
let socket_path = self.imp().socket_path.borrow();
let socket_path = socket_path.as_ref().unwrap().as_path();
let host_name = data.hostname;
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 port = data.port;
let event = if client.active() {
FrontendEvent::DelClient(host_name, port as u16)
} else {
FrontendEvent::AddClient(host_name, port as u16, position)
};
let json = serde_json::to_string(&event).unwrap();
let Ok(mut stream) = UnixStream::connect(socket_path) else {
log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}");
return;
};
if let Err(e) = stream.write(json.as_bytes()) {
log::error!("error sending message: {e}");
};
}
fn setup_callbacks(&self) {
self.imp()
.add_client_button
.connect_clicked(clone!(@weak self as window => move |_| {
window.new_client();
window.set_placeholder_visible(false);
}));
}
fn connect_stream(&self) {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(v) => v,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
let socket_path = Path::new(xdg_runtime_dir.as_str())
.join("lan-mouse-socket.sock");
self.imp().socket_path.borrow_mut().replace(PathBuf::from(socket_path));
}
}

View File

@@ -0,0 +1,63 @@
use std::{cell::{Cell, RefCell}, path::PathBuf};
use glib::subclass::InitializingObject;
use adw::{prelude::*, ActionRow};
use adw::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, ListBox, gio};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
pub number: Cell<i32>,
#[template_child]
pub add_client_button: TemplateChild<Button>,
#[template_child]
pub client_list: TemplateChild<ListBox>,
#[template_child]
pub client_placeholder: TemplateChild<ActionRow>,
pub clients: RefCell<Option<gio::ListStore>>,
pub socket_path: RefCell<Option<PathBuf>>,
}
#[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 = gtk::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_button_clicked(&self, button: &Button) {
let number_increased = self.number.get() + 1;
self.number.set(number_increased);
button.set_label(&number_increased.to_string())
}
}
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
obj.setup_callbacks();
obj.connect_stream();
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}

View File

@@ -2,10 +2,10 @@ pub mod client;
pub mod config;
pub mod dns;
pub mod event;
pub mod request;
pub mod consumer;
pub mod producer;
pub mod backend;
pub mod frontend;
pub mod ioutils;

View File

@@ -1,111 +1,72 @@
use std::{sync::{mpsc, Arc}, process, env};
use std::{process, error::Error};
use env_logger::Env;
use lan_mouse::{
client::ClientManager,
consumer, producer,
config, event, request,
config::{Config, Frontend::{Cli, Gtk}}, event::server::Server,
frontend::{FrontendAdapter, cli},
};
fn usage() {
eprintln!("usage: {} [--backend <backend>] [--port <port>]",
env::args().next().unwrap_or("lan-mouse".into()));
}
#[cfg(all(unix, feature = "gtk"))]
use lan_mouse::frontend::gtk;
pub fn main() {
// parse config file
let config = match config::Config::new() {
Err(e) => {
eprintln!("{e}");
usage();
process::exit(1);
}
Ok(config) => config,
};
// port or default
let port = config.port;
// init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
env_logger::init_from_env(env);
// event channel for producing events
let (produce_tx, produce_rx) = mpsc::sync_channel(128);
// event channel for consuming events
let (consume_tx, consume_rx) = mpsc::sync_channel(128);
// create client manager
let client_manager = match ClientManager::new(&config) {
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
Ok(m) => m,
};
// start receiving client connection requests
let (request_server, request_thread) = match request::Server::listen(port) {
Err(e) => {
eprintln!("Could not bind to port {port}: {e}");
process::exit(1);
}
Ok(r) => r,
};
println!("Press Ctrl+Alt+Shift+Super to release the mouse");
// start producing and consuming events
let event_producer = match producer::start(produce_tx, client_manager.get_clients(), request_server) {
Err(e) => {
eprintln!("Could not start event producer: {e}");
None
},
Ok(p) => Some(p),
};
let event_consumer = match consumer::start(consume_rx, client_manager.get_clients(), config.backend) {
Err(e) => {
eprintln!("Could not start event consumer: {e}");
None
},
Ok(p) => Some(p),
};
if event_consumer.is_none() && event_producer.is_none() {
process::exit(1);
}
// start sending and receiving events
let event_server = match event::server::Server::new(port) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
};
let (receiver, sender) = match event_server.run(Arc::new(client_manager), produce_rx, consume_tx) {
Ok((r,s)) => (r,s),
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
};
request_thread.join().unwrap();
// stop receiving events and terminate event-consumer
if let Err(e) = receiver.join().unwrap() {
eprint!("{e}");
process::exit(1);
}
if let Some(thread) = event_consumer {
thread.join().unwrap();
}
// stop producing events and terminate event-sender
if let Some(thread) = event_producer {
thread.join().unwrap();
}
if let Err(e) = sender.join().unwrap() {
eprint!("{e}");
if let Err(e) = run() {
log::error!("{e}");
process::exit(1);
}
}
pub fn run() -> Result<(), Box<dyn Error>> {
// parse config file
let config = Config::new()?;
// start producing and consuming events
let producer = producer::create()?;
let consumer = consumer::create()?;
// create frontend communication adapter
let frontend_adapter = FrontendAdapter::new()?;
// start sending and receiving events
let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?;
// any threads need to be started after event_server sets up signal handling
match config.frontend {
#[cfg(all(unix, feature = "gtk"))]
Gtk => { gtk::start()?; }
#[cfg(any(not(feature = "gtk"), not(unix)))]
Gtk => panic!("gtk frontend requested but feature not enabled!"),
Cli => { cli::start()?; }
};
// this currently causes issues, because the clients from
// the config arent communicated to gtk yet.
if config.frontend == Gtk {
log::warn!("clients defined in config currently have no effect with the gtk frontend");
} else {
// add clients from config
config.get_clients().into_iter().for_each(|(c, h, p)| {
let host_name = match h {
Some(h) => format!(" '{}'", h),
None => "".to_owned(),
};
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);
});
}
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
// run event loop
event_server.run()?;
Ok(())
}

View File

@@ -1,10 +1,10 @@
use mio::event::Source;
use std::{error::Error, vec::Drain};
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
use crate::backend::producer;
#[cfg(unix)]
use std::env;
use std::{thread::{JoinHandle, self}, sync::mpsc::SyncSender, error::Error};
use crate::{client::{Client, ClientHandle}, event::Event, request::Server};
use crate::backend::producer;
#[cfg(unix)]
enum Backend {
@@ -12,41 +12,52 @@ enum Backend {
X11,
}
pub fn start(
produce_tx: SyncSender<(Event, ClientHandle)>,
clients: Vec<Client>,
request_server: Server,
) -> Result<JoinHandle<()>, Box<dyn Error>> {
Ok(thread::Builder::new()
.name("event producer".into())
.spawn(move || {
#[cfg(windows)]
producer::windows::run(produce_tx, request_server, clients);
pub fn create() -> Result<Box<dyn EventProducer>, Box<dyn Error>> {
#[cfg(windows)]
return Ok(Box::new(producer::windows::WindowsProducer::new()));
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => Backend::X11,
"wayland" => Backend::Wayland,
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[cfg(unix)]
match backend {
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
producer::x11::run(produce_tx, request_server, clients);
}
Backend::Wayland => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
producer::wayland::run(produce_tx, request_server, clients);
}
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => {
log::info!("XDG_SESSION_TYPE = x11 -> using X11 event producer");
Backend::X11
},
"wayland" => {
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event producer");
Backend::Wayland
}
})?)
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[cfg(unix)]
match backend {
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
Ok(Box::new(producer::x11::X11Producer::new()))
}
Backend::Wayland => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
Ok(Box::new(producer::wayland::WaylandEventProducer::new()?))
}
}
}
pub trait EventProducer: Source {
/// notify event producer of configuration changes
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
fn release(&mut self);
}

View File

@@ -1,139 +0,0 @@
use std::{
collections::HashMap,
error::Error,
fmt::Display,
io::prelude::*,
net::{SocketAddr, TcpListener, TcpStream},
sync::{Arc, RwLock},
thread::{self, JoinHandle},
};
use memmap::MmapMut;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum Request {
KeyMap,
Connect,
}
impl TryFrom<[u8; 4]> for Request {
fn try_from(buf: [u8; 4]) -> Result<Self, Self::Error> {
let val = u32::from_ne_bytes(buf);
match val {
x if x == Request::KeyMap as u32 => Ok(Self::KeyMap),
x if x == Request::Connect as u32 => Ok(Self::Connect),
_ => Err("Bad Request"),
}
}
type Error = &'static str;
}
#[derive(Clone)]
pub struct Server {
data: Arc<RwLock<HashMap<Request, MmapMut>>>,
}
impl Server {
fn handle_request(&self, mut stream: TcpStream) -> Result<(), Box<dyn Error>> {
let mut buf = [0u8; 4];
stream.read_exact(&mut buf)?;
match Request::try_from(buf) {
Ok(Request::KeyMap) => {
let data = self.data.read().unwrap();
let buf = data.get(&Request::KeyMap);
match buf {
None => {
stream.write(&0u32.to_ne_bytes())?;
}
Some(buf) => {
stream.write(&buf[..].len().to_ne_bytes())?;
stream.write(&buf[..])?;
}
}
stream.flush()?;
}
Ok(Request::Connect) => todo!(),
Err(msg) => eprintln!("{}", msg),
}
Ok(())
}
pub fn listen(port: u16) -> Result<(Server, JoinHandle<()>), Box<dyn Error>> {
let data: Arc<RwLock<HashMap<Request, MmapMut>>> = Arc::new(RwLock::new(HashMap::new()));
let listen_addr = SocketAddr::new("0.0.0.0".parse()?, port);
let server = Server { data };
let server_copy = server.clone();
let listen_socket = TcpListener::bind(listen_addr)?;
let thread = thread::Builder::new()
.name("tcp server".into())
.spawn(move || {
for stream in listen_socket.incoming() {
match stream {
Ok(stream) => {
if let Err(e) = server.handle_request(stream) {
eprintln!("{}", e);
}
}
Err(e) => {
eprintln!("{}", e);
}
}
}
})?;
Ok((server_copy, thread))
}
pub fn offer_data(&self, req: Request, d: MmapMut) {
self.data.write().unwrap().insert(req, d);
}
}
#[derive(Debug)]
pub struct BadRequest;
impl Display for BadRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BadRequest")
}
}
impl Error for BadRequest {}
pub fn request_data(addr: SocketAddr, req: Request) -> Result<Vec<u8>, Box<dyn Error>> {
// connect to server
let mut sock = match TcpStream::connect(addr) {
Ok(sock) => sock,
Err(e) => return Err(Box::new(e)),
};
// write the request to the socket
// convert to u32
let req: u32 = req as u32;
if let Err(e) = sock.write(&req.to_ne_bytes()) {
return Err(Box::new(e));
}
if let Err(e) = sock.flush() {
return Err(Box::new(e));
}
// read the response = (len, data) - len 0 means no data / bad request
// read len
let mut buf = [0u8; 8];
if let Err(e) = sock.read_exact(&mut buf[..]) {
return Err(Box::new(e));
}
let len = usize::from_ne_bytes(buf);
// check for bad request
if len == 0 {
return Err(Box::new(BadRequest {}));
}
// read the data
let mut data: Vec<u8> = vec![0u8; len];
if let Err(e) = sock.read_exact(&mut data[..]) {
return Err(Box::new(e));
}
Ok(data)
}